diff --git a/.github/workflows/arduino.yaml b/.github/workflows/arduino.yaml new file mode 100755 index 0000000..a4ebb00 --- /dev/null +++ b/.github/workflows/arduino.yaml @@ -0,0 +1,69 @@ +--- +name: arduino + +on: + push: + tags: + - 'v*.*.*' + +jobs: + + build: + permissions: write-all + + strategy: + matrix: + arduino-platform: ["esp8266:esp8266"] + include: + - arduino-platform: "esp8266:esp8266" + fqbn: "esp8266:esp8266:generic" + + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v3 + with: + submodules: recursive + + - name: Setup Arduino CLI + uses: arduino/setup-arduino-cli@v1 + + - name: Install platform + run: > + arduino-cli core install + --additional-urls=http://arduino.esp8266.com/stable/package_esp8266com_index.json + ${{ matrix.arduino-platform }} + + - name: Install time lib + run: arduino-cli lib install time wifimanager ezTime + + - name: Make timezones + run: python firmware/cities.py > firmware/cities.h + + - name: Compile Sketch + run: > + arduino-cli compile --fqbn ${{ matrix.fqbn }} -e + firmware + + - name: Create Release + id: create_release + uses: actions/create-release@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + tag_name: ${{ github.ref }} + release_name: Release ${{ github.ref }} + draft: false + prerelease: false + + - name: Upload Release Asset + id: upload-release-asset + uses: actions/upload-release-asset@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + upload_url: ${{ steps.create_release.outputs.upload_url }} + asset_path: ./firmware/build/esp8266.esp8266.generic/firmware.ino.bin + asset_name: nixiesp12.bin + asset_content_type: application/bin diff --git a/.gitignore b/.gitignore index bac989f..3510d95 100644 --- a/.gitignore +++ b/.gitignore @@ -3,9 +3,8 @@ _autosave* export *.kicad_pcb-bak *rescue.lib -firmware/*.bin *.drl -*.g* +*.gcode *.ps *.zip fp-info-cache diff --git a/firmware/.gitignore b/firmware/.gitignore new file mode 100644 index 0000000..23f622a --- /dev/null +++ b/firmware/.gitignore @@ -0,0 +1,2 @@ +build +cities.h diff --git a/firmware/Makefile b/firmware/Makefile index 54aa15b..6b95be6 100644 --- a/firmware/Makefile +++ b/firmware/Makefile @@ -1,23 +1,21 @@ -NAME=esp8266-1m-20210618-v1.16.bin +SKETCH_FOLDER := $(shell dirname $(realpath $(firstword $(MAKEFILE_LIST)))) +UPLOAD_PORT ?= /dev/ttyUSB0 -flash: - wget -c http://micropython.org/resources/firmware/${NAME} - esptool.py -p /dev/ttyUSB0 write_flash --flash_size=1MB 0 ${NAME} +all: $(SKETCH_FOLDER)/build/firmware.ino.bin -erase: - esptool.py -p /dev/ttyUSB0 erase_flash +$(SKETCH_FOLDER)/cities.h: cities.py + python3 cities.py > $(SKETCH_FOLDER)/cities.h -upload: - ampy -p /dev/ttyUSB0 put picoweb.py - ampy -p /dev/ttyUSB0 put timezone.py - ampy -p /dev/ttyUSB0 put main.py +$(SKETCH_FOLDER)/build/firmware.ino.bin: $(SKETCH_FOLDER)/firmware.ino $(SKETCH_FOLDER)/cities.h + arduino-cli compile -e -b esp8266:esp8266:generic $(SKETCH_FOLDER) + +deps: + arduino-cli core install \ + --additional-urls=http://arduino.esp8266.com/stable/package_esp8266com_index.json esp8266:esp8266 + arduino-cli lib install wifimanager ESP8266TimerInterrupt ezTime + +flash: $(SKETCH_FOLDER)/firmware.ino.bin + arduino-cli upload -b esp8266:esp8266:generic -p $(UPLOAD_PORT) $(SKETCH_FOLDER) console: - echo "Ctrl-A + Ctrl-Q to close Picocom" - picocom -b115200 /dev/ttyUSB0 - -clone_read: - esptool.py -p /dev/ttyUSB0 read_flash 0 0x100000 clone.bin - -clone_write: - esptool.py -p /dev/ttyUSB0 write_flash --flash_size=1MB 0 clone.bin + picocom -b 9600 $(UPLOAD_PORT) diff --git a/firmware/cities.py b/firmware/cities.py new file mode 100644 index 0000000..3ff5413 --- /dev/null +++ b/firmware/cities.py @@ -0,0 +1,42 @@ +import requests +import csv + +# Looks like ESP-IDF pulls in whole POSIX stack +# Some forums mention ESP-IDF uses this CSV + +coords = {} + +r = requests.get("https://gist.githubusercontent.com/erdem/8c7d26765831d0f9a8c62f02782ae00d/raw/248037cd701af0a4957cce340dabb0fd04e38f4c/countries.json") +data = r.json() +for j in data: + for tz in j["timezones"]: + coords[tz] = j["latlng"] + + +r = requests.get("https://raw.githubusercontent.com/nayarsystems/posix_tz_db/master/zones.csv") +cr = csv.reader(r.text.splitlines(), delimiter=',', quotechar='"') + +print("""const char cities[] = R"( +
+ + +)"; +""") diff --git a/firmware/firmware.ino b/firmware/firmware.ino new file mode 100644 index 0000000..facd33d --- /dev/null +++ b/firmware/firmware.ino @@ -0,0 +1,511 @@ +/* +https://github.com/laurivosandi/nixiesp12/blob/master/firmware/main.py +https://randomnerdtutorials.com/wifimanager-with-esp8266-autoconnect-custom-parameter-and-manage-your-ssid-and-password/ +*/ +#include +#include +#include +#include "cities.h" + +// #define DEBUG 1 +// #define DIMMING_ENABLED 1 +// #define TEST_SEQUENCE 1 + +#define PIN_CLOCK 3 +#define PIN_DATA 2 +#define PIN_LATCH 0 + +#define SUNRISE 6 +#define SUNSET 22 + +// ezTime structs +tmElements_t tm; +Timezone local; + +int configDimmingDutyCycle = 0; + +int current_dimming_duty_cycle; +enum typeOperationMode { + OPERATION_MODE_NORMAL, + OPERATION_MODE_DIMMED, +} operationModeCurrent = OPERATION_MODE_NORMAL; + +#define DISPLAY_MODE_TIME 1 +#define DISPLAY_MODE_DATE 2 +#define DISPLAY_MODE_DATETIME 3 + +int configDisplayModesEnabled = 1; +int displayModeCurrent = 1; + +WiFiManager wm; + +const char displayModesCombobox[] = R"( +
+ + +)"; + +#ifdef DIMMING_ENABLED +const char dimmerSliderSnippet[] = R"( +
+ + +)"; +#endif + +WiFiManagerParameter paramNetworkTimeServer("networkTimeServer", "Network time server", "ee.pool.ntp.org", 63); +WiFiManagerParameter paramDisplayMode("displayMode", "Will be hidden", "1", 2); +WiFiManagerParameter paramDisplayModeCombobox(displayModesCombobox); +WiFiManagerParameter paramCity(cities); +WiFiManagerParameter paramTimezone("timezone", "Timezone encoding", "EET-2EEST,M3.5.0/3,M10.5.0/4", 30); +//WiFiManagerParameter paramLong("long", "Longitude", "26", 10); +//WiFiManagerParameter paramLat("lat", "Latitude", "59", 10); + +#ifdef DIMMING_ENABLED +WiFiManagerParameter paramDimmingDutyCycle("dimming_duty_cycle", "", "1000", 4); +WiFiManagerParameter paramDimmingDutyCycleSlider(dimmerSliderSnippet); + +// Dimmer settings +volatile long displayInterruptCount = 0; +#endif + +volatile bool blink = true; +int lookup[] = {11, 9, 12, 8, 0, 4, 1, 3, 2, 10}; + +void ICACHE_RAM_ATTR bitbang_bit(int value){ + if(value & 1){ + digitalWrite(PIN_DATA, HIGH); + } + else{ + digitalWrite(PIN_DATA, LOW); + } + digitalWrite(PIN_CLOCK, HIGH); + digitalWrite(PIN_CLOCK, LOW); +} + +void ICACHE_RAM_ATTR bitbang_digit(int digit){ + int i = 0; + if (!blink && timeStatus() != timeSet) { + for(i=0;i<4;i++){ + bitbang_bit(1); + } + } else { + for(i=0;i<4;i++){ + bitbang_bit(lookup[digit] << i >> 3); + } + } +} + +void ICACHE_RAM_ATTR renderTest(int j) { + for(int i=0; i<6; i++){ + bitbang_bit(0); + bitbang_digit(j); + bitbang_bit(0); + bitbang_bit(0); + bitbang_bit(0); + } + digitalWrite(PIN_LATCH, HIGH); + digitalWrite(PIN_LATCH, LOW); +} + +void ICACHE_RAM_ATTR renderTime(){ + int hour = tm.Hour; + int minute = tm.Minute; + int second = tm.Second; + bitbang_bit(0); + bitbang_digit(hour / 10); + bitbang_bit(0); + bitbang_bit(0); + bitbang_bit(0); + + bitbang_bit(0); + bitbang_digit(hour % 10); + bitbang_bit(blink); + bitbang_bit(blink); + bitbang_bit(blink); + + bitbang_bit(blink); + bitbang_digit(minute / 10); + bitbang_bit(0); + bitbang_bit(0); + bitbang_bit(0); + + bitbang_bit(0); + bitbang_digit(minute % 10); + bitbang_bit(blink); + bitbang_bit(blink); + bitbang_bit(blink); + + bitbang_bit(blink); + bitbang_digit(second / 10); + bitbang_bit(0); + bitbang_bit(0); + bitbang_bit(0); + + bitbang_bit(0); + bitbang_digit(second % 10); + bitbang_bit(0); + bitbang_bit(0); + bitbang_bit(0); +} + +void ICACHE_RAM_ATTR renderDate(){ + int day = tm.Day; + int month = tm.Month; + int year = tm.Year-30; + bitbang_bit(0); + bitbang_digit((year) / 10); + bitbang_bit(0); + bitbang_bit(0); + bitbang_bit(0); + + bitbang_bit(0); + bitbang_digit((year) % 10); + bitbang_bit(0); + bitbang_bit(1); + bitbang_bit(0); + + bitbang_bit(0); + bitbang_digit(month/ 10); + bitbang_bit(0); + bitbang_bit(0); + bitbang_bit(0); + + bitbang_bit(0); + bitbang_digit(month % 10); + bitbang_bit(0); + bitbang_bit(1); + bitbang_bit(0); + + bitbang_bit(0); + bitbang_digit(day / 10); + bitbang_bit(0); + bitbang_bit(0); + bitbang_bit(0); + + bitbang_bit(0); + bitbang_digit(day % 10); + bitbang_bit(0); + bitbang_bit(0); + bitbang_bit(0); +} + +void ICACHE_RAM_ATTR renderDisplay() { + switch (configDisplayModesEnabled) { + case 1: + renderTime(); + break; + case 2: + renderDate(); + break; + case 3: + if (millis() % 30000 < 15000) { + renderTime(); + } else { + renderDate(); + } + break; + } + digitalWrite(PIN_LATCH, HIGH); + digitalWrite(PIN_LATCH, LOW); +} + +void ICACHE_RAM_ATTR clearDisplay() { + for(int i=0; i<6; i++){ + bitbang_bit(1); + bitbang_bit(0); + bitbang_bit(0); + bitbang_bit(0); + bitbang_bit(0); + bitbang_bit(1); + bitbang_bit(1); + bitbang_bit(1); + } + + digitalWrite(PIN_LATCH, HIGH); + digitalWrite(PIN_LATCH, LOW); +} + +int counter = 0; + +struct timeval tv; + +#ifdef DIMMING_ENABLED +void ICACHE_RAM_ATTR dimmerTimerCallback() { + noInterrupts(); + displayInterruptCount++; + gettimeofday(&tv, NULL); + blink = tv.tv_usec < 500000; + int j = current_dimming_duty_cycle; + if (j < configDimmingDutyCycle) { + j = configDimmingDutyCycle; + } + + // if (counter == 0) { + renderDisplay(); + timer1_write(j+1); + // counter = 1; + // } else if (counter == 1) { + // clearDisplay(); + // timer1_write(3840-j+1); + // counter = 0; + // } + interrupts(); +} +#endif + +int loadClockConfig() { + + unsigned char timezone[30] = {'\0'}; + File file = LittleFS.open("/timezone", "r"); + if (!file) { return 1; } + if (!file.read(timezone, sizeof(timezone))) { return 2; } + Serial.print("Using timezone: "); + Serial.println((const char*)timezone); + + unsigned char timeserver[63] = {'\0'}; + file = LittleFS.open("/timeserver", "r"); + if (!file) { return 3; } + if (!file.read(timeserver, sizeof(timeserver))) { return 4; } + Serial.print("Using time server: "); + Serial.println((const char*)timeserver); + + setServer((const char*)timeserver); + local.setPosix((const char*)timezone); + + unsigned char modes[1] = {'\0'}; + file = LittleFS.open("/modes", "r"); + if (!file) { return 5; } + if (!file.read(modes, sizeof(modes))) { return 6; } + configDisplayModesEnabled = atoi((const char*)modes); + Serial.print("Enabled display modes:"); + if (configDisplayModesEnabled & DISPLAY_MODE_DATE) { + Serial.print(" DATE"); + } + if (configDisplayModesEnabled & DISPLAY_MODE_TIME) { + Serial.print(" TIME"); + } + Serial.println(); + + #ifdef DIMMING_ENABLED + unsigned char bufDimming[10] = {'\0'}; + file = LittleFS.open("/dimming", "r"); + if (!file) { return 7; } + if (!file.read(bufDimming, sizeof(bufDimming))) { return 8; } + configDimmingDutyCycle = atoi((const char*)bufDimming); + Serial.print("Night time dimming duty cycle: "); + Serial.println((const char*)bufDimming); + #endif + + return 0; +} + +void saveParamsCallback() { + File file = LittleFS.open("/timeserver", "w"); + file.print(paramNetworkTimeServer.getValue()); + file.close(); + + file = LittleFS.open("/timezone", "w"); + file.print(paramTimezone.getValue()); + file.close(); + + #ifdef DIMMING_ENABLED + file = LittleFS.open("/dimming", "w"); + file.print(paramDimmingDutyCycle.getValue()); + file.close(); + #endif + + file = LittleFS.open("/modes", "w"); + file.print(paramDisplayMode.getValue()); + file.close(); + + loadClockConfig(); +} + +void handleMetrics(){ + char tbuf[30]; + + String buf = ""; + + buf += "nixie_sketch_size_bytes "; + buf += ESP.getSketchSize(); + buf += "\n"; + + buf += "nixie_flash_space_bytes "; + buf += ESP.getFlashChipRealSize(); + buf += "\n"; + + buf += "nixie_free_heap_bytes "; + buf += ESP.getFreeHeap(); + buf += "\n"; + + #ifdef DIMMING_ENABLED + buf += "nixie_display_interrupt_count "; + buf += displayInterruptCount; + buf += "\n"; + #endif + + buf += "nixie_ntp_last_sync_timestamp_seconds "; + buf += lastNtpUpdateTime(); + buf += "\n"; + + wm.server->send(200, "text/plain", buf); +} + +void initializePins() { + pinMode(PIN_CLOCK, OUTPUT); + pinMode(PIN_LATCH, OUTPUT); + pinMode(PIN_DATA, OUTPUT); + digitalWrite(PIN_CLOCK, LOW); + digitalWrite(PIN_LATCH, LOW); + digitalWrite(PIN_DATA, LOW); + clearDisplay(); +} + +void setup() { + Serial.begin(9600); + Serial.println("Nixie clock booting up"); + initializePins(); + + #ifdef DEBUG + setDebug(INFO); + #endif + + #ifdef TEST_SEQUENCE + for(int i = 0; i < 10; i++) { + renderTest(i); + delay(3000); + } + #endif + + wm.setDebugOutput(true); + wm.setMinimumSignalQuality(50); + + if (!LittleFS.begin()) { + Serial.println("LittleFS mount failed"); + } else { + if(loadClockConfig() != 0) { + Serial.println("Failed to load clock configuration from LittleFS"); + } else { + Serial.println("Configuration loaded"); + } + } + wm.addParameter(¶mNetworkTimeServer); + wm.addParameter(¶mCity); + wm.addParameter(¶mTimezone); + //wm.addParameter(¶mLong); + //wm.addParameter(¶mLat); + wm.addParameter(¶mDisplayMode); + wm.addParameter(¶mDisplayModeCombobox); + + #ifdef DIMMING_ENABLED + wm.addParameter(¶mDimmingDutyCycle); + wm.addParameter(¶mDimmingDutyCycleSlider); + #endif + + wm.setSaveParamsCallback(saveParamsCallback); + wm.setShowInfoUpdate(false); // https://github.com/tzapu/WiFiManager/issues/1262 + wm.setShowInfoErase(false); + wm.setConfigPortalBlocking(false); + + Serial.println("Autostarting wireless"); + + wm.autoConnect(); + + Serial.println("Starting config portal"); + + + wm.startConfigPortal(); + + #ifdef DEBUG + wm.server->on("/metrics", handleMetrics); + #else + wm.setDebugOutput(false); + #endif + + renderNormal(); +} + + +void renderNormal() { + gettimeofday(&tv, NULL); + blink = tv.tv_usec < 500000; + breakTime(local.now(), tm); + + renderDisplay(); + delay((500 - (local.ms(LAST_READ) % 500)) + 1); + + #ifdef DEBUG + Serial.print(1970+tm.Year); + Serial.print("-"); + Serial.print(tm.Month); + Serial.print("-"); + Serial.print(tm.Day); + Serial.print(" "); + Serial.print(tm.Hour); + Serial.print(":"); + Serial.print(tm.Minute); + Serial.print(":"); + Serial.print(tm.Second); + Serial.print("."); + Serial.println(local.ms(LAST_READ)); + #endif +} + +void loop() { + wm.process(); + events(); // this invokes yield() + + #ifdef DIMMING_ENABLED + switch (operationModeCurrent) { + case OPERATION_MODE_NORMAL: + + if ((tm.Hour < SUNRISE || tm.Hour > SUNSET) && timeStatus() == timeSet) { + operationModeCurrent = OPERATION_MODE_DIMMED; + #ifdef DEBUG + Serial.println("Clock synchronized, disabling wireless, enabling dimming"); + #endif + + WiFi.disconnect(); + WiFi.mode(WIFI_OFF); + + timer1_attachInterrupt(dimmerTimerCallback); + timer1_isr_init(); + timer1_enable(TIM_DIV256, TIM_EDGE, TIM_SINGLE); + timer1_write(100); + } else { + renderNormal(); + } + break; + case OPERATION_MODE_DIMMED: + if (tm.Hour > SUNRISE && tm.Hour < SUNSET) { + operationModeCurrent = OPERATION_MODE_NORMAL; + timer1_detachInterrupt(); + timer1_disable(); + + #ifdef DEBUG + Serial.println("Disabling dimming"); + #endif + + WiFi.mode(WIFI_STA); + } else if (tm.Hour == SUNRISE) { + current_dimming_duty_cycle = tm.Minute << 6; + } else if (tm.Hour == SUNSET) { + current_dimming_duty_cycle = (61 - tm.Minute) << 6; + } + break; + } + #else + renderNormal(); + #endif +} diff --git a/firmware/main.py b/firmware/main.py deleted file mode 100644 index 79a6302..0000000 --- a/firmware/main.py +++ /dev/null @@ -1,187 +0,0 @@ -import gc -import network -import picoweb -import json -from time import sleep_ms -from timezone import TIMEZONES - -app = picoweb.WebApp(__name__) -ap_if = network.WLAN(network.AP_IF) -sta_if = network.WLAN(network.STA_IF) -sta_if.active(True) -nets = sta_if.scan() -config = dict() -try: - with open("config.json") as fh: - config = json.loads(fh.read()) - sta_if.connect(config.get("ssid"), config.get("password")) -except OSError: - pass - -print("Scanning for wireless networks...") - -@app.route("/connect") -def index(req, resp): - if req.method == "POST": - yield from req.read_form_data() - else: - req.parse_qs() - yield from picoweb.start_response(resp) - with open("config.json", "w") as fh: - fh.write(json.dumps(req.form)) - yield from resp.awrite("Setting saved please power cycle device") - - -@app.route("/") -def index(req, resp): - print("Serving index") - yield from picoweb.start_response(resp) - yield from resp.awrite("") - yield from resp.awrite("") - yield from resp.awrite("") - yield from resp.awrite("") - yield from resp.awrite("") - yield from resp.awrite("Welcome to NixiESP12 configuration wizard") - yield from resp.awrite("

