From 56f5ee0af9fc5ea559fb55b0a4790e1b4bcc500f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lauri=20V=C3=B5sandi?= Date: Tue, 29 Jul 2025 21:56:27 +0300 Subject: [PATCH] More AI slop --- .github/workflows/esp32c3-arduino.yaml | 70 ++ .../{arduino.yaml => esp8266-arduino.yaml} | 10 +- .gitignore | 1 + firmware/esp32c3-arduino/.gitignore | 1 + firmware/esp32c3-arduino/Makefile | 34 + firmware/esp32c3-arduino/README.md | 56 ++ firmware/esp32c3-arduino/cities.py | 42 + firmware/esp32c3-arduino/esp32c3-arduino.ino | 801 ++++++++++++++++++ firmware/esp32c3-python/README.md | 52 ++ firmware/esp32c3-python/main.py | 239 ++++++ firmware/esp8266-arduino/.gitignore | 1 + firmware/{ => esp8266-arduino}/Makefile | 11 +- firmware/esp8266-arduino/README.md | 15 + .../esp8266-arduino.ino} | 0 14 files changed, 1323 insertions(+), 10 deletions(-) create mode 100644 .github/workflows/esp32c3-arduino.yaml rename .github/workflows/{arduino.yaml => esp8266-arduino.yaml} (84%) create mode 100644 firmware/esp32c3-arduino/.gitignore create mode 100644 firmware/esp32c3-arduino/Makefile create mode 100644 firmware/esp32c3-arduino/README.md create mode 100644 firmware/esp32c3-arduino/cities.py create mode 100644 firmware/esp32c3-arduino/esp32c3-arduino.ino create mode 100644 firmware/esp32c3-python/README.md create mode 100644 firmware/esp32c3-python/main.py create mode 100644 firmware/esp8266-arduino/.gitignore rename firmware/{ => esp8266-arduino}/Makefile (58%) create mode 100644 firmware/esp8266-arduino/README.md rename firmware/{firmware.ino => esp8266-arduino/esp8266-arduino.ino} (100%) diff --git a/.github/workflows/esp32c3-arduino.yaml b/.github/workflows/esp32c3-arduino.yaml new file mode 100644 index 0000000..8f89585 --- /dev/null +++ b/.github/workflows/esp32c3-arduino.yaml @@ -0,0 +1,70 @@ +--- +name: esp32c3-arduino + +on: + push: + tags: + - 'v*.*.*' + +jobs: + + build: + permissions: write-all + + strategy: + matrix: + arduino-platform: ["esp32:esp32"] + include: + - arduino-platform: "esp32:esp32" + fqbn: "esp32:esp32:esp32c3" + + 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 config set network.connection_timeout 600s + arduino-cli core install + ${{ matrix.arduino-platform }} + + - name: Install time lib + run: arduino-cli lib install time wifimanager ezTime + + - name: Make timezones + run: python firmware/cities.py > firmware/esp32c3-arduino/cities.h + + - name: Compile Sketch + run: > + arduino-cli compile --fqbn ${{ matrix.fqbn }} -e + firmware/esp32c3-arduino + + - 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/esp32c3-arduino/build/esp32.esp32.esp32c3/esp32c3-arduino.ino.bin + asset_name: nixiesp12-esp32c3-arduino.bin + asset_content_type: application/bin diff --git a/.github/workflows/arduino.yaml b/.github/workflows/esp8266-arduino.yaml similarity index 84% rename from .github/workflows/arduino.yaml rename to .github/workflows/esp8266-arduino.yaml index a4ebb00..0c8a682 100755 --- a/.github/workflows/arduino.yaml +++ b/.github/workflows/esp8266-arduino.yaml @@ -1,5 +1,5 @@ --- -name: arduino +name: esp8266-arduino on: push: @@ -39,12 +39,12 @@ jobs: run: arduino-cli lib install time wifimanager ezTime - name: Make timezones - run: python firmware/cities.py > firmware/cities.h + run: python firmware/cities.py > firmware/esp8266-arduino/cities.h - name: Compile Sketch run: > arduino-cli compile --fqbn ${{ matrix.fqbn }} -e - firmware + firmware/esp8266 - name: Create Release id: create_release @@ -64,6 +64,6 @@ jobs: 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_path: ./firmware/esp8266-arduino/build/esp8266.esp8266.generic/esp32c3-arduino.ino.bin + asset_name: nixiesp12-esp8266-arduino.bin asset_content_type: application/bin diff --git a/.gitignore b/.gitignore index 3510d95..003ecd7 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ +bin _autosave* *.bak export diff --git a/firmware/esp32c3-arduino/.gitignore b/firmware/esp32c3-arduino/.gitignore new file mode 100644 index 0000000..798d116 --- /dev/null +++ b/firmware/esp32c3-arduino/.gitignore @@ -0,0 +1 @@ +cities.h diff --git a/firmware/esp32c3-arduino/Makefile b/firmware/esp32c3-arduino/Makefile new file mode 100644 index 0000000..de066d1 --- /dev/null +++ b/firmware/esp32c3-arduino/Makefile @@ -0,0 +1,34 @@ +SKETCH_FOLDER := $(shell dirname $(realpath $(firstword $(MAKEFILE_LIST)))) +#UPLOAD_PORT ?= /dev/ttyACM0 +UPLOAD_PORT ?= /dev/ttyUSB0 + +all: $(SKETCH_FOLDER)/build/esp32c3-arduino.ino.bin + +$(SKETCH_FOLDER)/cities.h: ../cities.py + python3 ../cities.py > $(SKETCH_FOLDER)/cities.h + +$(SKETCH_FOLDER)/build/esp32c3-arduino.ino.bin: $(SKETCH_FOLDER)/esp32c3-arduino.ino $(SKETCH_FOLDER)/cities.h + arduino-cli compile -e -b esp32:esp32:esp32c3 $(SKETCH_FOLDER) + +deps: + arduino-cli config set network.connection_timeout 600s + arduino-cli core install esp32:esp32 + arduino-cli lib install wifimanager + +erase: + esptool.py --chip esp32c3 --port $(UPLOAD_PORT) erase_flash + +flash-dev: +# arduino-cli upload -b esp32:esp32:esp32c3:CDCOnBoot=cdc -p $(UPLOAD_PORT) $(SKETCH_FOLDER) + arduino-cli compile --fqbn esp32:esp32:esp32c3:CDCOnBoot=cdc $(SKETCH_FOLDER) --upload -p /dev/ttyACM0 +# arduino-cli compile --fqbn esp32:esp32:esp32c3 $(SKETCH_FOLDER) --upload -p $(UPLOAD_PORT) --build-property build.extra_flags=-Os + +flash: + arduino-cli compile --fqbn esp32:esp32:esp32c3 $(SKETCH_FOLDER) --upload -p $(UPLOAD_PORT) + + +console-dev: + picocom -b 115200 /dev/ttyACM0 + +console: + picocom -b 115200 $(UPLOAD_PORT) diff --git a/firmware/esp32c3-arduino/README.md b/firmware/esp32c3-arduino/README.md new file mode 100644 index 0000000..6601c71 --- /dev/null +++ b/firmware/esp32c3-arduino/README.md @@ -0,0 +1,56 @@ +# Arduino variant for ESP32C3 + +This firmware is designed to run on the [LilyGO T-01C3](https://lilygo.cc/products/t-01c3?srsltid=AfmBOopddAvO0gTIFNYZyTcoF1PxQjxZ0YAxuhoQviz9i5j4B0WOdg4l) board and serves as a drop-in replacement for the original ESP8266-based Nixie clock firmware. + +**Compared to the ESP8266, the ESP32C3 provides much better clock accuracy and reliability, thanks to its advanced hardware and improved timekeeping features.** +If you received a Nixie clock from me, you are eligible for a free hardware upgrade - just contact me! + +This firmware features a high refresh rate display with advanced dimming and blending effects, leveraging the ESP32's RMT (Remote Control) peripheral for precise timing and smooth transitions. + +## Highlights + +- **High Refresh Rate:** Achieves smooth digit transitions and flicker-free display using RMT hardware +- **Dimming & Blending:** Configurable dimming and blending for night mode and digit transitions +- **WiFiManager Configuration:** Easily configure all clock options via a web portal +- **Prometheus Metrics:** Exposes runtime and system metrics for monitoring + +## Configuration Options via WiFiManager + +- **NTP Time Server:** Set the network time server for clock synchronization +- **City:** Select your city for timezone presets +- **Timezone:** Manually specify the POSIX timezone string +- **Display Mode:** Choose between time, date, or alternating time/date display +- **Dimming Duty Cycle:** Adjust brightness for night mode +- **Dimming Start/End:** Set hours for automatic dimming +- **Blending Duration:** Control the duration of digit blending transitions +- **Hour Format:** Select 12-hour or 24-hour display + +## Prometheus Metrics + +The firmware exposes the following metrics for monitoring: + +- `nixie_boot_timestamp_seconds`: Boot time of the device +- `nixie_last_ntp_sync_timestamp_seconds`: Last successful NTP synchronization +- `nixie_littlefs_total_bytes`: Total LittleFS storage size +- `nixie_littlefs_used_bytes`: Used LittleFS storage +- `nixie_uptime_seconds`: Device uptime +- `nixie_display_task_iterations_total`: Number of display task iterations +- `nixie_ntp_sync_count_total`: Number of NTP sync events +- `nixie_wifi_manager_process_count_total`: Number of WiFiManager process events + +## Advanced Features + +- **Automatic NTP Sync:** Periodically synchronizes time with the configured NTP server +- **Stale NTP Detection:** Blinks all digits and colons if NTP sync is stale +- **Critical Section Display Updates:** Uses FreeRTOS and portMUX for safe, high-speed display updates + +## Getting Started + +1. Flash the firmware to your ESP32-C3 +2. Connect to the WiFiManager AP (SSID is based on device MAC) +3. Configure your clock settings via the web portal +4. Monitor metrics via Prometheus-compatible tools + +--- + +For more details, see the source code and comments. diff --git a/firmware/esp32c3-arduino/cities.py b/firmware/esp32c3-arduino/cities.py new file mode 100644 index 0000000..3ff5413 --- /dev/null +++ b/firmware/esp32c3-arduino/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/esp32c3-arduino/esp32c3-arduino.ino b/firmware/esp32c3-arduino/esp32c3-arduino.ino new file mode 100644 index 0000000..b37ca3f --- /dev/null +++ b/firmware/esp32c3-arduino/esp32c3-arduino.ino @@ -0,0 +1,801 @@ +#include +#include +#include +#include "esp_sntp.h" +#include +#include +#include +#include "cities.h" +#include + +// --- Configuration --- +#define CONFIG_CPU_FREQUENCY 80000000 +#define CONFIG_RMT_DIVISOR 50 +#define CONFIG_REFRESH_RATE 50 +#define REFRESH_CYCLE CONFIG_CPU_FREQUENCY / CONFIG_RMT_DIVISOR / CONFIG_REFRESH_RATE - 100 + +static_assert(REFRESH_CYCLE > 0, "REFRESH_CYCLE must be > 0"); +static_assert(REFRESH_CYCLE < 32768, "REFRESH_CYCLE must be < 32768"); + + +// --- Pin Definitions --- +#define PIN_CLOCK 20 +#define PIN_LATCH 9 +#define PIN_DATA 2 +#define PIN_UNUSED 11 + +// --- Colon and Symbol Definitions --- +#define COLON_LEFT_BOTTOM (1ULL << (17 + 16)) +#define COLON_LEFT_TOP (1ULL << (18 + 16)) +#define COLON_RIGHT_BOTTOM (1ULL << 17) +#define COLON_RIGHT_TOP (1ULL << 18) + +#define COLON_LEFT_BOTH (COLON_LEFT_TOP | COLON_LEFT_BOTTOM) +#define COLON_RIGHT_BOTH (COLON_RIGHT_TOP | COLON_RIGHT_BOTTOM) +#define COLON_BOTTOM_BOTH (COLON_LEFT_BOTTOM | COLON_RIGHT_BOTTOM) +#define COLON_TOP_BOTH (COLON_LEFT_TOP | COLON_RIGHT_TOP) +#define COLON_ALL (COLON_LEFT_BOTH | COLON_RIGHT_BOTH) + +#define IN15A_MICRO 0 +#define IN15A_PERCENT 2 +#define IN15A_PETA 3 +#define IN15A_KILO 4 +#define IN15A_MEGA 5 +#define IN15A_MILLI 6 +#define IN15A_PLUS 7 +#define IN15A_MINUS 8 +#define IN15A_PICO 9 + + +#define NTP_SYNC_TIMEOUT_SECONDS 8*3600 + +int configDimmingDutyCycle = 0; +int configDimmingStart = 0; +int configDimmingEnd = 0; +int configHourFormat = 12; + +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"( +
+ + +)"; + + +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); +#define CONFIG_TIMEZONE "EET-2EEST,M3.5.0/3,M10.5.0/4" // Estonia, adjust as needed + +const char dimmerSliderSnippet[] = R"( +
+ + +)"; +WiFiManagerParameter paramDimmingDutyCycle("dimming_duty_cycle", "", "1000", 6); +WiFiManagerParameter paramDimmingDutyCycleSlider(dimmerSliderSnippet); + +time_t bootTimestamp = 0; +time_t lastNtpSyncTimestamp = 0; + + +// Dimmer settings +volatile long displayInterruptCount = 0; + +const char blendingSliderSnippet[] = R"( +
+ + +)"; +WiFiManagerParameter paramBlendingDuration("blending_duration", "", "200", 4); +WiFiManagerParameter paramBlendingDurationSlider(blendingSliderSnippet); + + +const char dimmingStartCombobox[] = R"( +
+ + + +)"; + +const char dimmingEndCombobox[] = R"( +
+ + + +)"; + +const char hourFormatCombobox[] = R"( +
+ + + +)"; + +WiFiManagerParameter paramHourFormat("hour_format", "", "12", 3); +WiFiManagerParameter paramHourFormatCombobox(hourFormatCombobox); +WiFiManagerParameter paramDimmingStart("dimming_start", "", "22", 3); +WiFiManagerParameter paramDimmingStartCombobox(dimmingStartCombobox); +WiFiManagerParameter paramDimmingEnd("dimming_end", "", "6", 3); +WiFiManagerParameter paramDimmingEndCombobox(dimmingEndCombobox); + +// --- NTP Sync Counter --- +volatile unsigned long displayTaskIterationsCount = 0; +volatile unsigned long configBlendingDuration = 0; +volatile unsigned long ntpSyncCount = 0; +volatile unsigned long wifiManagerProcessCount = 0; + +void IRAM_ATTR timeavailable(struct timeval *t) { + ntpSyncCount++; + if (bootTimestamp == 0 && t) { + bootTimestamp = t->tv_sec; + } + if (t) { + lastNtpSyncTimestamp = t->tv_sec; + } +} + +#define RMT_TX_GPIO 9 + + +// --- State --- +uint64_t STATE_PREVIOUS = 0x787878787878ULL; +int STATE_REFRESH_MODE = 0; + +// --- Helper Functions --- +void spi_write_bytes(const uint8_t* data, size_t len) { + for (size_t i = 0; i < len; i++) { + uint8_t b = data[i]; + for (int bit = 7; bit >= 0; bit--) { + digitalWrite(PIN_CLOCK, LOW); + digitalWrite(PIN_DATA, (b >> bit) & 1); + digitalWrite(PIN_CLOCK, HIGH); + } + } +} + +int clamp(int v, int lower = -99, int upper = 99) { + if (v > upper) return upper; + if (v < lower) return lower; + return v; +} + +uint64_t render_digit(int j, int position = 0) { + static const int table[11] = {11, 9, 12, 8, 0, 4, 1, 3, 2, 10, 15}; + if (j < -1 || j > 9) j = 10; + if (j == -1) j = 10; + return ((uint64_t)table[j] << 3) << (position * 8); +} + +uint64_t render_digits(int d5, int d4, int d3, int d2, int d1, int d0) { + uint64_t z = 0; + int vals[6] = {d0, d1, d2, d3, d4, d5}; + for (int position = 0; position < 6; position++) { + z |= render_digit(vals[position], position); + } + return z; +} + +uint64_t render_time(bool colons = true) { + struct tm timeinfo; + time_t now = time(nullptr); + localtime_r(&now, &timeinfo); + int h = timeinfo.tm_hour % configHourFormat; + int m = timeinfo.tm_min; + int s = timeinfo.tm_sec; + return render_digits(h / 10, h % 10, m / 10, m % 10, s / 10, s % 10) | (colons ? COLON_ALL : 0); +} + +uint64_t render_date(bool colons = true) { + struct tm timeinfo; + time_t now = time(nullptr); + localtime_r(&now, &timeinfo); + int y = timeinfo.tm_year % 100; + int m = timeinfo.tm_mon + 1; + int d = timeinfo.tm_mday; + return render_digits(y / 10, y % 10, m / 10, m % 10, d / 10, d % 10) | (colons ? COLON_BOTTOM_BOTH : 0); +} + +uint64_t render_temperature(int t) { + int val = abs(clamp(t)); + return render_digits(-1, t < 0 ? IN15A_MINUS : -1, val / 10, val % 10, -1, -1) | COLON_RIGHT_TOP; +} + +void rmt_pulse(uint32_t duration) { + rmt_data_t items[3]; + items[0].level0 = 1; items[0].duration0 = 1; // latch immediately + items[0].level1 = 0; items[0].duration1 = duration; + items[1].level0 = 1; items[1].duration0 = 1; + items[1].level1 = 0; items[1].duration1 = 1; + rmtWriteAsync(RMT_TX_GPIO, items, duration == 0 ? 1 : 2); +} + +void display_static(uint64_t value) { + uint8_t bytes[6]; + for (int i = 0; i < 6; i++) bytes[i] = (value >> (8 * (5 - i))) & 0xFF; + spi_write_bytes(bytes, 6); + rmt_data_t items[1]; + items[0].level0 = 1; items[0].duration0 = 1; + items[0].level1 = 0; items[0].duration1 = 1; + rmtWrite(RMT_TX_GPIO, items, 1, RMT_WAIT_FOR_EVER); +} + +void display_dimmed(uint64_t value, int duty) { + uint8_t bytes[6]; + for (int i = 0; i < 6; i++) bytes[i] = (value >> (8 * (5 - i))) & 0xFF; + if (duty >= 32767) { + display_static(value); + } else { + spi_write_bytes(bytes, 6); + rmt_pulse(duty); + uint8_t blank[6] = {0x78, 0x78, 0x78, 0x78, 0x78, 0x78}; + spi_write_bytes(blank, 6); + } +} + +void display_blended(uint64_t value, uint64_t prev, float progression) { + int duty = 32767 * progression; + if (duty < 500) { duty = 500; } + uint8_t bytes[6], prev_bytes[6]; + for (int i = 0; i < 6; i++) { + bytes[i] = (value >> (8 * (5 - i))) & 0xFF; + prev_bytes[i] = (prev >> (8 * (5 - i))) & 0xFF; + } + if (progression >= 1.0) { + display_static(value); + } else { + spi_write_bytes(bytes, 6); + rmt_pulse(duty); + spi_write_bytes(prev_bytes, 6); + } +} + +bool isNtpStale() { + time_t now = time(nullptr); + return (lastNtpSyncTimestamp == 0) || (now - lastNtpSyncTimestamp > NTP_SYNC_TIMEOUT_SECONDS); +} + +void handleMetrics(){ + String buf = ""; + + + buf += "nixie_sketch_size_bytes "; + buf += ESP.getSketchSize(); + buf += "\n"; + + buf += "nixie_flash_space_bytes "; + buf += ESP.getFlashChipSize(); + buf += "\n"; + + buf += "nixie_free_heap_bytes "; + buf += ESP.getFreeHeap(); + buf += "\n"; + + buf += "nixie_min_free_heap_bytes "; + buf += ESP.getMinFreeHeap(); + buf += "\n"; + + buf += "nixie_cpu_frequency_mhz "; + buf += getCpuFrequencyMhz(); + buf += "\n"; + + buf += "nixie_uptime_seconds "; + buf += millis() / 1000; + buf += "\n"; + + buf += "nixie_task_count "; + buf += uxTaskGetNumberOfTasks(); + buf += "\n"; + + buf += "nixie_wifi_rssi_dbm "; + buf += WiFi.RSSI(); + buf += "\n"; + + buf += "nixie_wifi_channel "; + buf += WiFi.channel(); + buf += "\n"; + + int wifiStatus = WiFi.status(); + const char* wifiStatusStr = "UNKNOWN"; + switch (wifiStatus) { + case WL_IDLE_STATUS: wifiStatusStr = "IDLE"; break; + case WL_NO_SSID_AVAIL: wifiStatusStr = "NO_SSID_AVAIL"; break; + case WL_SCAN_COMPLETED: wifiStatusStr = "SCAN_COMPLETED"; break; + case WL_CONNECTED: wifiStatusStr = "CONNECTED"; break; + case WL_CONNECT_FAILED: wifiStatusStr = "CONNECT_FAILED"; break; + case WL_CONNECTION_LOST: wifiStatusStr = "CONNECTION_LOST"; break; + case WL_DISCONNECTED: wifiStatusStr = "DISCONNECTED"; break; + } + buf += "nixie_wifi_status_info{status=\""; + buf += wifiStatusStr; + buf += "\"} "; + buf += wifiStatus; + buf += "\n"; + + buf += "nixie_wifi_info{ssid=\""; + buf += WiFi.SSID(); + buf += "\",bssid=\""; + buf += WiFi.BSSIDstr(); + buf += "\",ip=\""; + buf += WiFi.localIP().toString(); + buf += "\",gateway=\""; + buf += WiFi.gatewayIP().toString(); + buf += "\",dns=\""; + buf += WiFi.dnsIP().toString(); + buf += "\"} 1\n"; + + buf += "nixie_ntp_sync_count "; + buf += ntpSyncCount; + buf += "\n"; + + buf += "nixie_wifi_manager_process_count "; + buf += wifiManagerProcessCount; + buf += "\n"; + + buf += "nixie_task_stack_high_water_mark "; + buf += uxTaskGetStackHighWaterMark(NULL); + buf += "\n"; + + buf += "nixie_display_task_iterations_count"; + buf += displayTaskIterationsCount; + buf += "\n"; + + buf += "nixie_boot_timestamp_seconds "; + buf += bootTimestamp; + buf += "\n"; + + buf += "nixie_last_ntp_sync_timestamp_seconds "; + buf += lastNtpSyncTimestamp; + buf += "\n"; + + buf += "nixie_littlefs_total_bytes "; + buf += LittleFS.totalBytes(); + buf += "\n"; + + buf += "nixie_littlefs_used_bytes "; + buf += LittleFS.usedBytes(); + buf += "\n"; + + wm.server->send(200, "text/plain", buf); +} + +unsigned char timeserver[63] = {'\0'}; +unsigned char timezone[30] = {'\0'}; +bool ntp_started = false; + +int loadClockConfig() { + File file = LittleFS.open("/timezone", "r"); + if (!file) { return 1; } + if (!file.read(timezone, sizeof(timezone))) { return 2; } + + file = LittleFS.open("/timeserver", "r"); + if (!file) { return 3; } + if (!file.read(timeserver, sizeof(timeserver))) { return 4; } + + if (!ntp_started) { + Serial.print("Using time server: "); + Serial.println((const char*)timeserver); + configTime(0, 0, (const char*)timeserver); + ntp_started = true; + } else { + Serial.print("NTP already started, reset to apply new time server"); + + } + + Serial.print("Using timezone: "); + Serial.println((const char*)timezone); + setenv("TZ", (const char*)timezone, 1); + tzset(); + + + unsigned char modes[1] = {'\0'}; + file = LittleFS.open("/modes", "r"); + if (!file) { return 5; } + if (!file.read(modes, sizeof(modes))) { return 6; } + displayModeCurrent = atoi((const char*)modes); + Serial.print("Enabled display modes:"); + if (displayModeCurrent & DISPLAY_MODE_DATE) { + Serial.print(" DATE"); + } + if (displayModeCurrent & DISPLAY_MODE_TIME) { + Serial.print(" TIME"); + } + Serial.println(); + + 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); // ensure integer + Serial.print("Night time dimming duty cycle: "); + Serial.println(configDimmingDutyCycle); + + unsigned char bufBlending[10] = {'\0'}; + file = LittleFS.open("/blending_duration", "r"); + if (file && file.read(bufBlending, sizeof(bufBlending))) { + configBlendingDuration = atoi((const char*)bufBlending); + Serial.print("Blending duration (ms): "); + Serial.println(configBlendingDuration); + } + + unsigned char bufDimmingStart[4] = {'\0'}; + file = LittleFS.open("/dimming_start", "r"); + if (file && file.read(bufDimmingStart, sizeof(bufDimmingStart))) { + configDimmingStart = atoi((const char*)bufDimmingStart); + Serial.print("Dimming start hour: "); + Serial.println(configDimmingStart); + } + + unsigned char bufDimmingEnd[4] = {'\0'}; + file = LittleFS.open("/dimming_end", "r"); + if (file && file.read(bufDimmingEnd, sizeof(bufDimmingEnd))) { + configDimmingEnd = atoi((const char*)bufDimmingEnd); + Serial.print("Dimming end hour: "); + Serial.println(configDimmingEnd); + } + + unsigned char bufHourFormat[4] = {'\0'}; + file = LittleFS.open("/hour_format", "r"); + if (file && file.read(bufHourFormat, sizeof(bufHourFormat))) { + configHourFormat = atoi((const char*)bufHourFormat); // 12 or 24 + Serial.print("Hour format: "); + Serial.println(configHourFormat); + } else { + configHourFormat = 12; // default + } + + return 0; +} + +void saveParamsCallback() { + File file = LittleFS.open("/timezone", "w"); + Serial.println("Saving timezone: " + String(paramTimezone.getValue())); + file.print(paramTimezone.getValue()); + file.close(); + + file = LittleFS.open("/dimming", "w"); + Serial.println("Saving dimming duty cycle: " + String(paramDimmingDutyCycle.getValue())); + file.print(paramDimmingDutyCycle.getValue()); // save as integer string + file.close(); + configDimmingDutyCycle = atoi(paramDimmingDutyCycle.getValue()); // update runtime value + + file = LittleFS.open("/modes", "w"); + file.print(paramDisplayMode.getValue()); + file.close(); + + file = LittleFS.open("/blending_duration", "w"); + file.print(paramBlendingDuration.getValue()); + file.close(); + configBlendingDuration = atoi(paramBlendingDuration.getValue()); + + file = LittleFS.open("/dimming_start", "w"); + file.print(paramDimmingStart.getValue()); + file.close(); + configDimmingStart = atoi(paramDimmingStart.getValue()); + + file = LittleFS.open("/dimming_end", "w"); + file.print(paramDimmingEnd.getValue()); + file.close(); + configDimmingEnd = atoi(paramDimmingEnd.getValue()); + + file = LittleFS.open("/timeserver", "w"); + Serial.println("Saving timeserver: " + String(paramNetworkTimeServer.getValue())); + file.print(paramNetworkTimeServer.getValue()); + file.close(); + + file = LittleFS.open("/hour_format", "w"); + Serial.println("Saving hour format: " + String(paramHourFormat.getValue())); + file.print(paramHourFormat.getValue()); + file.close(); + configHourFormat = atoi(paramHourFormat.getValue()); + + loadClockConfig(); +} + + + +portMUX_TYPE displayMux = portMUX_INITIALIZER_UNLOCKED; +int calibration = 0; +int ps = 0; + +void DisplayTask(void *pvParameters) { + const TickType_t interval = pdMS_TO_TICKS(1000 / CONFIG_REFRESH_RATE); + TickType_t xLastWakeTime = xTaskGetTickCount(); + for (;;) { + struct tm timeinfo; + time_t now = time(nullptr); + localtime_r(&now, &timeinfo); + + int h = timeinfo.tm_hour; + int m = timeinfo.tm_min; + int s = timeinfo.tm_sec; + + int subsec = millis() % 1000; + + // At second change calibrate CPU milliseconds offset + if (ps != 0 && s != ps) { + calibration = subsec; + + // Print date, time, seconds since last NTP sync, display mode, rendering mode + struct tm timeinfo; + time_t now = time(nullptr); + localtime_r(&now, &timeinfo); + + Serial.printf("%04d-%02d-%02d ", timeinfo.tm_year + 1900, timeinfo.tm_mon + 1, timeinfo.tm_mday); + Serial.printf("%02d:%02d:%02d ", timeinfo.tm_hour, timeinfo.tm_min, timeinfo.tm_sec); + + Serial.print("Seconds since last NTP sync: "); + Serial.print(now - lastNtpSyncTimestamp); Serial.print(" "); + + Serial.print("Display mode: "); + bool showing_date = false; + if (displayModeCurrent == DISPLAY_MODE_DATE) { + Serial.print("DATE "); + showing_date = true; + } else if (displayModeCurrent == DISPLAY_MODE_DATETIME) { + if ((now % 20) >= 10) { + Serial.print("DATE "); + showing_date = true; + } else { + Serial.print("TIME "); + } + } else { + Serial.print("TIME "); + } + + Serial.print("Rendering mode: "); + if (configDimmingDutyCycle < 32767 && (timeinfo.tm_hour >= configDimmingStart || timeinfo.tm_hour < configDimmingEnd)) { + Serial.print("DIMMING"); + } else if (configBlendingDuration > 0) { + Serial.print("BLENDING"); + } else { + Serial.print("STATIC"); + } + Serial.println(); + } + ps = s; + + subsec = (1000 + subsec - calibration) % 1000; + + uint64_t current = 0; + uint64_t prev = STATE_PREVIOUS; + + // Select display mode + switch (displayModeCurrent) { + case DISPLAY_MODE_DATE: + current = render_date(); + break; + case DISPLAY_MODE_DATETIME: { + // Alternate every 10 seconds between time and date + time_t now = time(nullptr); + if ((now % 20) < 10) { + current = render_time(); + } else { + current = render_date(); + } + break; + } + case DISPLAY_MODE_TIME: + default: + current = render_time(); + break; + } + + // If NTP is stale, blink all digits and colons + if (isNtpStale()) { + if (subsec >= 500) { + current = 0x787878787878ULL; + } + prev = current; + } else { + // Determine if date is currently shown + bool showing_date = false; + if (displayModeCurrent == DISPLAY_MODE_DATE) { + showing_date = true; + } else if (displayModeCurrent == DISPLAY_MODE_DATETIME) { + time_t now = time(nullptr); + if ((now % 20) >= 10) { + showing_date = true; + } + } + + // Use ternary to select colon mask + uint64_t colon_mask = showing_date ? COLON_BOTTOM_BOTH : COLON_ALL; + + // Show colons if date is shown OR subsec < 500 + if (showing_date || subsec < 500) { + current |= colon_mask; + prev |= colon_mask; + } else { + current &= ~colon_mask; + prev &= ~colon_mask; + } + } + + if (configDimmingDutyCycle < 32767 && (h >= configDimmingStart || h < configDimmingEnd)) { + if (STATE_REFRESH_MODE != 1) { + Serial.println("Switching to dimming mode"); + STATE_REFRESH_MODE = 1; + } + portENTER_CRITICAL(&displayMux); + display_dimmed(current, configDimmingDutyCycle); // Use duty cycle as brightness + portEXIT_CRITICAL(&displayMux); + } else if (configBlendingDuration > 0) { + if (STATE_REFRESH_MODE != 2) { + Serial.println("Switching to blending mode"); + STATE_REFRESH_MODE = 2; + } + if (subsec == 0) { + + portENTER_CRITICAL(&displayMux); + display_static(prev); + portEXIT_CRITICAL(&displayMux); + } + else if (subsec <= configBlendingDuration) { + if (current != prev) { + portENTER_CRITICAL(&displayMux); + display_blended(current, prev, (float)subsec / configBlendingDuration); + portEXIT_CRITICAL(&displayMux); + + } + } else { + portENTER_CRITICAL(&displayMux); + display_static(current); + portEXIT_CRITICAL(&displayMux); + + STATE_PREVIOUS = current; + + } + } else if (current != prev) { + portENTER_CRITICAL(&displayMux); + display_static(current); + portEXIT_CRITICAL(&displayMux); + } + displayTaskIterationsCount++; + vTaskDelayUntil(&xLastWakeTime, interval); + } +} + + +void setup() { + Serial.println("Initalizing clock, data pins"); + pinMode(PIN_CLOCK, OUTPUT); + pinMode(PIN_DATA, OUTPUT); + if (!rmtInit(RMT_TX_GPIO, RMT_TX_MODE, RMT_MEM_NUM_BLOCKS_1, CONFIG_CPU_FREQUENCY / CONFIG_RMT_DIVISOR)) { + Serial.println("RMT initialization failed\n"); + } + display_static(0x787878787878ULL); + + Serial.begin(115200); + setCpuFrequencyMhz(CONFIG_CPU_FREQUENCY / 1000000); + + // Setup WifiManager + uint8_t mac[6]; + WiFi.macAddress(mac); + char hostname[20]; + snprintf(hostname, sizeof(hostname), "Nixie%02X%02X%02X", mac[3], mac[4], mac[5]); + wm.setHostname(hostname); + wm.setTitle("Nixie"); + wm.setShowInfoUpdate(false); // https://github.com/tzapu/WiFiManager/issues/1262 + wm.setShowInfoErase(false); + wm.setConfigPortalBlocking(false); + wm.setMinimumSignalQuality(50); + wm.setShowInfoUpdate(false); + wm.addParameter(¶mNetworkTimeServer); + wm.addParameter(¶mCity); + wm.addParameter(¶mTimezone); + wm.addParameter(¶mDisplayMode); + wm.addParameter(¶mDisplayModeCombobox); + wm.addParameter(¶mDimmingDutyCycle); + wm.addParameter(¶mDimmingDutyCycleSlider); + wm.addParameter(¶mDimmingStart); + wm.addParameter(¶mDimmingStartCombobox); + wm.addParameter(¶mDimmingEnd); + wm.addParameter(¶mDimmingEndCombobox); + wm.addParameter(¶mBlendingDuration); + wm.addParameter(¶mBlendingDurationSlider); + wm.addParameter(¶mHourFormat); + wm.addParameter(¶mHourFormatCombobox); + wm.setSaveParamsCallback(saveParamsCallback); + wm.setConfigPortalTimeout(0); + wm.startConfigPortal(hostname); // Set AP SSID to hostname + wm.server->on("/metrics", handleMetrics); + + Serial.println("Autostarting wireless"); + if (!wm.autoConnect(hostname)) { // Set AP SSID to hostname + Serial.println("Failed to connect or start config portal"); + } else { + Serial.println("WiFi connected or config portal completed"); + WiFi.softAPdisconnect(true); + } + + + if (!LittleFS.begin(true)) { + Serial.println("LittleFS mount failed"); + } else { + if (loadClockConfig() != 0) { + Serial.println("Failed to load clock configuration from LittleFS, restoring defaults"); + saveParamsCallback(); + if (loadClockConfig() != 0) { + Serial.println("Failed to reset LittleFS defaults"); + } + } else { + Serial.println("Configuration loaded"); + } + } + + // Register SNTP time sync notification callback + sntp_set_time_sync_notification_cb(timeavailable); + + // Start FreeRTOS display refresh task + Serial.println("Starting display refresh task"); + xTaskCreatePinnedToCore(DisplayTask, "DisplayTask", 8192, NULL, 24, NULL, 0); +} + +void loop() { + wm.process(); + wifiManagerProcessCount++; + vTaskDelay(pdMS_TO_TICKS(10)); // Keep loop responsive, but display is handled by DisplayTask +} \ No newline at end of file diff --git a/firmware/esp32c3-python/README.md b/firmware/esp32c3-python/README.md new file mode 100644 index 0000000..caa7400 --- /dev/null +++ b/firmware/esp32c3-python/README.md @@ -0,0 +1,52 @@ +# MicroPython variant for ESP32C3 + +This firmware is designed to run on ESP32-C3 boards using MicroPython. It drives a Nixie tube clock with high refresh rate, advanced dimming, and blending effects, leveraging the ESP32's RMT peripheral for precise timing + +## Highlights + +- **High Refresh Rate:** Smooth, flicker-free display using RMT hardware +- **Dimming & Blending:** Configurable night mode brightness and digit blending transitions +- **Configurable via `config.py`:** All major settings can be overridden by uploading a `config.py` file +- **NTP Synchronization:** Periodically syncs time with NTP servers +- **Timezone & DST Support:** Adjustable timezone and daylight saving settings + +## Configuration Options + +You can override any of these defaults by uploading a `config.py` file: + +- `CONFIG_TIMEZONE`: Timezone offset from UTC (default: 2) +- `CONFIG_DAYLIGHT_SAVING_ENABLED`: Enable/disable DST (default: True) +- `CONFIG_NTP_SYNC_INTERVAL_HOURS`: NTP sync interval (default: 24) +- `CONFIG_DIMMING_ENABLED`: Enable night dimming (default: True) +- `CONFIG_DIMMING_BRIGHTNESS`: Night brightness (default: 0.4) +- `CONFIG_DIMMING_START`: Dimming start hour (default: 15) +- `CONFIG_DIMMING_END`: Dimming end hour (default: 6) +- `CONFIG_DIMMING_GAMMA`: Dimming gamma correction (default: 2.2) +- `CONFIG_BLENDING_ENABLED`: Enable digit blending (default: True) +- `CONFIG_BLENDING_DURATION`: Blending duration in ms (default: 150) +- `CONFIG_NETWORKS`: Dictionary of SSIDs and passwords for auto-connect + +## Features + +- **WiFi Auto-Connect:** Scans and connects to known networks +- **NTP Time Sync:** Sets system time via NTP after WiFi connection +- **Display Modes:** Time, date, and temperature rendering functions +- **RMT-Based Display:** Uses ESP32 RMT for precise display timing +- **Night Mode:** Automatic dimming based on configured hours +- **Blending:** Smooth digit transitions for improved aesthetics + +## Usage + +1. Flash MicroPython to your ESP32-C3 board +2. Upload `main.py` and (optionally) `config.py` to the device +3. Reboot the board. The clock will auto-connect to WiFi, sync time, and start displaying + +## Notes + +- The firmware is intended for advanced users familiar with MicroPython and ESP32 hardware +- For custom settings, create and upload a `config.py` file with your overrides +- RMT and SPI pin assignments are set for typical Nixie clock hardware; adjust as needed for your build + +--- + +For more details, see the source code and diff --git a/firmware/esp32c3-python/main.py b/firmware/esp32c3-python/main.py new file mode 100644 index 0000000..8feb70c --- /dev/null +++ b/firmware/esp32c3-python/main.py @@ -0,0 +1,239 @@ +import network +import json +import os +import ntptime +import machine +from time import ticks_ms, localtime, sleep_ms +from esp32 import RMT +from time import sleep +from machine import SPI, SoftSPI, Timer, Pin +from time import sleep_ms + +# Timezone settings +CONFIG_TIMEZONE = 2 +CONFIG_DAYLIGHT_SAVING_ENABLED = True +CONFIG_NTP_SYNC_INTERVAL_HOURS = 24 + +# General animation settings +CONFIG_CPU_FREQUENCY = 80000000 +CONFIG_RMT_DIVISOR = 255 +CONFIG_REFRESH_RATE = 100 + +# Night time dimming configuration +CONFIG_DIMMING_ENABLED = True +CONFIG_DIMMING_BRIGHTNESS = 0.4 +CONFIG_DIMMING_START = 15 +CONFIG_DIMMING_END = 6 +CONFIG_DIMMING_GAMMA = 2.2 + +# Daytime frame blending configuration +CONFIG_BLENDING_ENABLED = True +CONFIG_BLENDING_DURATION = 150 + +# Wireless networks +CONFIG_NETWORKS = { + "k-space.ee legacy": "", +} + +try: + from config import * +except ImportError: + print("Upload config.py to override configuration") +except: + print("Failed to load config.py") + +assert CONFIG_TIMEZONE >= -12 +assert CONFIG_TIMEZONE < 14 +assert CONFIG_DAYLIGHT_SAVING_ENABLED in (True, False) +assert CONFIG_CPU_FREQUENCY in (80000000, 160000000, 240000000) +assert CONFIG_DIMMING_BRIGHTNESS >= 0 +assert CONFIG_DIMMING_BRIGHTNESS <= 1 +assert CONFIG_DIMMING_START >= 12 +assert CONFIG_DIMMING_START <= 23 +assert CONFIG_DIMMING_END >= 1 +assert CONFIG_DIMMING_END < 12 +assert CONFIG_DIMMING_GAMMA >= 1 +assert CONFIG_BLENDING_DURATION < 500 +assert CONFIG_BLENDING_DURATION >= 0 +assert CONFIG_RMT_DIVISOR >= 1 +assert CONFIG_RMT_DIVISOR <= 255 + +print("Setting CPU frequency to", CONFIG_CPU_FREQUENCY // 1000000, "MHz") +machine.freq(CONFIG_CPU_FREQUENCY) + +sta_if = network.WLAN(network.STA_IF) +sta_if.active(True) +for jssid, _, _, _, _, _ in sta_if.scan(): + ssid = jssid.decode("utf-8") + if ssid in CONFIG_NETWORKS: + sta_if.connect(ssid, CONFIG_NETWORKS[ssid]) + print("Connecting to", ssid, "...") + while not sta_if.isconnected(): + pass + print("Obtained DHCP lease for", sta_if.ifconfig()[0]) + ntptime.settime() + sta_if.active(False) + break +else: + print("No configured wireless network found") + +print("Press Ctrl-C now to abort main.py execution and retain keyboard input") +sleep_ms(2000) + +clock = Pin(20, mode=Pin.OUT) +latch = Pin(9, mode=Pin.OUT) +data = Pin(2, mode=Pin.OUT) +unused = Pin(11) +spi = SoftSPI(baudrate=3000000, sck=clock, mosi=data, miso=unused) + +COLON_LEFT_BOTTOM = 1 << 17 << 16 +COLON_LEFT_TOP = 1 << 18 << 16 +COLON_RIGHT_BOTTOM = 1 << 17 +COLON_RIGHT_TOP = 1 << 18 + +COLON_LEFT_BOTH = COLON_LEFT_TOP | COLON_LEFT_BOTTOM +COLON_RIGHT_BOTH = COLON_RIGHT_TOP | COLON_RIGHT_BOTTOM +COLON_BOTTOM_BOTH = COLON_LEFT_BOTTOM | COLON_RIGHT_BOTTOM +COLON_TOP_BOTH = COLON_LEFT_TOP | COLON_RIGHT_TOP +COLON_ALL = COLON_LEFT_BOTH | COLON_RIGHT_BOTH + +IN15A_MICRO = 0 +IN15A_PERCENT = 2 +IN15A_PETA = 3 +IN15A_KILO = 4 +IN15A_MEGA = 5 +IN15A_MILLI = 6 +IN15A_PLUS = 7 +IN15A_MINUS = 8 +IN15A_PICO = 9 + +def clamp(v, lower=-99, upper=99): + if v > upper: + return upper + elif v < lower: + return lower + +def render_digit(j, position=0): + assert j >= -1 and j <= 9 + if j == -1: j = 10 + return [11, 9, 12, 8, 0, 4, 1, 3, 2, 10, 15][j] << 3 << (position << 3) + +def render_digits(*args): + z = 0 + for position, value in enumerate(reversed(args)): + z |= render_digit(value, position) + return z + +def render_time(colons=True): + _, _, _, h, m, s, _, _ = localtime() + return render_digits(h // 10, h % 10, m // 10, m % 10, s // 10, s % 10) | (colons and COLON_ALL) + +def render_date(colons=True): + y, m, d, _, _, _, _, _ = localtime() + return render_digits(y // 10, y % 10, m // 10, m % 10, d // 10, d % 10) | (colons and COLON_BOTTOM_BOTH) + +def render_temperature(t): + val = abs(clamp(t)) + return render_digits(-1, IN15A_MINUS if t < 0 else -1, val // 10, val % 10, -1, -1) | COLON_RIGHT_TOP + +rmt = RMT(0, pin=latch, clock_div=CONFIG_RMT_DIVISOR, idle_level=False) +tim = Timer(0, mode=Timer.PERIODIC) + +RMT_DURATION = CONFIG_CPU_FREQUENCY // CONFIG_RMT_DIVISOR // CONFIG_REFRESH_RATE +assert RMT_DURATION <= 32767, "RMT duration %d overflows 32767" % RMT_DURATION + +print("Refresh rate: %d Hz" % CONFIG_REFRESH_RATE) +print("PWM period: %d RMT pulses" % RMT_DURATION) + +i = 0 +d = 0 + +from time import time, ticks_ms, localtime +z = time() +calibration = 0 +while z == time(): + calibration = ticks_ms() % 1000 + +def is_dst(y, mo, d, h, m): + if mo < 3 or mo > 11: + return False + if 3 < mo < 11: + return True + if mo == 3: + return d >= 8 # Approximation + if mo == 11: + return d < 7 # Approximation + return False + +def get_time(): + subsec = (ticks_ms() - calibration) % 1000 + now = time() + CONFIG_TIMEZONE * 3600 + y, mo, d, h, m, s, _, _ = localtime(now) + if CONFIG_DAYLIGHT_SAVING_ENABLED and is_dst(y, mo, d, h, m): + now += 3600 # Add 1 hour + y, mo, d, h, m, s, _, _ = localtime(now) + return y, mo, d, h, m, s, subsec + + +def display_static(j): + spi.write(j.to_bytes(6)) + rmt.write_pulses((1,1), 0) + +def display_dimmed(j, brightness=0.5): + duty = brightness ** CONFIG_DIMMING_GAMMA + assert brightness >= 0 + assert brightness <= 1 + assert duty >= 0 + assert duty <= 1 + pulses = (1, 100, 1 + int(RMT_DURATION * duty), 1) + if brightness == 1.0: + display_static(j) + else: + spi.write(j.to_bytes(6)) + rmt.write_pulses(pulses, 0) + spi.write('\x78\x78\x78\x78\x78\x78') + +def display_blended(j, i, progression=0.5): + assert progression >= 0 + assert progression <= 1 + duty = progression + pulses = (1, 100, 1 + int(RMT_DURATION * duty), 1) + if progression == 1.0: + display_static(j) + else: + spi.write(j.to_bytes(6)) + rmt.write_pulses(pulses, 0) + spi.write(i.to_bytes(6)) + +STATE_PREVIOUS = 0x787878787878 +STATE_REFRESH_MODE = 0 + +def run_clock(*args): + global STATE_PREVIOUS + global STATE_REFRESH_MODE + y, mo, d, h, m, s, subsec = get_time() + current = render_digits(h // 10, h % 10, m // 10, m % 10, s // 10, s % 10) + prev = STATE_PREVIOUS + if subsec <= 500: + current |= COLON_ALL + prev |= COLON_ALL + if CONFIG_DIMMING_ENABLED and (h >= CONFIG_DIMMING_START or h < CONFIG_DIMMING_END): + if STATE_REFRESH_MODE != 1: + print("Switching to dimming mode, because", CONFIG_DIMMING_START, "<=", h, "<=", CONFIG_DIMMING_END) + STATE_REFRESH_MODE = 1 + display_dimmed(current, CONFIG_DIMMING_BRIGHTNESS) + elif CONFIG_BLENDING_ENABLED: + if STATE_REFRESH_MODE != 2: + print("Switching to blending mode") + STATE_REFRESH_MODE = 2 + if subsec <= CONFIG_BLENDING_DURATION: + if current != prev: + display_blended(current, prev, progression=subsec / CONFIG_BLENDING_DURATION) + else: + display_static(current) + STATE_PREVIOUS = current + else: + display_static(current) + +print("Setting up periodic timer at %d Hz (%d ms)" % (CONFIG_REFRESH_RATE, 1000 // CONFIG_REFRESH_RATE)) +tim.init(mode=Timer.PERIODIC, period=1000 // CONFIG_REFRESH_RATE, callback=run_clock) diff --git a/firmware/esp8266-arduino/.gitignore b/firmware/esp8266-arduino/.gitignore new file mode 100644 index 0000000..798d116 --- /dev/null +++ b/firmware/esp8266-arduino/.gitignore @@ -0,0 +1 @@ +cities.h diff --git a/firmware/Makefile b/firmware/esp8266-arduino/Makefile similarity index 58% rename from firmware/Makefile rename to firmware/esp8266-arduino/Makefile index 6b95be6..8afbcd8 100644 --- a/firmware/Makefile +++ b/firmware/esp8266-arduino/Makefile @@ -1,20 +1,21 @@ SKETCH_FOLDER := $(shell dirname $(realpath $(firstword $(MAKEFILE_LIST)))) UPLOAD_PORT ?= /dev/ttyUSB0 -all: $(SKETCH_FOLDER)/build/firmware.ino.bin +all: $(SKETCH_FOLDER)/build/esp8266-arduino.ino.bin -$(SKETCH_FOLDER)/cities.h: cities.py - python3 cities.py > $(SKETCH_FOLDER)/cities.h +$(SKETCH_FOLDER)/cities.h: ../cities.py + python3 ../cities.py > $(SKETCH_FOLDER)/cities.h -$(SKETCH_FOLDER)/build/firmware.ino.bin: $(SKETCH_FOLDER)/firmware.ino $(SKETCH_FOLDER)/cities.h +$(SKETCH_FOLDER)/build/esp8266-arduino.ino.bin: $(SKETCH_FOLDER)/esp8266-arduino.ino $(SKETCH_FOLDER)/cities.h arduino-cli compile -e -b esp8266:esp8266:generic $(SKETCH_FOLDER) deps: + arduino-cli config set network.connection_timeout 600s 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 +flash: $(SKETCH_FOLDER)/esp8266-arduino.ino.bin arduino-cli upload -b esp8266:esp8266:generic -p $(UPLOAD_PORT) $(SKETCH_FOLDER) console: diff --git a/firmware/esp8266-arduino/README.md b/firmware/esp8266-arduino/README.md new file mode 100644 index 0000000..9b44eca --- /dev/null +++ b/firmware/esp8266-arduino/README.md @@ -0,0 +1,15 @@ +# Arduino variant for ESP8266 + +This firmware is designed for Nixie tube clocks using the ESP8266 microcontroller. + +## Features + +- WiFi configuration via captive portal (WiFiManager) +- NTP time synchronization +- Time and date display +- Timezone and NTP server configuration +- Clock drift mitigation using the ezTime library + +## Note + +The main issue with ESP8266-based clocks is clock drift. This firmware uses the ezTime library to reduce drift, but accuracy is still limited compared to ESP32-based \ No newline at end of file diff --git a/firmware/firmware.ino b/firmware/esp8266-arduino/esp8266-arduino.ino similarity index 100% rename from firmware/firmware.ino rename to firmware/esp8266-arduino/esp8266-arduino.ino