From 0d3e8780517b6ae867e14b479ef8b9beda2c309a Mon Sep 17 00:00:00 2001 From: MangaValk Date: Sun, 25 Oct 2026 21:23:10 +0100 Subject: [PATCH] Run ESP_UniClock on the NixieIN12v2 clock Adds CLOCK_90 and the nixie595.ino driver: two 74HC595s on hardware SPI (GPIO13/14, latch GPIO15), multiplexed from an IRAM-only timer1 ISR. Neon dots on P1; P2 is the decimal-point line on this board. Also: - NeoPixel on any ESP8266 pin via bit-bang (GPIO12), and take the pin back after SPI.begin() claimed it as MISO - ArduinoOTA (USE_ARDUINO_OTA), with an OTA upload env in platformio.ini - DST: EU end date (last Sunday of October) instead of the first Sunday of November, and recompute the time from the rules when DST is toggled instead of shifting it by 1 h - forward declarations so PlatformIO's .ino conversion compiles - FIRMWARE_SERVER can be overridden; empty for CLOCK_90 Verified on the clock: tubes, order, dots, LEDs, LDR, NTP time with DST. Co-Authored-By: Claude Opus 5.5 --- CLAUDE.md | 48 +++--- .../ESP32_UniClock2/ESP32_UniClock2.ino | 75 ++++++++- firmware/uniclock/ESP32_UniClock2/clocks.h | 35 +++- .../uniclock/ESP32_UniClock2/nixie595.ino | 154 ++++++++++++++++++ .../ESP32_UniClock2/secrets.h.example | 6 + .../uniclock/ESP32_UniClock2/z_neopixel.ino | 13 +- firmware/uniclock/platformio.ini | 7 + 7 files changed, 304 insertions(+), 34 deletions(-) create mode 100644 firmware/uniclock/ESP32_UniClock2/nixie595.ino create mode 100644 firmware/uniclock/ESP32_UniClock2/secrets.h.example diff --git a/CLAUDE.md b/CLAUDE.md index 0af8745..00d43ed 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -6,8 +6,9 @@ with ArduinoOTA and Home Assistant (MQTT) support. ## The device -- On the network at **10.1.1.97** (WiFi `dlink-iot`). Stock web UI on port 80, nothing else - open: no OTA, no API, no MQTT. +- On the network at **10.1.1.97** (WiFi `dlink-iot`). Since 2026-10-25 it runs ESP_UniClock + (`firmware/uniclock/`, CLOCK_90): web UI on port 80 (JSON at `/getConfiguration`, + `/getCurrentInfos`), ArduinoOTA on UDP 8266 (hostname `nixie-in12`, no password yet). - Board: RetroModding (Konstantin Repnikov) "NixieIN12v2 HV2.0", stock firmware **SV1.3** (©2020). ESP-12 (ESP8266, MAC 30:83:98:99:BF:DE, Espressif). - **Powered by USB-C 5 V only** (no DC jack). Most likely the maker's "v2.0L lightweight @@ -34,28 +35,29 @@ with ArduinoOTA and Home Assistant (MQTT) support. ## Pinout From the 12 V v2 schematic (`docs/schematic_v2_12V.jpg`), checked against the stock firmware -by disassembly (`docs/stock-firmware-pins.md`, tools in `tools/`). "Binary" = confirmed in -the stock SV1.3 code; it has not yet been tested on the hardware. +by disassembly (`docs/stock-firmware-pins.md`, tools in `tools/`). "Hardware" = also works +on the clock with UniClock; "Binary" = only confirmed in the stock SV1.3 code. | Function | GPIO | Source | |---|---|---| -| 74HC595 DATA / CLOCK (OE tied to GND) | 13 / 14 = HSPI MOSI / SCK (hardware SPI, MSB first) | Binary | -| 74HC595 LATCH | 15, plain `digitalWrite` (has 1k pull-down) | Binary | -| WS2812 data | 12 (NeoPixel bit-bang, GRB 800 kHz; stock assumes 12 LEDs, 8 fitted) | Binary | -| Photoresistor (LDR, 10k to GND) | ADC (A0) | Binary | +| 74HC595 DATA / CLOCK (OE tied to GND) | 13 / 14 = HSPI MOSI / SCK (hardware SPI, MSB first) | Hardware | +| 74HC595 LATCH | 15, plain `digitalWrite` (has 1k pull-down) | Hardware | +| WS2812 data | 12 (NeoPixel bit-bang, GRB 800 kHz; 8 fitted, stock assumes 12). `SPI.begin()` turns GPIO12 into MISO: set it back to OUTPUT afterwards | Hardware | +| Photoresistor (LDR, 10k to GND) | ADC (A0) | Hardware | | BTN1 / BTN2 (not fitted) | 0 / 2, INPUT_PULLUP. Stock: GPIO0 is a button, GPIO2 a motion/presence input that wakes the display | Binary | | DFPlayer Mini (stock feature) | UART0: GPIO1 TX / GPIO3 RX, 9600 Bd | Binary | -| DS3231M RTC SDA / SCL | 4 / 5. **Stock firmware has no I2C code** (NTP only); if fitted, unknown | Schematic only | +| DS3231M RTC SDA / SCL | 4 / 5: **no I2C device answers** (UniClock scan), so no RTC on this board; NTP only | Hardware | Two chained 74HC595s make one 16-bit word, all bits active-high (confirmed from the binary; the high byte is sent first and goes to U2): - U1 QA..QH = cathodes 0..7 (bits 0..7), U2 QA = cathode 8 (bit 8), QB = cathode 9 (bit 9) (via 2x ULN2003, shared by all tubes) -- U2 QC = P1 (neon dot 1, bit 10), QD..QG = anodes A1..A4 (bits 11..14, MMBTA42/92), QH = P2 - (bit 15; neon dot 2, and the decimal point if jumper JP2 is closed) +- U2 QC = P1 (bit 10) drives **both** IN-3 neon dots. QD..QG = anodes A1..A4 (bits 11..14, + MMBTA42/92). QH = P2 (bit 15) drives the IN-12B **decimal points** on this board (JP2 closed), + not a neon. - The display is multiplexed: one anode on at a time. **A1 = leftmost tube (hour tens), - A4 = rightmost (minute ones)**, from the stock digit order. Stock lights P1/P2 in only one - multiplex slot. + A4 = rightmost (minute ones)** (verified on the tubes). The dots are lit in the minute-tens + slot only, as on stock. ## Programming header @@ -65,7 +67,12 @@ Wiring: GND-GND, adapter TXD -> clock RXD, adapter RXD -> clock TXD, adapter 3.3 connected (the clock is powered from its own USB-C). No auto-reset: to enter flash mode, hold GPIO0 to GND, tap RST to GND, then release GPIO0. -- esptool is installed for the user (`python -m esptool`, v5.6.0). +- esptool is installed for the user (`python -m esptool`, v5.6.0). After `--after no-reset` + the stub keeps running at the new baud rate and the next esptool call can't connect: do + everything in one call, or re-enter flash mode. +- Normal updates go over WiFi: `pio run -e nixie_in12_ota -t upload` in `firmware/uniclock/`. + ArduinoOTA does not answer after the clock switched from AP mode to WiFi without a reboot + (first setup); a power cycle fixes it. - `firmware/stock/` holds the stock flash dump. **Never overwrite or delete it**; it is the only way back to SV1.3 (`write-flash 0 `). Dumps are gitignored because the flash holds the WiFi password; never commit them. @@ -79,10 +86,9 @@ GPIO0 to GND, tap RST to GND, then release GPIO0. ## Plan -1. Dump and inspect the stock firmware (version strings, pins if possible). -2. Write a test sketch that steps through the 16 shift-register bits, to verify the pinout - and the tube order. -3. Add a 74HC595-multiplex driver + CLOCK config to ESP_UniClock, plus ArduinoOTA and - Home Assistant controls (display on/off, brightness, LEDs); its stock MQTT only - publishes sensors. -4. Work continues in VS Code (PlatformIO). +1. Done: dump and inspect the stock firmware. +2. Skipped (the disassembly was enough): bit-stepping test sketch. +3. Done: 74HC595-multiplex driver (`nixie595.ino`) + CLOCK_90 in ESP_UniClock, ArduinoOTA, + EU DST fix. Runs on the clock. +4. Next: Home Assistant controls over MQTT (display on/off, brightness, LEDs; upstream MQTT + only publishes sensors), OTA password via `secrets.h`. diff --git a/firmware/uniclock/ESP32_UniClock2/ESP32_UniClock2.ino b/firmware/uniclock/ESP32_UniClock2/ESP32_UniClock2.ino index e0a33a8..f81fa73 100644 --- a/firmware/uniclock/ESP32_UniClock2/ESP32_UniClock2.ino +++ b/firmware/uniclock/ESP32_UniClock2/ESP32_UniClock2.ino @@ -44,6 +44,11 @@ #include //#define byte uint8_t #include "clocks.h" //DEFINE YOUR CLOCKS SETUP IN THIS FILE!!! - clocks.h picks the single active CLOCK_NN block, which #defines the driver macro, pins and feature flags read throughout this file +// PlatformIO turns the .ino files into one .cpp and puts prototypes for every function near the top, +// without evaluating #ifdefs. Declare the types of optional libraries here so those prototypes compile +// when the feature (and its #include) is switched off. +class AsyncWiFiManager; +struct RgbColor; // Detect which generation of the ESP8266 Arduino core is in use: older cores (2.6.x/2.7.x) name the // RAM-resident-ISR attribute ICACHE_RAM_ATTR, while newer cores renamed it to IRAM_ATTR (shared with @@ -161,7 +166,9 @@ unsigned long intCounter = 0; //for testing only, interrupt counter byte c_MinBrightness = RGB_MIN_BRIGHTNESS; //minimum LED brightness byte c_MaxBrightness = RGB_MAX_BRIGHTNESS; //maximum LED brightness //-------------------------------------------------------------------------------------------------- -#define FIRMWARE_SERVER "http://c.landventure.hu/store" //default OTA update server (see doFirmwareUpdate()); overridable per-clock via prm.firmwareServer / the web admin "firmware" field +#ifndef FIRMWARE_SERVER + #define FIRMWARE_SERVER "http://c.landventure.hu/store" //default OTA update server (see doFirmwareUpdate()); overridable per-clock via prm.firmwareServer / the web admin "firmware" field +#endif String usedPinsStr; //human-readable list of registered GPIOs, built by listPins(), shown on the web admin "clock details" page String driverSetupStr; //driver-specific extra setup info string (set by some drivers), also shown on the web admin page @@ -237,6 +244,9 @@ DNSServer dnsServer; #include #include #include "ArduinoJson.h" +#ifdef USE_ARDUINO_OTA + #include +#endif #if defined(USE_NEOPIXEL) || defined(WORDCLOCK) #include @@ -441,8 +451,10 @@ IPAddress ip; // switchover DATES are not configurable from the web UI. myTZ is rebuilt with the current offsets // every time updateTimefromTimeserver() runs (WiFi/NTP path only - the GPS-only fallback in gps.ino // uses a simpler, always-on +1h DST rule, see CODE_REVIEW.md). +// NOTE: upstream ended DST on the first Sunday of November (the US date). The EU switches at 01:00 UTC: +// last Sunday of March 02:00 standard time, last Sunday of October 03:00 summer time. TimeChangeRule myDST = {"DST", Last, Sun, Mar, 2, 0}; -TimeChangeRule mySTD = {"STD", First, Sun, Nov, 2, 0}; +TimeChangeRule mySTD = {"STD", Last, Sun, Oct, 3, 0}; Timezone myTZ(myDST, mySTD); time_t prevTime = 0; //last second() value that was actually rendered to the display - timeProgram() only redraws when now()!=prevTime, i.e. at most once per real second time_t protectTimer = 0; //timestamp of the last cathode-protect animation, used by timeProgram()'s prm.interval-minute scheduler @@ -589,6 +601,9 @@ void Fdelay(unsigned long d) { #ifdef USE_GPS smartDelay(1); //feed GPS #endif + #ifdef USE_ARDUINO_OTA + ArduinoOTA.handle(); //also during long animations/startup waits + #endif yield(); } } @@ -877,6 +892,39 @@ void startMDNS() { #endif } +// Optional ArduinoOTA (USE_ARDUINO_OTA in clocks.h): push a new firmware from PlatformIO/Arduino IDE +// over WiFi ("pio run -t upload --upload-port "). Shows the progress in percent on the two right +// tubes. Password from OTA_PASSWORD (secrets.h) if defined. +void startArduinoOTA() { + #ifdef USE_ARDUINO_OTA + static boolean otaStarted = false; + if (otaStarted) return; + ArduinoOTA.setHostname(OTA_HOSTNAME); + #ifdef OTA_PASSWORD + ArduinoOTA.setPassword(OTA_PASSWORD); + #endif + ArduinoOTA.onStart([]() { + DPRINTLN(F("ArduinoOTA: update started")); + clearDigits(); + }); + ArduinoOTA.onProgress([](unsigned int progress, unsigned int total) { + int p = (int)(progress / (total / 100 + 1)); + digit[1] = (p / 10) % 10; + digit[0] = p % 10; + }); + ArduinoOTA.onEnd([]() { + DPRINTLN(F("ArduinoOTA: done, rebooting")); + }); + ArduinoOTA.onError([](ota_error_t error) { + DPRINTF("ArduinoOTA: error %u\n", error); + clearDigits(); + }); + ArduinoOTA.begin(); + otaStarted = true; + DPRINT(F("ArduinoOTA ready: ")); DPRINTLN(OTA_HOSTNAME); + #endif +} + // The Standalone(AP)-mode path (prm.wifiMode==false, or a fallback from a fully-failed STA connect): // the clock hosts its own WiFi hotspot at the fixed address 192.168.4.1 (prm.ApSsid/prm.ApPsw) and // runs a DNS server that answers every query with its own IP (dnsServer.start(53,"*",...)) so most @@ -1242,6 +1290,9 @@ void handleConfigChanged(AsyncWebServerRequest *request) { if (old_utc_offset != prm.utc_offset) { //Change time zone setTime(now()+(prm.utc_offset-old_utc_offset)*3600); updateRTC(); + mySTD.offset = prm.utc_offset * 60; //keep myTZ current for a later enableDST change + myDST.offset = mySTD.offset + (prm.enableDST ? 60 : 0); + myTZ = Timezone(myDST, mySTD); } } else if (key == "set12_24") { @@ -1262,13 +1313,15 @@ void handleConfigChanged(AsyncWebServerRequest *request) { // running clock by +-1h right away and re-syncs the RTC, instead of only taking effect on the // next scheduled NTP refresh (myTZ itself is rebuilt from prm.enableDST inside // updateTimefromTimeserver() on that next refresh). + // FIXED: this used to shift the clock by a blind +-1h, which is wrong outside the summer period + // (switching DST on in winter put the clock 1h ahead until the next NTP sync). Now the local + // time is converted back to UTC with the old rules and forward with the new ones. prm.enableDST = (value == "true"); - if (oldDST && !prm.enableDST) { //Switching off DST - setTime(now()-3600); - updateRTC(); - } - if (!oldDST && prm.enableDST) { //Switching on DST - setTime(now()+3600); + if (oldDST != prm.enableDST) { + time_t utc = myTZ.toUTC(now()); + myDST.offset = mySTD.offset + (prm.enableDST ? 60 : 0); + myTZ = Timezone(myDST, mySTD); + setTime(myTZ.toLocal(utc)); updateRTC(); } } @@ -1889,10 +1942,11 @@ void setup() { startMDNS(); startServer(); + startArduinoOTA(); #ifdef USE_MQTT setupMqtt(); #endif - + showMyIp(); Fdelay(500); clearDigits(); @@ -3221,6 +3275,9 @@ void loop(void) { } } editor(); + #ifdef USE_ARDUINO_OTA + ArduinoOTA.handle(); + #endif if (makeFirmwareUpdate) { makeFirmwareUpdate = false; doFirmwareUpdate(); diff --git a/firmware/uniclock/ESP32_UniClock2/clocks.h b/firmware/uniclock/ESP32_UniClock2/clocks.h index f11b12c..e7b1041 100644 --- a/firmware/uniclock/ESP32_UniClock2/clocks.h +++ b/firmware/uniclock/ESP32_UniClock2/clocks.h @@ -52,6 +52,7 @@ ZM1500_ESP32 (CURRENTLY ACTIVE, CLOCK_72 below) -> ZM1500.ino WORDCLOCK -> wordclock.ino (+ font5x7.h for its scrolling/overlay font) DUMMY -> dummy.ino (sensor-only "clock", no physical display) + NIXIE595_MULTIPLEX (CLOCK_90) -> nixie595.ino */ //#define CLOCK_1 //8266, UNFI PCB clock, 4x IN-16 tubes @@ -110,11 +111,43 @@ //#define CLOCK_57A //V2-WROOM ESP32, UNFI 2xHV5222 6xZ57xM clock /flat panel //#define CLOCK_70 //1 tube esp8266 Nixie Clock by UNFI //#define CLOCK_71 //2x VQC10 clock by UNFI -#define CLOCK_72 //ESP32, UNFI board, ZM1500 +//#define CLOCK_72 //ESP32, UNFI board, ZM1500 //#define CLOCK_80 //word clock, english version //#define CLOCK_81 //word clock, german version //#define CLOCK_82 //word clock, hungarian version //#define CLOCK_99 //Dummy clock, sensor box with MQTT connector +#define CLOCK_90 //8266, RetroModding NixieIN12v2 HV2.0 (5 V): 4x IN-12, 2x IN-3, 2x 74HC595 multiplex + +#ifdef CLOCK_90 //8266, RetroModding NixieIN12v2 HV2.0 (5 V): 4x IN-12, 2x IN-3, 2x 74HC595 multiplex +// Pins and shift-register bit map confirmed from the stock firmware SV1.3 +// (docs/stock-firmware-pins.md in the nixie-clock repo). +#define DEBUG +#define FW "fw90" //firmware name +#define MAXBRIGHTNESS 100 +#define NIXIE595_MULTIPLEX +#define PIN_LATCH 15 //74HC595 storage clock. DATA = GPIO13 (HSPI MOSI), CLOCK = GPIO14 (HSPI SCK) +#define SPI_FREQ 4000000 //stock uses 25 MHz; 16 bits per slot need far less +byte anodeBits[] = {14, 13, 12, 11}; //595 word bit of each tube's anode: position 0 = rightmost (A4) ... 3 = leftmost (A1) +#define NEON_BITS (1 << 10) //IN-3 dots on P1. P2 (bit 15) drives the IN-12B decimal points on this board (JP2 closed) +#define NEON_POS 1 //the dots are lit in this tube's multiplex slot, as on stock +#define USE_NEOPIXEL +#define NEOPIXEL_PIN 12 //bit-banged, GRB 800 kHz +byte tubePixels[] = {0, 0, 1, 1, 2, 2, 3, 3}; //8 WS2812, two per tube (strip order not verified yet) +#define USE_RTC //DS3231 on I2C, detected at startup (address 0x68); the stock firmware never used it +#define PIN_SDA 4 +#define PIN_SCL 5 +#define LIGHT_SENSOR_PIN A0 //LDR to Vcc, 10k to GND (REF_RESISTANCE default) +#define MAXIMUM_LUX 100 //Lux level for maximum tube brightness +#define AP_NAME "UNICLOCK" +#define AP_PASSWORD "" +#define WEBNAME "IN-12 Nixie Clock" +#define FIRMWARE_SERVER "" //no web-triggered HTTP update; use ArduinoOTA +#define USE_ARDUINO_OTA +#define OTA_HOSTNAME "nixie-in12" +#if __has_include("secrets.h") + #include "secrets.h" //optional, gitignored: DEFAULT_SSID, DEFAULT_PSW, OTA_PASSWORD (see secrets.h.example) +#endif +#endif //______________________ESP8266 CLOCKS by UNFI and GP ______________________________________________________ #ifdef CLOCK_1 //8266, UNFI PCB clock, 4x IN-16 tubes diff --git a/firmware/uniclock/ESP32_UniClock2/nixie595.ino b/firmware/uniclock/ESP32_UniClock2/nixie595.ino new file mode 100644 index 0000000..557f621 --- /dev/null +++ b/firmware/uniclock/ESP32_UniClock2/nixie595.ino @@ -0,0 +1,154 @@ +// ============================================================================ +// Nixie driver: two cascaded 74HC595s on hardware SPI, multiplexed anodes +// ---------------------------------------------------------------------------- +// Hardware: RetroModding NixieIN12v2 (CLOCK_90). The two 595s hold one 16-bit +// word that drives the whole display: +// bits 0..9 cathodes for digits 0..9 (via 2x ULN2003, shared by all tubes) +// bits 11..14 tube anodes (anodeBits[] in clocks.h), one tube at a time +// bits 10, 15 neon dots P1/P2 (NEON_BITS in clocks.h) +// The high byte is shifted out first (ends up in the second 595), MSB first. +// DATA and CLOCK are the HSPI MOSI/SCK pins (GPIO13/GPIO14), PIN_LATCH is the +// 595 storage clock. Pinout and bit map come from the stock firmware, see +// docs/stock-firmware-pins.md in the nixie-clock repo. +// +// The multiplex ISR writes the SPI1 and GPIO registers directly instead of +// calling the SPI library, so it runs entirely from IRAM and keeps refreshing +// while the flash cache is off (EEPROM/SPIFFS writes, OTA). +// +// Platform: ESP8266 ONLY (timer1 + SPI1 registers). The ISR state machine +// (brightness by blanking time, animMask wipe) follows multiplex74141.ino. +// ============================================================================ +#ifdef NIXIE595_MULTIPLEX +#include +char tubeDriver[] = "NIXIE595_MULTIPLEX"; + +#if !defined(ESP8266) + #error "NIXIE595_MULTIPLEX is for ESP8266 only!" +#endif + +int maxDigits = sizeof(anodeBits); //anodeBits is byte[], so sizeof == element count +int maxDig = maxDigits; + +// timer1 runs at TIM_DIV16 = 5 MHz (0.2 us per tick): 11000 ticks = 2.2 ms per tube slot, +// about 110 Hz for 4 tubes. PWM_min/PWM_max are the lit part of a slot (brightness). +// const, so the ISR's static locals are constant-initialized (no flash-resident init guard). +const int PWMrefresh = 11000; +const int PWM_min = 1000; +const int PWM_max = 10000; + +// Shift one 16-bit word into both 595s and latch it. Also called from main code via clearTubes(), +// so the caller must keep the ISR out (see clearTubes()). +static inline void IRAM_ATTR write595(uint16_t w) { + while (SPI1CMD & SPIBUSY) {} + SPI1W0 = (w >> 8) | ((w & 0xFF) << 8); //SPI1W0 byte 0 goes out first: high byte first + SPI1CMD |= SPIBUSY; + while (SPI1CMD & SPIBUSY) {} + GPOS = 1 << PIN_LATCH; //rising edge copies the shift registers to the outputs + __asm__ __volatile__("nop; nop; nop; nop;"); + GPOC = 1 << PIN_LATCH; +} + +void setup_pins() { + DPRINTLN("Nixie clock - setup pins - 74HC595 multiplex mode..."); + pinMode(PIN_LATCH, OUTPUT); + digitalWrite(PIN_LATCH, LOW); + regPin(PIN_LATCH, "PIN_LATCH"); + + SPI.begin(); //HSPI: GPIO14 = SCK, GPIO13 = MOSI. It also claims GPIO12 (MISO), which the NeoPixel setup takes back later. + SPI.setFrequency(SPI_FREQ); + SPI.setBitOrder(MSBFIRST); + SPI.setDataMode(SPI_MODE0); + regPin(13, "PIN_DATA (HSPI MOSI)"); + regPin(14, "PIN_CLK (HSPI SCK)"); + //16-bit transfers, so one write fills both 595s + SPI1U1 = (SPI1U1 & ~((SPIMMOSI << SPILMOSI) | (SPIMMISO << SPILMISO))) + | (15 << SPILMOSI) | (15 << SPILMISO); + write595(0); //all anodes, cathodes and dots off + + DPRINT("Tubes: "); DPRINTLN(maxDigits); + startTimer(); +} + +void IRAM_ATTR writeDisplay() { //timer1 ISR, single-shot: must re-arm timer1 at the end of every call + static byte pos = 0; + static byte state = 0; + static int timer = PWMrefresh; + static byte num; + static int brightness; + static int PWMtimeBrightness = PWM_min; + uint16_t w; + + if (EEPROMsaving) { //keep the tubes dark while settings are written + write595(0); + timer1_write(PWMrefresh); + return; + } + + intCounter++; + brightness = displayON ? prm.dayBright : prm.nightBright; + if (brightness > MAXBRIGHTNESS) brightness = MAXBRIGHTNESS; //only for safety + + timer = PWMrefresh; + + // Same state machine as multiplex74141.ino: + // state 0: next tube; show the old digit (animation) or the current digit + // state 1: show the new digit for the rest of the lit time (only while animating) + // state 2: blank for the rest of the slot (this sets the brightness) + // state 3: blank output, then back to state 0 + switch (state) { + case 0: + pos++; + if (pos > maxDig - 1) { + pos = 0; + if (autoBrightness && displayON) + PWMtimeBrightness = max(PWM_min, PWM_max * lx / MAXIMUM_LUX); + else + PWMtimeBrightness = max(PWM_min, PWM_max * brightness / MAXBRIGHTNESS); + } + if (animMask[pos] > 0) { + num = oldDigit[pos]; + timer = (PWMtimeBrightness * (20 - animMask[pos])) / 20; + state = 1; + } + else { + num = digit[pos]; + timer = PWMtimeBrightness; + state = 2; + } + break; + case 1: + num = newDigit[pos]; + timer = (PWMtimeBrightness * animMask[pos]) / 20; + state = 2; + break; + case 2: + timer = PWMrefresh - PWMtimeBrightness; + state = 3; + break; + } + if (timer < 500) timer = 500; //a too short timer1 reload can miss the interrupt + + if ((brightness == 0) || (state == 3) || (!radarON)) { + state = 0; + w = 0; + } + else { + w = 0; + if (num <= 9) w |= 1 << num; //cathode; 10 = blank, other glyphs are not available on a Nixie + if ((pos == NEON_POS) && colonBlinkState) w |= NEON_BITS; + if (w) w |= 1 << anodeBits[pos]; + } + write595(w); + + timer1_write(timer); +} + +void clearTubes() { + noInterrupts(); //the ISR also writes the 595s + write595(0); + interrupts(); +} + +// Not needed: the ISR refreshes the display continuously (same as multiplex74141.ino). +void writeDisplaySingle() {} +#endif diff --git a/firmware/uniclock/ESP32_UniClock2/secrets.h.example b/firmware/uniclock/ESP32_UniClock2/secrets.h.example new file mode 100644 index 0000000..1d9c212 --- /dev/null +++ b/firmware/uniclock/ESP32_UniClock2/secrets.h.example @@ -0,0 +1,6 @@ +// Copy to secrets.h (gitignored) and fill in. All entries are optional. +// DEFAULT_SSID/DEFAULT_PSW are only applied on a factory reset (first boot with a new EEPROM layout); +// otherwise set the WiFi from the clock's web page. +#define DEFAULT_SSID "" +#define DEFAULT_PSW "" +#define OTA_PASSWORD "" //ArduinoOTA password; remove the line for no password diff --git a/firmware/uniclock/ESP32_UniClock2/z_neopixel.ino b/firmware/uniclock/ESP32_UniClock2/z_neopixel.ino index 278701d..9abbb7c 100644 --- a/firmware/uniclock/ESP32_UniClock2/z_neopixel.ino +++ b/firmware/uniclock/ESP32_UniClock2/z_neopixel.ino @@ -100,13 +100,17 @@ inline byte tubeForPixel(int i) { //than NeoEsp32Rmt7Ws2812xMethod, which can glitch the strict WS2812 bit //timing (~1.25us/bit) if an ISR delays the RMT hardware mid-frame. NeoPixelBrightnessBus strip(PixelCount+4,PixelPin); //NeoEsp32I2s1800KbpsMethod is used here because it is more reliable than NeoEsp32Rmt7Ws2812xMethod! -#else - #define NEOPIXEL_PIN 3 +#elif NEOPIXEL_PIN == 3 //default (see clocks.h) byte PixelPin = 3; // on 8266 it MUST use GPIO3 (RX pin) //8266 has no I2S/RMT WS2812 helper here; Neo800KbpsMethod bit-bangs the //protocol with cycle-counted delays, which is why GPIO3/RX is mandated - //it's one of the few pins whose timing isn't disturbed by the UART/boot ROM. NeoPixelBrightnessBus strip(PixelCount+4); +#else + //Any other 8266 pin: cycle-counted bit-bang. Interrupts are off while Show() + //runs (about 30 us per pixel), which briefly delays the multiplex ISR. + byte PixelPin = NEOPIXEL_PIN; + NeoPixelBrightnessBus strip(PixelCount+4, PixelPin); #endif //Gamma-correction lookup table object - NOTE: currently declared but never @@ -121,7 +125,10 @@ void setupNeopixel() { DPRINT("Pixel count: "); DPRINTLN(PixelCount); DPRINT("Brightness:"); DPRINT(c_MinBrightness); DPRINT(" - "); DPRINTLN(c_MaxBrightness); neoBrightness = prm.rgbBrightness; - strip.Begin(); //initialize the underlying I2S/RMT/bit-bang driver and GPIO + //The bit-bang method sets the pin to OUTPUT in its constructor (before setup()), and Begin() + //does not. A driver may have claimed the pin since (nixie595.ino: SPI.begin() makes GPIO12 MISO). + pinMode(PixelPin, OUTPUT); + strip.Begin(); //initialize the underlying I2S/RMT/bit-bang driver and GPIO strip.Show(); //push the (still all-black, default-constructed) buffer out so the strip starts dark, not showing stale/garbage pixel data strip.SetBrightness(neoBrightness); fixColor(prm.rgbFixColor); //apply the persisted fixed color immediately so the backlight is correct even before the first doAnimationMakuna() tick diff --git a/firmware/uniclock/platformio.ini b/firmware/uniclock/platformio.ini index 117e72a..2b4344f 100644 --- a/firmware/uniclock/platformio.ini +++ b/firmware/uniclock/platformio.ini @@ -28,3 +28,10 @@ upload_resetmethod = ck monitor_port = COM7 monitor_speed = 115200 monitor_filters = esp8266_exception_decoder + +; Same firmware, uploaded over WiFi with ArduinoOTA (espota). Add --auth= to +; upload_flags once secrets.h sets one. +[env:nixie_in12_ota] +extends = env:nixie_in12 +upload_protocol = espota +upload_port = 10.1.1.97