Detected wireless networks:

") - yield from resp.awrite("
") - yield from resp.awrite("

Select wireless network:

") - yield from resp.awrite("") - yield from resp.awrite("

Wireless password is applicable:

") - yield from resp.awrite("") - yield from resp.awrite("

Timezone:

") - yield from resp.awrite("") - yield from resp.awrite("

NTP resynchronization interval:

") - yield from resp.awrite("") - yield from resp.awrite("

 

") - yield from resp.awrite("") - yield from resp.awrite("
") - yield from resp.awrite("") - yield from resp.awrite("") - -timed_out = True -if config: - print("Connecting to", config.get("ssid")) - for j in range(0,30): - if sta_if.isconnected(): - ap_if.active(False) - timed_out = False - break - sleep_ms(200) - -if timed_out: - ap_if.active(True) - print("Starting setup wizard on", ap_if.ifconfig()) - app.run() - - -TIMEZONE = TIMEZONES[int(config.get("timezone", 30))][1] -print("Using timezone", TIMEZONES[int(config.get("timezone", 30))]) -RESYNC = int(config.get("interval")) # Resync once in 8 hours -print("NTP resynchronization interval", RESYNC, "seconds") -DEBUG = False - -print("Press Ctrl-C now to abort main.py execution and retain keyboard input") -sleep_ms(2000) - -import time -import ntptime -from machine import Pin, Timer - -# Note that keyboard input is lost beyond this point! -clock = Pin(3, mode=Pin.OUT) -latch = Pin(0, mode=Pin.OUT) -data = Pin(2, mode=Pin.OUT) -blink = False -lookup = 11, 9, 12, 8, 0, 4, 1, 3, 2, 10 -countdown = 0 - -def bitbang_bit(value): - if value & 1: - data.on() - else: - data.off() - clock.on() - clock.off() - -def bitbang_digit(digit): - bitbang_bit(blink) - for i in range(0,4): - bitbang_bit(lookup[digit] << i >> 3) - bitbang_bit(blink) - bitbang_bit(blink) - bitbang_bit(blink) - -def dst_offset(month, day, dow): - if month < 3 or month > 10: - return 0 - if month > 3 and month < 10: - return 1 - previous_sunday = day - dow - if month == 3: - return int(previous_sunday >= 25) - return int(previous_sunday < 25) - - -def dump_time(year, month, day, hour, minute, second, dow): - offset = dst_offset(month, day, dow) - if DEBUG: - print("Time is %02d:%02d:%02d, dst offset %d" % (hour, minute, second, offset)) - hour = (hour + TIMEZONE + offset) % 24 - bitbang_digit(hour // 10) - bitbang_digit(hour % 10) - bitbang_digit(minute // 10) - bitbang_digit(minute % 10) - bitbang_digit(second // 10) - bitbang_digit(second % 10) - -# RTC accuracy is still garbage, time.ticks_ms() which is bound to CPU ticks seems to be more accurate -# https://forum.micropython.org/viewtopic.php?t=3251#p19092 - - -# Boot up test sequence -for j in range(0, 10): - for i in range(0, 6): - bitbang_digit(j) - latch.on() - latch.off() - sleep_ms(500) - - -while True: - if countdown <= 0: - try: - ticks_then, time_then = time.ticks_ms(), ntptime.time() - except OSError: - sleep_ms(500) - print("Resync failed") - continue - else: - countdown = RESYNC - print("Resync done") - else: - year, month, day, hour, minute, second, dow, _ = time.localtime(time_then + (time.ticks_ms() - ticks_then) // 1000) - sleep_ms(500-(time.ticks_ms() - ticks_then) % 1000) - blink = True - dump_time(year, month, day, hour, minute, second, dow) - latch.on() - latch.off() - countdown -= 1 - - year, month, day, hour, minute, second, dow, _ = time.localtime(time_then + (time.ticks_ms() - ticks_then) // 1000) - sleep_ms(1001-(time.ticks_ms() - ticks_then) % 1000) - blink = False - dump_time(year, month, day, hour, minute, second, dow) - latch.on() - latch.off() - -main() diff --git a/firmware/picoweb.py b/firmware/picoweb.py deleted file mode 100644 index 76d1908..0000000 --- a/firmware/picoweb.py +++ /dev/null @@ -1,279 +0,0 @@ -# Picoweb web pico-framework for MicroPython -# Copyright (c) 2014-2018 Paul Sokolovsky -# SPDX-License-Identifier: MIT -import sys -import gc -import micropython -import utime -import uio -import ure as re -import uerrno -import uasyncio as asyncio - -def unquote_plus(s): - # TODO: optimize - s = s.replace("+", " ") - arr = s.split("%") - arr2 = [chr(int(x[:2], 16)) + x[2:] for x in arr[1:]] - return arr[0] + "".join(arr2) - -def parse_qs(s): - res = {} - if s: - pairs = s.split("&") - for p in pairs: - vals = [unquote_plus(x) for x in p.split("=", 1)] - if len(vals) == 1: - vals.append(True) - old = res.get(vals[0]) - if old is not None: - if not isinstance(old, list): - old = [old] - res[vals[0]] = old - old.append(vals[1]) - else: - res[vals[0]] = vals[1] - return res - -def get_mime_type(fname): - # Provide minimal detection of important file - # types to keep browsers happy - if fname.endswith(".html"): - return "text/html" - if fname.endswith(".css"): - return "text/css" - if fname.endswith(".png") or fname.endswith(".jpg"): - return "image" - return "text/plain" - -def sendstream(writer, f): - buf = bytearray(64) - while True: - l = f.readinto(buf) - if not l: - break - yield from writer.awrite(buf, 0, l) - - -def jsonify(writer, dict): - import ujson - yield from start_response(writer, "application/json") - yield from writer.awrite(ujson.dumps(dict)) - -def start_response(writer, content_type="text/html", status="200", headers=None): - yield from writer.awrite("HTTP/1.0 %s NA\r\n" % status) - yield from writer.awrite("Content-Type: ") - yield from writer.awrite(content_type) - if not headers: - yield from writer.awrite("\r\n\r\n") - return - yield from writer.awrite("\r\n") - if isinstance(headers, bytes) or isinstance(headers, str): - yield from writer.awrite(headers) - else: - for k, v in headers.items(): - yield from writer.awrite(k) - yield from writer.awrite(": ") - yield from writer.awrite(v) - yield from writer.awrite("\r\n") - yield from writer.awrite("\r\n") - -def http_error(writer, status): - yield from start_response(writer, status=status) - yield from writer.awrite(status) - - -class HTTPRequest: - - def __init__(self): - pass - - def read_form_data(self): - size = int(self.headers[b"Content-Length"]) - data = yield from self.reader.read(size) - form = parse_qs(data.decode()) - self.form = form - - def parse_qs(self): - form = parse_qs(self.qs) - self.form = form - - -class WebApp: - - def __init__(self, pkg, routes=None): - if routes: - self.url_map = routes - else: - self.url_map = [] - if pkg and pkg != "__main__": - self.pkg = pkg.split(".", 1)[0] - else: - self.pkg = None - self.mounts = [] - self.inited = False - # Instantiated lazily - self.template_loader = None - self.headers_mode = "parse" - - def parse_headers(self, reader): - headers = {} - while True: - l = yield from reader.readline() - if l == b"\r\n": - break - k, v = l.split(b":", 1) - headers[k] = v.strip() - return headers - - def _handle(self, reader, writer): - close = True - try: - request_line = yield from reader.readline() - if request_line == b"": - yield from writer.aclose() - return - req = HTTPRequest() - # TODO: bytes vs str - request_line = request_line.decode() - method, path, proto = request_line.split() - path = path.split("?", 1) - qs = "" - if len(path) > 1: - qs = path[1] - path = path[0] - - #print("================") - #print(req, writer) - #print(req, (method, path, qs, proto), req.headers) - - # Find which mounted subapp (if any) should handle this request - app = self - while True: - found = False - for subapp in app.mounts: - root = subapp.url - #print(path, "vs", root) - if path[:len(root)] == root: - app = subapp - found = True - path = path[len(root):] - if not path.startswith("/"): - path = "/" + path - break - if not found: - break - - # We initialize apps on demand, when they really get requests - if not app.inited: - app.init() - - # Find handler to serve this request in app's url_map - found = False - for e in app.url_map: - pattern = e[0] - handler = e[1] - extra = {} - if len(e) > 2: - extra = e[2] - - if path == pattern: - found = True - break - elif not isinstance(pattern, str): - # Anything which is non-string assumed to be a ducktype - # pattern matcher, whose .match() method is called. (Note: - # Django uses .search() instead, but .match() is more - # efficient and we're not exactly compatible with Django - # URL matching anyway.) - m = pattern.match(path) - if m: - req.url_match = m - found = True - break - - if not found: - headers_mode = "skip" - else: - headers_mode = extra.get("headers", self.headers_mode) - - if headers_mode == "skip": - while True: - l = yield from reader.readline() - if l == b"\r\n": - break - elif headers_mode == "parse": - req.headers = yield from self.parse_headers(reader) - else: - assert headers_mode == "leave" - - if found: - req.method = method - req.path = path - req.qs = qs - req.reader = reader - close = yield from handler(req, writer) - else: - yield from start_response(writer, status="404") - yield from writer.awrite("404\r\n") - #print(req, "After response write") - except Exception as e: - pass - - if close is not False: - yield from writer.aclose() - - def mount(self, url, app): - "Mount a sub-app at the url of current app." - # Inspired by Bottle. It might seem that dispatching to - # subapps would rather be handled by normal routes, but - # arguably, that's less efficient. Taking into account - # that paradigmatically there's difference between handing - # an action and delegating responisibilities to another - # app, Bottle's way was followed. - app.url = url - self.mounts.append(app) - - def route(self, url, **kwargs): - def _route(f): - self.url_map.append((url, f, kwargs)) - return f - return _route - - def add_url_rule(self, url, func, **kwargs): - # Note: this method skips Flask's "endpoint" argument, - # because it's alleged bloat. - self.url_map.append((url, func, kwargs)) - - def _load_template(self, tmpl_name): - if self.template_loader is None: - import utemplate.source - self.template_loader = utemplate.source.Loader(self.pkg, "templates") - return self.template_loader.load(tmpl_name) - - def render_template(self, writer, tmpl_name, args=()): - tmpl = self._load_template(tmpl_name) - for s in tmpl(*args): - yield from writer.awrite(s) - - def render_str(self, tmpl_name, args=()): - #TODO: bloat - tmpl = self._load_template(tmpl_name) - return ''.join(tmpl(*args)) - - def init(self): - """Initialize a web application. This is for overriding by subclasses. - This is good place to connect to/initialize a database, for example.""" - self.inited = True - - def run(self, host="0.0.0.0", port=80, lazy_init=False): - gc.collect() - self.init() - if not lazy_init: - for app in self.mounts: - app.init() - loop = asyncio.get_event_loop() - loop.create_task(asyncio.start_server(self._handle, host, port)) - loop.run_forever() - loop.close() - diff --git a/firmware/timezone.py b/firmware/timezone.py deleted file mode 100644 index d3a4c66..0000000 --- a/firmware/timezone.py +++ /dev/null @@ -1,75 +0,0 @@ -TIMEZONES = ( - (0, -12, "(GMT-12:00) International Date Line West"), - (0, -11, "(GMT-11:00) Midway Island, Samoa"), - (0, -10, "(GMT-10:00) Hawaii"), - (1, -9, "(GMT-09:00) Alaska"), - (1, -8, "(GMT-08:00) Pacific Time (US & Canada)"), - (1, -8, "(GMT-08:00) Tijuana, Baja California"), - (0, -7, "(GMT-07:00) Arizona"), - (1, -7, "(GMT-07:00) Chihuahua, La Paz, Mazatlan"), - (1, -7, "(GMT-07:00) Mountain Time (US & Canada)"), - (0, -6, "(GMT-06:00) Central America"), - (1, -6, "(GMT-06:00) Central Time (US & Canada)"), - (1, -6, "(GMT-06:00) Guadalajara, Mexico City, Monterrey"), - (0, -6, "(GMT-06:00) Saskatchewan"), - (0, -5, "(GMT-05:00) Bogota, Lima, Quito, Rio Branco"), - (1, -5, "(GMT-05:00) Eastern Time (US & Canada)"), - (1, -5, "(GMT-05:00) Indiana (East)"), - (1, -4, "(GMT-04:00) Atlantic Time (Canada)"), - (0, -4, "(GMT-04:00) Caracas, La Paz"), - (0, -4, "(GMT-04:00) Manaus"), - (1, -4, "(GMT-04:00) Santiago"), - (1, -3, "(GMT-03:00) Brasilia"), - (0, -3, "(GMT-03:00) Buenos Aires, Georgetown"), - (1, -3, "(GMT-03:00) Greenland"), - (1, -3, "(GMT-03:00) Montevideo"), - (1, -2, "(GMT-02:00) Mid-Atlantic"), - (0, -1, "(GMT-01:00) Cape Verde Is."), - (1, -1, "(GMT-01:00) Azores"), - (0, 0, "(GMT+00:00) Casablanca, Monrovia, Reykjavik"), - (1, 0, "(GMT+00:00) Greenwich Mean Time : Dublin, Edinburgh, Lisbon, London"), - (1, 1, "(GMT+01:00) Amsterdam, Berlin, Bern, Rome, Stockholm, Vienna"), - (1, 1, "(GMT+01:00) Belgrade, Bratislava, Budapest, Ljubljana, Prague"), - (1, 1, "(GMT+01:00) Brussels, Copenhagen, Madrid, Paris"), - (1, 1, "(GMT+01:00) Sarajevo, Skopje, Warsaw, Zagreb"), - (1, 1, "(GMT+01:00) West Central Africa"), - (1, 2, "(GMT+02:00) Amman"), - (1, 2, "(GMT+02:00) Athens, Bucharest, Istanbul"), - (1, 2, "(GMT+02:00) Beirut"), - (1, 2, "(GMT+02:00) Cairo"), - (0, 2, "(GMT+02:00) Harare, Pretoria"), - (1, 2, "(GMT+02:00) Helsinki, Kyiv, Riga, Sofia, Tallinn, Vilnius"), - (1, 2, "(GMT+02:00) Jerusalem"), - (1, 2, "(GMT+02:00) Minsk"), - (1, 2, "(GMT+02:00) Windhoek"), - (0, 3, "(GMT+03:00) Kuwait, Riyadh, Baghdad"), - (1, 3, "(GMT+03:00) Moscow, St. Petersburg, Volgograd"), - (0, 3, "(GMT+03:00) Nairobi"), - (0, 3, "(GMT+03:00) Tbilisi"), - (0, 4, "(GMT+04:00) Abu Dhabi, Muscat"), - (1, 4, "(GMT+04:00) Baku"), - (1, 4, "(GMT+04:00) Yerevan"), - (1, 5, "(GMT+05:00) Yekaterinburg"), - (0, 5, "(GMT+05:00) Islamabad, Karachi, Tashkent"), - (1, 6, "(GMT+06:00) Almaty, Novosibirsk"), - (0, 6, "(GMT+06:00) Astana, Dhaka"), - (0, 7, "(GMT+07:00) Bangkok, Hanoi, Jakarta"), - (1, 7, "(GMT+07:00) Krasnoyarsk"), - (0, 8, "(GMT+08:00) Beijing, Chongqing, Hong Kong, Urumqi"), - (0, 8, "(GMT+08:00) Kuala Lumpur, Singapore"), - (0, 8, "(GMT+08:00) Irkutsk, Ulaan Bataar"), - (0, 8, "(GMT+08:00) Perth"), - (0, 8, "(GMT+08:00) Taipei"), - (0, 9, "(GMT+09:00) Osaka, Sapporo, Tokyo"), - (0, 9, "(GMT+09:00) Seoul"), - (1, 9, "(GMT+09:00) Yakutsk"), - (0, 10, "(GMT+10:00) Brisbane"), - (1, 10, "(GMT+10:00) Canberra, Melbourne, Sydney"), - (1, 10, "(GMT+10:00) Hobart"), - (0, 10, "(GMT+10:00) Guam, Port Moresby"), - (1, 10, "(GMT+10:00) Vladivostok"), - (1, 11, "(GMT+11:00) Magadan, Solomon Is., New Caledonia"), - (1, 12, "(GMT+12:00) Auckland, Wellington"), - (0, 12, "(GMT+12:00) Fiji, Kamchatka, Marshall Is."), - (0, 13, "(GMT+13:00) Nuku'alofa") -)