");
+ request->send(response);
+ */
+ });
+
+//____________________________________________________________________________
+ server.on("/site.css", HTTP_GET, [](AsyncWebServerRequest * request) {
+ DPRINTLN("Webserver: /site.css");
+ if (SPIFFS.exists("/site.css")) {
+ AsyncWebServerResponse *response = request->beginResponse(SPIFFS, "/site.css", "text/css");
+ response->addHeader("Cache-Control", CACHE_MAX_AGE);
+ request->send(response);
+ }
+ else {
+ request->send( 204, "text/html", "File Not Found" );
+ DPRINTLN("/site.css not found");
+ }
+ });
+//____________________________________________________________________________
+ server.on("/reset", HTTP_POST, [] (AsyncWebServerRequest *request) {
+ DPRINTLN("/reset:");
+
+ request->send(200, "text/plain", "Reset: Restarting the Box!");
+ delay(200);
+ doReset();
+ });
+//____________________________________________________________________________
+ server.on("/factoryreset", HTTP_POST, [] (AsyncWebServerRequest *request) {
+ DPRINTLN("/factoryreset:");
+
+ request->send(200, "text/plain", "Factory Reset!");
+ delay(200);
+ factoryReset();
+ doReset();
+ });
+
+//____________________________________________________________________________
+ server.on("/firmwareupdate", HTTP_POST, [](AsyncWebServerRequest * request) {
+ makeFirmwareUpdate = true;
+ });
+//____________________________________________________________________________
+ server.on("/cathodeProtect", HTTP_POST, [](AsyncWebServerRequest * request) {
+ makeCathodeProtect = true;
+ });
+
+
+ server.on("/saveSetting", HTTP_POST, handleConfigChanged);
+ server.on("/getConfiguration", HTTP_GET, handleSendConfig);
+ server.on("/getCurrentInfos", HTTP_GET, handleSendCurrentInfos);
+ server.on("/getClockDetails", HTTP_GET, handleSendClockDetails);
+ server.onNotFound(handleNotFound);
+
+ server.begin();
+
+} //end of procedure
+
+// Catch-all 404 handler (server.onNotFound) - also doubles as a debug request-dumper (prints every
+// request param, GET/POST, to the serial console when DEBUG is on) which is handy for reverse-
+// engineering what the SPA's JS is actually sending during development.
+void handleNotFound(AsyncWebServerRequest *request) {
+ int params = request->params();
+ for (int i = 0; i < params; i++) {
+ #ifdef DEBUG
+ AsyncWebParameter* p = request->getParam(i);
+ if (p->isFile()) { //p->isPost() is also true
+ Serial.printf("FILE[%s]: %s, size: %u\n", p->name().c_str(), p->value().c_str(), p->size());
+ } else if (p->isPost()) {
+ Serial.printf("POST[%s]: %s\n", p->name().c_str(), p->value().c_str());
+ } else {
+ Serial.printf("GET[%s]: %s\n", p->name().c_str(), p->value().c_str());
+ }
+ #endif
+ }
+
+ String message = "File Not Found\n\n";
+ message += "URL: ";
+ message += request->url();
+ message += "\nMethod: ";
+ message += ( request->method() == HTTP_GET ) ? "GET" : "POST";
+ message += "\nArguments: ";
+ message += request->args();
+ message += "\n";
+
+ for ( uint8_t i = 0; i < request->args(); i++ ) {
+ message += " " + request->argName ( i ) + ": " + request->arg ( i ) + "\n";
+ }
+
+ DPRINTLN(message);
+ request->send( 204, "text/html", "URL Not Found" );
+}
+
+// POST /saveSetting - the SINGLE entry point for every settings change made from the web admin UI.
+// Request carries two form fields, `key` (a string identifying which setting) and `value` (its new
+// value as a string) - handled as one long if/else-if chain matching `key`, converting `value` to the
+// right type and writing it into the matching prm.* field (or, for a few keys, a plain global like
+// c_MinBrightness/cathProtMin/autoBrightness). Any recognized key ends with saveEEPROM() so the
+// change survives a reboot; an unrecognized key returns HTTP 404 without touching EEPROM. Most
+// branches are simple 1:1 field assignments and aren't commented individually below - only branches
+// with side effects beyond "store this value" are called out.
+void handleConfigChanged(AsyncWebServerRequest *request) {
+ if (request->hasParam("key", true) && request->hasParam("value", true)) {
+
+ //int args = request->args();
+
+ //for(int i=0;iargName(i).c_str(), request->arg(i).c_str());
+ //}
+ boolean oldDST = prm.enableDST;
+ int old_utc_offset = prm.utc_offset;
+
+ String key = request->getParam("key", true)->value();
+ String value = request->getParam("value", true)->value();
+ DPRINT(key); DPRINT(" = "); DPRINTLN(value);
+
+ boolean paramFound = true;
+
+ if (key == "utc_offset") {
+ // Changing the UTC offset immediately shifts the LIVE running clock (not just the stored
+ // preference) by the delta in hours, and pushes that change into the RTC right away via
+ // updateRTC() - so the displayed time updates instantly rather than waiting for the next NTP
+ // sync (which would otherwise take up to TIMESERVER_REFRESH to reflect the new zone).
+ prm.utc_offset = value.toInt();
+ if (old_utc_offset != prm.utc_offset) { //Change time zone
+ setTime(now()+(prm.utc_offset-old_utc_offset)*3600);
+ updateRTC();
+ }
+ }
+ else if (key == "set12_24") {
+ prm.set12_24 = (value == "true");
+ DPRINT("set12_24:");
+ DPRINTLN(prm.set12_24);
+ }
+ else if (key == "showZero") {
+ prm.showZero = (value == "true");
+ DPRINT("showZero:");
+ DPRINTLN(prm.showZero);
+ }
+ else if (key == "enableBlink") {
+ prm.enableBlink = (value == "true");
+ }
+ else if (key == "enableDST") {
+ // Same immediate-effect pattern as utc_offset above: toggling DST support live-shifts the
+ // 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).
+ 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);
+ updateRTC();
+ }
+ }
+ else if (key == "interval") {
+ prm.interval = value.toInt();
+ }
+ else if (key == "enableAutoShutoff") {
+ prm.enableAutoShutoff = (value == "true");
+ }
+
+ else if (key == "dayTimeHours") {
+ prm.dayHour = value.toInt();
+ }
+ else if (key == "dayTimeMinutes") {
+ prm.dayMin = value.toInt();
+ }
+ else if (key == "nightTimeHours") {
+ prm.nightHour = value.toInt();
+ }
+ else if (key == "nightTimeMinutes") {
+ prm.nightMin = value.toInt();
+ }
+ else if (key == "dayBright") {
+ prm.dayBright = value.toInt();
+ }
+ else if (key == "nightBright") {
+ prm.nightBright = value.toInt();
+ }
+ else if (key == "animMode") {
+ prm.animMode = value.toInt();
+ }
+ else if (key == "manualOverride") {
+ // Not a prm.* field - "manualOverride" is the web UI's day/night ON/OFF toggle: it flips the
+ // live displayON state directly and sets the manualOverride flag so evalShutoffTime() (called
+ // every second from timeProgram()) stops auto-driving displayON from the day/night schedule
+ // until the schedule naturally catches up to the state the user forced (see evalShutoffTime()).
+ boolean v = value == "false";
+ if (v != displayON) {
+ manualOverride = true;
+ displayON = v;
+ }
+ }
+ else if (key == "wifiMode") {
+ prm.wifiMode = (value == "true");
+ }
+ else if (key == "alarmEnable") {
+ prm.alarmEnable = (value == "true");
+ }
+ else if (key == "alarmTimeHours") {
+ prm.alarmHour = value.toInt();
+ }
+ else if (key == "alarmTimeMinutes") {
+ prm.alarmMin = value.toInt();
+ }
+ else if (key == "alarmPeriod") {
+ prm.alarmPeriod = value.toInt();
+ }
+
+ //RGB LED values
+ else if (key == "rgbEffect") {
+ prm.rgbEffect = value.toInt();
+ }
+ else if (key == "rgbBrightness") {
+ prm.rgbBrightness = value.toInt();
+ }
+ else if (key == "rgbFixColor") {
+ prm.rgbFixColor = value.toInt();
+ }
+ else if (key == "rgbSpeed") {
+ prm.rgbSpeed = value.toInt();
+ }
+ else if (key == "rgbDir") {
+ prm.rgbDir = (value == "true");
+ }
+ else if (key == "rgbMinBrightness") {
+ c_MinBrightness = value.toInt();
+ }
+ else if (key == "wifiSsid") {
+ value.toCharArray(prm.wifiSsid,sizeof(prm.wifiSsid));
+ }
+ else if (key == "wifiPsw") {
+ value.toCharArray(prm.wifiPsw,sizeof(prm.wifiPsw));
+ }
+ else if (key == "ApSsid") {
+ // Sanitizes the OLD prm.ApSsid in place (replacing any non-printable/non-ASCII byte with '_')
+ // BEFORE overwriting it with the new value from the web form - i.e. this cleans up a
+ // possibly-corrupted stored SSID from a prior session, not the incoming `value` itself (same
+ // repair loop also runs unconditionally in factoryReset(), see below).
+ for (int i=0;i<(int)strlen(prm.ApSsid);i++) { //repair bad chars in AP SSID
+ if ((prm.ApSsid[i]<32) || (prm.ApSsid[i]>126)) prm.ApSsid[i]='_';
+ }
+ value.toCharArray(prm.ApSsid,sizeof(prm.ApSsid));
+ }
+ else if (key == "ApPsw") {
+ value.toCharArray(prm.ApPsw,sizeof(prm.ApPsw));
+ }
+ else if (key == "NtpServer") {
+ value.toCharArray(prm.NtpServer,sizeof(prm.NtpServer));
+ }
+ else if (key == "firmware") {
+ // Side effect: merely changing the firmware-server URL setting also immediately kicks off an
+ // OTA update attempt (makeFirmwareUpdate, picked up by loop()->doFirmwareUpdate()) - per the
+ // "for testing only" comment this looks like a leftover dev convenience rather than intended
+ // production behavior (a user just correcting a typo in the URL would trigger a real update).
+ value.toCharArray(prm.firmwareServer,sizeof(prm.firmwareServer));
+ makeFirmwareUpdate = true; //for testing only
+ }
+ else if (key == "mqttBrokerAddr") {
+ value.toCharArray(prm.mqttBrokerAddr,sizeof(prm.mqttBrokerAddr));
+ }
+ else if (key == "mqttBrokerUser") {
+ value.toCharArray(prm.mqttBrokerUser,sizeof(prm.mqttBrokerUser));
+ }
+ else if (key == "mqttBrokerPsw") {
+ value.toCharArray(prm.mqttBrokerPsw,sizeof(prm.mqttBrokerPsw));
+ }
+ else if (key == "mqttBrokerRefresh") {
+ prm.mqttBrokerRefresh = value.toInt();
+ }
+ else if (key == "mqttEnable") {
+ prm.mqttEnable = (value == "true");
+ }
+ else if (key == "dateMode") {
+ prm.dateMode = value.toInt();
+ }
+ else if (key == "dateStart") {
+ prm.dateStart = value.toInt();
+ }
+ else if (key == "dateEnd") {
+ prm.dateEnd = value.toInt();
+ }
+ else if (key == "tempStart") {
+ prm.tempStart = value.toInt();
+ }
+ else if (key == "tempEnd") {
+ prm.tempEnd = value.toInt();
+ }
+ else if (key == "humidStart") {
+ prm.humidStart = value.toInt();
+ }
+ else if (key == "humidEnd") {
+ prm.humidEnd = value.toInt();
+ }
+ else if (key == "dateRepeatMin") {
+ prm.dateRepeatMin = value.toInt();
+ if (prm.dateRepeatMin>10) prm.dateRepeatMin = 10;
+ }
+ else if (key == "tempRepeatMin") {
+ prm.tempRepeatMin = value.toInt();
+ if (prm.tempRepeatMin>10) prm.tempRepeatMin = 10;
+ }
+ else if (key == "enableDoubleBlink") {
+ prm.enableDoubleBlink = (value == "true");
+ }
+ else if (key == "enableTimeDisplay") {
+ prm.enableTimeDisplay = (value == "true");
+ }
+ else if (key == "enableAutoDim") {
+ prm.enableAutoDim = (value == "true");
+ autoBrightness = prm.enableAutoDim; //mirrors the flag into the live `autoBrightness` global read by getLightSensor()/the display brightness logic, so the change takes effect immediately rather than only after the next getLightSensor() poll
+ }
+ else if (key == "enableRadar") {
+ prm.enableRadar = (value == "true");
+ radarLastOn = millis(); //resets the radar "last motion seen" timestamp so toggling the feature on doesn't immediately look timed-out and shut the tubes off
+ radarON = true;
+ }
+ else if (key == "radarTimeout"){
+ prm.radarTimeout = value.toInt();
+ if (prm.radarTimeout>60) prm.radarTimeout = 60;
+ if (prm.radarTimeout<1) prm.radarTimeout = 1;
+ radarLastOn = millis(); //same reasoning as enableRadar above - avoid an immediate false shutoff after changing the timeout value
+ radarON = true;
+ }
+
+ else if (key == "tempCF") {
+ prm.tempCF = (value == "true");
+ }
+ else if (key == "corrT0") {
+ prm.corrT0 = value.toFloat();
+ }
+ else if (key == "corrT1") {
+ prm.corrT1 = value.toFloat();
+ }
+ else if (key == "corrH0") {
+ prm.corrH0 = value.toFloat();
+ }
+ else if (key == "corrH1") {
+ prm.corrH1 = value.toFloat();
+ }
+ else if (key == "cathProtMin") {
+ cathProtMin = value.toInt();
+ }
+ else {
+ paramFound = false;
+ }
+ if (paramFound) {
+ saveEEPROM();
+ request->send(200, "text/plain", "Ok");
+ }
+ else {
+ request->send(404, "text/plain", "404: Parameter not found");
+ }
+ }
+ else {
+ request->send(400, "text/plain", "400: Invalid Request. Parameters: key and value");
+ }
+}
+
+// GET /getConfiguration - dumps (almost) the entire `prm` struct plus a handful of live/derived values
+// (current date/time, sensor readings, MAC, driver name...) as one JSON object. This is what the web
+// SPA fetches once on page load to populate every settings form field; a "255" sentinel value marks a
+// sensor reading/feature as "not installed" (the JS treats 255 specially rather than 0, since 0 is a
+// plausible real reading). Compare with handleSendCurrentInfos() below, which is the lightweight,
+// frequently-polled subset used to keep the live clock/sensor display refreshed without re-sending
+// the whole settings form each time.
+void handleSendConfig(AsyncWebServerRequest *request) {
+ StaticJsonDocument<2048> doc;
+ char buf[20]; //conversion buffer
+
+ DPRINTLN("Sending configuration to web client.");
+
+ //Global data
+ doc["version"] = webName;
+ doc["FW"] = FW;
+ doc["tubeDriver"] = tubeDriver;
+ doc["maxDigits"] = maxDigits; //number of digits (tubes)
+ doc["maxBrightness"] = MAXBRIGHTNESS; //Maximum tube brightness usually 10, sometimes 12
+
+ //Actual time and environment data
+ sprintf(buf, "%4d.%02d.%02d", year(), month(), day());
+ doc["currentDate"] = buf;
+ sprintf(buf, "%02d:%02d", hour(), minute());
+ doc["currentTime"] = buf;
+ //DPRINT("useTemp:"); DPRINT(useTemp); DPRINT("useHumid:"); DPRINT(useHumid);
+ if (useTemp > 0) {
+ doc["temperature"] = round1(temperature[0] + prm.corrT0);
+ }
+ else
+ doc["temperature"] = 255;
+
+if (useTemp > 1)
+ doc["temperature2"] = round1(temperature[1] + prm.corrT1);
+ else
+ doc["temperature2"] = 255;
+
+ if (useHumid>0) {
+ doc["humidity"] = round1(humid[0] + prm.corrH0);
+ }
+ else
+ doc["humidity"] = 255;
+
+ if (useHumid>1)
+ doc["humidity2"] = round1(humid[1] + prm.corrH1);
+ else
+ doc["humidity2"] = 255;
+
+ if (usePress>0) {
+ doc["pressure"] = pressur[0];
+ }
+ else
+ doc["pressure"] = 255;
+
+ if (useLux>0) {
+ doc["lux"] = lx;
+ }
+ else
+ doc["lux"] = 255;
+
+ //Clock calculation and display parameters
+ doc["utc_offset"] = prm.utc_offset;
+ doc["enableDST"] = prm.enableDST; // Flag to enable DST (summer time...)
+ doc["set12_24"] = prm.set12_24; // Flag indicating 12 vs 24 hour time (false = 12, true = 24);
+ doc["showZero"] = prm.showZero; // Flag to indicate whether to show zero in the hour ten's place
+ doc["enableBlink"] = prm.enableBlink; // Flag to indicate whether center colon should blink
+ doc["interval"] = prm.interval; // interval in minutes, with 0 = off
+
+ //Day/Night dimmer parameters
+ doc["enableAutoShutoff"] = prm.enableAutoShutoff; // Flag to enable/disable nighttime shut off
+ sprintf(buf, "%02d:%02d", prm.dayHour, prm.dayMin);
+ doc["dayTime"] = buf;
+ sprintf(buf, "%02d:%02d", prm.nightHour, prm.nightMin);
+ doc["nightTime"] = buf;
+ doc["dayBright"] = prm.dayBright;
+ doc["nightBright"] = prm.nightBright;
+ doc["animMode"] = prm.animMode; //Tube animation
+ doc["manualOverride"] = !displayON;
+
+ //Alarm values
+ doc["alarmEnable"] = prm.alarmEnable; //1 = ON, 0 = OFF
+ sprintf(buf, "%02d:%02d", prm.alarmHour, prm.alarmMin);
+ doc["alarmTime"] = buf;
+ doc["alarmPeriod"] = prm.alarmPeriod;
+
+ //RGB LED values
+ #if defined(USE_NEOPIXEL) || defined(USE_PWMLEDS)
+ doc["rgbEffect"] = prm.rgbEffect; // if 255, no RGB exist!
+ #else
+ doc["rgbEffect"] = 255; //Not installed Neopixels!!!
+ #endif
+
+ doc["rgbBrightness"] = prm.rgbBrightness; // c_MinBrightness..255
+ doc["rgbFixColor"] = prm.rgbFixColor; // 0..256
+ doc["rgbSpeed"] = prm.rgbSpeed; // 1..255
+ doc["rgbDir"] = prm.rgbDir; // 0 = right direction, 1 = left direction
+ doc["rgbMinBrightness"] = c_MinBrightness; //minimum brightness for range check!!
+ doc["wifiMode"] = prm.wifiMode;
+ doc["wifiSsid"] = prm.wifiSsid;
+ doc["wifiPsw"] = prm.wifiPsw;
+ doc["ApSsid"] = prm.ApSsid;
+ doc["ApPsw"] = prm.ApPsw;
+ doc["NtpServer"] = prm.NtpServer;
+ doc["firmware"] = prm.firmwareServer;
+
+ doc["mqttBrokerAddr"] = prm.mqttBrokerAddr;
+ doc["mqttBrokerUser"] = prm.mqttBrokerUser;
+ doc["mqttBrokerPsw"] = prm.mqttBrokerPsw;
+ doc["mqttEnable"] = prm.mqttEnable;
+ #if defined(USE_MQTT)
+ doc["mqttBrokerRefresh"] = prm.mqttBrokerRefresh;
+ #else
+ doc["mqttBrokerRefresh"] = 0;
+ #endif
+ doc["dateMode"] = prm.dateMode;
+ doc["dateRepeatMin"] = prm.dateRepeatMin;
+ doc["tempRepeatMin"] = prm.tempRepeatMin;
+ doc["tempCF"] = prm.tempCF;
+ doc["enableTimeDisplay"] = prm.enableTimeDisplay;
+ doc["enableDoubleBlink"] = prm.enableDoubleBlink;
+ doc["dateStart"] = prm.dateStart;
+ doc["dateEnd"] = prm.dateEnd;
+ doc["tempStart"] = prm.tempStart;
+ doc["tempEnd"] = prm.tempEnd;
+ doc["humidStart"] = prm.humidStart;
+ doc["humidEnd"] = prm.humidEnd;
+ doc["enableAutoDim"] = prm.enableAutoDim;
+ doc["enableRadar"] = prm.enableRadar;
+ #if (RADAR_PIN >= 0) || defined(USE_MQTT)
+ if (prm.radarTimeout<1) prm.radarTimeout = 1;
+ doc["radarTimeout"] = prm.radarTimeout;
+ #else
+ doc["radarTimeout"] = 0;
+ #endif
+ doc["corrT0"] = prm.corrT0;
+ doc["corrT1"] = prm.corrT1;
+ doc["corrH0"] = prm.corrH0;
+ doc["corrH1"] = prm.corrH1;
+ doc["cathProtMin"] = 3; //default value for cathProtMin slider
+ String json;
+ serializeJson(doc, json);
+ request->send(200, "application/json", json);
+}
+
+// GET /getClockDetails - returns a pre-formatted HTML fragment (not JSON, unlike the other two
+// handlers) with static/diagnostic info: firmware ID, driver name, MAC address, sensor counts, and
+// the full used-GPIO listing built by listPins()/regPin() (usedPinsStr) plus any driver-supplied
+// extra setup info (driverSetupStr). Shown on an "about/diagnostics" panel of the web admin UI.
+void handleSendClockDetails(AsyncWebServerRequest *request) {
+ StaticJsonDocument<200> doc;
+ //StaticJsonDocument<512> doc;
+ String dat;
+
+ DPRINTLN("SendClockDetails");
+ dat = String("
FirmwareID:") + FW + " ";
+ dat += String(" Tube driver:") + tubeDriver + " ";
+ dat += String(" Mac:") + String(WiFi.macAddress()) + " ";
+ dat += String("MAXBRIGHTNESS:") + String(MAXBRIGHTNESS) + " ";
+ if (RTCexist) dat += String("RTC exist: YES ");
+ if (useTemp>0) dat += String("Temperature sensors:") + useTemp + " ";
+ if (useHumid>0) dat += String("Humidity sensors:") + useHumid + " ";
+ if (usePress>0) dat += String("Pressure sensors:") + usePress + " ";
+ dat += usedPinsStr + " ";
+ dat += driverSetupStr + " ";
+ DPRINTLN(dat);
+ request->send(200, "text/html", dat);
+}
+
+// GET /getCurrentInfos - the lightweight, frequently AJAX-polled JSON endpoint (date/time,
+// temperature/humidity/pressure/lux, WiFi RSSI) used to keep the web admin page's live readouts
+// current without re-fetching the whole settings form (see handleSendConfig() above). Keeps both new
+// ("temperature1"/"humidity1") and legacy ("temperature"/"humidity") key names for backwards
+// compatibility with older versions of the web front-end.
+void handleSendCurrentInfos(AsyncWebServerRequest *request) {
+ StaticJsonDocument<400> doc;
+ //StaticJsonDocument<512> doc;
+ char buf[20]; //conversion buffer
+
+ DPRINTLN("WebRefresh");
+ //Actual time and environment data
+ sprintf(buf, "%4d.%02d.%02d", year(), month(), day());
+ doc["currentDate"] = buf;
+ sprintf(buf, "%02d:%02d", hour(), minute());
+ doc["currentTime"] = buf;
+ //DPRINT("useTemp:"); DPRINT(useTemp); DPRINT("useHumid:"); DPRINT(useHumid);
+ if (useTemp > 0) {
+ doc["temperature1"] = temperature[0] + prm.corrT0;
+ doc["temperature"] = temperature[0] + prm.corrT0; //for compatibility with the old web page
+ }
+ else
+ doc["temperature1"] = 255;
+
+if (useTemp > 1)
+ doc["temperature2"] = temperature[1] + prm.corrT1;
+ else
+ doc["temperature2"] = 255;
+
+ if (useHumid>0) {
+ doc["humidity1"] = humid[0] + prm.corrH0;
+ doc["humidity"] = humid[0] + prm.corrH0; //for compatibility with the old web page
+ }
+ else
+ doc["humidity1"] = 255;
+
+ if (useHumid>1)
+ doc["humidity2"] = humid[1] + prm.corrH1;
+ else
+ doc["humidity2"] = 255;
+
+ if (usePress>0) {
+ doc["pressure"] = pressur[0];
+ }
+ else
+ doc["pressure"] = 255;
+
+ if (useLux>0) {
+ doc["lux"] = lx;
+ }
+ else
+ doc["lx"] = 255;
+ doc["rssi"] = WiFi.RSSI();
+ String json;
+ serializeJson(doc, json);
+ request->send(200, "application/json", json);
+}
+
+// Startup sequence (see docs/DEVELOPER_GUIDE.md section 4.1 for the full ordered summary):
+// brownout detector (optional) -> EEPROM init -> setup_pins() (driver-specific GPIO setup) ->
+// optional peripherals (alarm/colon/radar/tube-power/light-sensor/PWM LEDs) -> temperature/I2C/RTC/GPS
+// setup -> loadEEPROM() (factoryReset() if the magic number doesn't match) -> NeoPixel setup -> tube
+// test animation -> SPIFFS mount (web page files) -> WiFi connect (STA or AP mode) -> web server start
+// -> MQTT setup -> show IP address on the tubes -> initial time sync. Order matters here: pins must be
+// configured before any peripheral touches them, and EEPROM must be loaded before WiFi connects
+// (credentials live in prm).
+void setup() {
+ #if defined(DISABLE_BROWNOUT) && defined(ESP32)
+ WRITE_PERI_REG(RTC_CNTL_BROWN_OUT_REG, 0); //disable brownout detector
+ #endif
+ delay(1000);
+ WiFi.mode(WIFI_OFF);
+ EEPROM.begin(EEPROM_SIZE);
+ memset(pinTxt,0,sizeof(pinTxt));
+ DPRINTBEGIN(115200); DPRINTLN(" ");
+ DPRINTLN(F("================================================="));
+ DPRINT("Starting "); DPRINTLN(webName);
+ DPRINT("Firmware code:"); DPRINT(FW); DPRINT(" Tube driver:"); DPRINTLN(tubeDriver);
+ DPRINT("MAXBRIGHTNESS:"); DPRINTLN(MAXBRIGHTNESS);
+ DPRINTLN(F("================================================="));
+ clearDigits();
+ setup_pins();
+ #if ALARMSPEAKER_PIN >= 0
+ pinMode(ALARMSPEAKER_PIN, OUTPUT); regPin(ALARMSPEAKER_PIN,"ALARMSPEAKER_PIN");
+ digitalWrite(ALARMSPEAKER_PIN,!ALARM_ON);
+ DPRINT(" - ON state:");
+ if (ALARM_ON == HIGH) {DPRINTLN("HIGH"); }
+ else {DPRINTLN("LOW"); }
+ #endif
+ #if ALARMBUTTON_PIN >= 0
+ pinMode(ALARMBUTTON_PIN, INPUT_PULLUP); regPin(ALARMBUTTON_PIN,"ALARMBUTTON_PIN");
+ #endif
+ #if COLON_PIN >= 0
+ pinMode(COLON_PIN, OUTPUT); regPin(COLON_PIN,"COLON_PIN");
+ #endif
+ #if LED_SWITCH_PIN >= 0
+ pinMode(LED_SWITCH_PIN, OUTPUT); regPin(LED_SWITCH_PIN,"LED_SWITCH_PIN");
+ #endif
+ #if DECIMALPOINT_PIN >= 0
+ pinMode(DECIMALPOINT_PIN, OUTPUT); regPin(DECIMALPOINT_PIN,"DECIMALPOINT_PIN");
+ #endif
+ #if RADAR_PIN >= 0
+ pinMode(RADAR_PIN, INPUT); regPin(RADAR_PIN,"RADAR_PIN");
+ DPRINT(" - RADAR Timeout:"); DPRINTLN(prm.radarTimeout);
+ #endif
+ #if TUBE_POWER_PIN >= 0
+ pinMode(TUBE_POWER_PIN, OUTPUT); regPin(TUBE_POWER_PIN,"TUBE_POWER_PIN");
+ digitalWrite(TUBE_POWER_PIN,TUBE_POWER_ON);
+ DPRINT(" - ON state:");
+ if (TUBE_POWER_ON == HIGH) {DPRINTLN("HIGH"); }
+ else {DPRINTLN("LOW"); }
+ #endif
+ #if LIGHT_SENSOR_PIN >= 0
+ pinMode(LIGHT_SENSOR_PIN, INPUT); regPin(LIGHT_SENSOR_PIN,"LIGHT_SENSOR_PIN");
+ DPRINT(" - MAXIMUM_LUX for MAXBRIGHTNESS:"); DPRINTLN(MAXIMUM_LUX);
+ useLux++;
+ LDRexist = true;
+ #endif
+
+ #ifdef USE_PWMLEDS
+ #if PWM1_PIN >= 0
+ pinMode(PWM1_PIN, OUTPUT); regPin(PWM1_PIN,"PWM1_PIN");
+ ledcAttachPin(PWM1_PIN, 0);
+ ledcSetup(0, 200, 8); // 12
+ #endif
+ #if PWM2_PIN >= 0
+ pinMode(PWM2_PIN, OUTPUT); regPin(PWM2_PIN,"PWM2_PIN");
+ ledcAttachPin(PWM2_PIN, 1);
+ ledcSetup(1, 200, 8); // 12
+ #endif
+ #if PWM3_PIN >= 0
+ pinMode(PWM3_PIN, OUTPUT); regPin(PWM3_PIN,"PWM3_PIN");
+ ledcAttachPin(PWM3_PIN, 2);
+ ledcSetup(2, 200, 8); // 12
+ #endif
+ #endif
+
+ #if defined(PIN_EXTINP1)
+ if (PIN_EXTINP1>=0)
+ pinMode(PIN_EXTINP1, INPUT); regPin(PIN_EXTINP1,"NOT_USED_INPUT");
+ #endif
+ #if defined(PIN_EXTINP2)
+ if (PIN_EXTINP2>=0)
+ pinMode(PIN_EXTINP2, INPUT); regPin(PIN_EXTINP2,"NOT_USED_INPUT");
+ #endif
+
+ #if TEMP_DALLAS_PIN >= 0
+ setupDallasTemp();
+ #endif
+ #if TEMP_DHT_PIN >= 0
+ setupDHTemp();
+ #endif
+ #ifdef USE_I2CSENSORS
+ setupI2Csensors();
+ #endif
+
+ #ifdef USE_RTC
+ setupRTC();
+ #endif
+
+ #ifdef USE_GPS
+ setupGPS();
+ #endif
+
+ decimalpointON = false;
+ DPRINT("Number of digits:"); DPRINTLN(maxDigits);
+
+ #ifdef FACTORYRESET_PIN
+ if (FACTORYRESET_PIN>=0) {
+ DPRINTLN("FactoryReset button is pushed!");
+ pinMode(FACTORYRESET_PIN,INPUT_PULLUP); regPin(FACTORYRESET_PIN,"FACTORYRESET_PIN");
+ if (digitalRead(FACTORYRESET_PIN)==LOW) factoryReset();
+ }
+ #endif
+ loadEEPROM();
+ if (prm.magic != MAGIC_VALUE) factoryReset(); //EEPROM struct version mismatch (see MAGIC_VALUE comment) - wipe to sane defaults
+
+ setupNeopixel();
+ listPins();
+ writeAlarmPin(ALARM_ON); writeAlarmPin(!ALARM_ON); //quick blip of the alarm pin at boot (on then immediately off) - a self-test/audible "I'm alive" click, not a real alarm
+ getDHTemp(); //get the first DHT temp+humid measure
+
+ // Temporarily force animMode to 2 (all changed tubes spin together) for the power-on tube test
+ // below, regardless of the user's saved animation preference, then restore it afterwards - ensures
+ // testTubes() always animates visibly even if the stored prm.animMode is 0 (no animation).
+ byte saveMode = prm.animMode;
+ prm.animMode = 2;
+ doAnimationPWM();
+ testTubes(300);
+
+ clearDigits();
+ Fdelay(200);
+ if (!SPIFFS.begin()) {
+ DPRINTLN("An Error has occurred while mounting SPIFFS");
+ }
+ else {
+ #if defined(ESP32)
+ DPRINT("SPIFFS Total bytes: "); DPRINTLN(SPIFFS.totalBytes());
+ DPRINT("SPIFFS Used bytes: "); DPRINTLN(SPIFFS.usedBytes());
+ #endif
+ DPRINTLN("SPIFFS started.");
+ if(!SPIFFS.exists("/index.html")) {DPRINTLN("/index.html not found!");}
+ if(!SPIFFS.exists("/site.css")) {DPRINTLN("/site.css not found!");}
+ if(!SPIFFS.exists("/page.js")) {DPRINTLN("/page.js not found!");}
+ if(!SPIFFS.exists("/jquery_360.js")) {DPRINTLN("/jquery_360.js not found!");}
+ }
+ memset(digit,10,sizeof(digit)); //clear display
+ memset(newDigit,10,sizeof(newDigit));
+ memset(oldDigit,10,sizeof(oldDigit));
+
+ wifiConnectRunning = true;
+ writeDisplay2();
+ if (prm.wifiMode) {
+ //findBestWifi();
+ startWifiMode(); //note: startWifiMode() already falls back to wifiManager() internally on failure/empty SSID - the retry loop below is an extra layer only exercised when USE_WIFIMANAGER is enabled
+
+ #ifdef USE_WIFIMANAGER
+ int counter = 0;
+ while (WiFi.status() != WL_CONNECTED) { //try to use wifiManeger, if enabled
+ DPRINT('.');
+ Fdelay(3000);
+ if (counter++>2) {
+ wifiManager();
+ break;
+ }
+ }
+ DPRINTLN(" ");
+ #endif
+
+ if (WiFi.status() != WL_CONNECTED) { //failed to connect to wifi - give up on infrastructure mode entirely and fall back to hosting our own AP so the clock is at least reachable
+ startStandaloneMode();
+ }
+ else {
+ ip = WiFi.localIP();
+ WiFi.setAutoReconnect(true);
+ }
+ }
+ else {
+ startStandaloneMode();
+ }
+ wifiConnectRunning = false;
+
+ startMDNS();
+ startServer();
+ #ifdef USE_MQTT
+ setupMqtt();
+ #endif
+
+ showMyIp();
+ Fdelay(500);
+ clearDigits();
+ Fdelay(200);
+ prm.animMode = saveMode;
+ timeClient.begin();
+ calcTime();
+ timeProgram();
+
+ //newCathodeProtect(5000,0);
+}
+
+// Time-source priority chain (docs/DEVELOPER_GUIDE.md section 4.4): NTP-over-WiFi is always tried
+// first when connected (updateTimefromTimeserver() self-throttles to once per TIMESERVER_REFRESH, so
+// this is cheap to call every cycle); GPS is tried next if fitted; the DS3231 RTC (if fitted) is the
+// LAST-RESORT fallback: if either of the above actually refreshed the system clock this cycle, the RTC
+// is instead WRITTEN (updateRTC(), to keep it in sync as a fallback for the next power-loss/no-network
+// boot) - otherwise the RTC is READ (getRTC()) to keep the clock ticking with no network/GPS at all.
+// A build with none of WiFi/GPS/RTC available simply free-runs on TimeLib's own millis()-derived clock
+// (set once at some point in the past) with no further correction.
+void calcTime() {
+ boolean refreshed = false;
+ boolean refreshed2 = false;
+
+ if (WiFi.status() == WL_CONNECTED) { //check wifi connection
+ refreshed = updateTimefromTimeserver(); //update time from wifi
+ }
+ if (GPSexist) { //update time from GPS, if exist
+ refreshed2 = getGPS();
+ }
+
+ if (RTCexist) { //update time from RTC, if exist
+ if (refreshed || refreshed2)
+ updateRTC(); //update RTC, if needed
+ else
+ getRTC();
+ }
+}
+
+
+// Main per-tick clock/sensor/display driver, called unconditionally every loop() iteration but
+// self-throttled to run at most every 200ms (calcTime()/sensor polling cadence) - the actual visible
+// digit refresh only happens on real second boundaries (`if (now() != prevTime)` below), so most 200ms
+// ticks only refresh sensor readings and return without touching the display buffers.
+void timeProgram() {
+ static unsigned long lastRun = 0;
+
+ if ((millis() - lastRun) < 200) return;
+ lastRun = millis();
+ calcTime();
+ if (useDallasTemp > 0) {
+ requestDallasTemp(false); //start measure
+ getTemp(); //get result, if ready
+ }
+ getDHTemp();
+ getI2Csensors(); //check all existing I2C sensors
+
+ #ifdef SIMULATE_TEMPERATURE_SENSOR //for test only
+ useTemp = 2; temperature[0] = 20.4; temperature[1] = 22.8; //test only
+ useHumid = 2; humid[0] = 41.2; humid[1] = 42.7;
+ #endif
+
+ if (now() != prevTime) { // Update time every second
+ prevTime = now();
+ evalShutoffTime(); // Check whether display should be turned off (Auto shutoff mode)
+ if (!displayON || !prm.enableBlink) colonBlinkState = false;
+ else colonBlinkState = (bool)(second() % 2);
+
+ // Decide which "screen" to show this second, in priority order: temperature > humidity > date >
+ // plain clock (see the showClock/showDate/showTemp*/showHumid* globals declared near the top of
+ // this file). Each window is an independent [xStart,xEnd) range of seconds-within-minute, so e.g.
+ // temperature and date windows can be configured to overlap or not from the web UI; whichever
+ // display*() function actually runs is decided later in displayTime4/6/8() by checking these
+ // flags in the same priority order.
+ showClock = false;
+ showDate = (prm.enableTimeDisplay && (prm.dateRepeatMin>0) && ((second() >= prm.dateStart) && (second() < prm.dateEnd)));
+ if (showDate && (prm.dateRepeatMin>1)) {
+ if ((minute() % prm.dateRepeatMin) != 0) {
+ showDate = false;
+ }
+ }
+//useTemp = 1; temperature[0] = -12.34; useHumid = 1; humid[0] = 26.7; //for test only
+ showTemp0 = (useTemp > 0) && (second() >= prm.tempStart) && (second() < prm.tempEnd);
+ showTemp1 = (useTemp > 1) && (second() >= prm.tempStart + (prm.tempEnd - prm.tempStart) / 2) && (second() < prm.tempEnd);
+ showHumid0 = (useHumid > 0) && (second() >= prm.humidStart) && (second() < prm.humidEnd);
+ showHumid1 = (useHumid >1) && (second() >= prm.humidStart + (prm.humidEnd-prm.humidStart)/2) && (second() < prm.humidEnd);
+ if ((prm.tempRepeatMin==0) || ((minute() % prm.tempRepeatMin) != 0)) {
+ showTemp0 = false;
+ showTemp1 = false;
+ showHumid0 = false;
+ showHumid1 = false;
+ }
+ if (maxDigits >= 8) displayTime8();
+ else if (maxDigits == 6) displayTime6();
+ else displayTime4();
+ // if (COLON_PIN>=0) digitalWrite(COLON_PIN,colonBlinkState); // Blink colon pin
+ #if LED_SWITCH_PIN >= 0
+ if (displayON) //Switch on backlight LED only daytime
+ digitalWrite(LED_SWITCH_PIN, HIGH);
+ else
+ digitalWrite(LED_SWITCH_PIN, LOW);
+ #endif
+ changeDigit();
+ printDigits(0);
+
+ if (prm.interval > 0) { // protection is enabled
+ // At the first top of the hour, initialize protection logic timer
+ if (!initProtectionTimer && (minute() == 0)) {
+ protectTimer = 0; // Ensures protection logic will be immediately triggered
+ initProtectionTimer = true;
+ }
+ if ((now() - protectTimer) >= 60 * prm.interval) {
+ protectTimer = now();
+ // The current time can drift slightly relative to the protectTimer when NIST time is updated
+ // Need to make a small adjustment to the timer to ensure it is triggered at the minute change
+ protectTimer -= ((second() + 30) % 60 - 30);
+ // random(3)-1 picks the animation direction for this run: -1, 0 (random-order) or +1 - see
+ // newCathodeProtect()'s dir parameter and its POSSIBLE BUG note further down.
+ if (displayON && (millis() > 50000)) newCathodeProtect(maxDigits*1500,random(3)-1); //don't play in the first 50 sec
+ }
+ }
+ }
+}
+
+// Raw single-block read of the whole `prm` struct from EEPROM address 0 - does NOT itself check
+// prm.magic; the caller (setup()) is responsible for calling factoryReset() if the loaded magic value
+// doesn't match MAGIC_VALUE.
+void loadEEPROM() {
+ DPRINT("Loading setting from EEPROM. Size:"); DPRINT(EEPROM_SIZE);
+
+ EEPROM.get(EEPROM_addr, prm);
+ DPRINT(" version:"); DPRINTLN(prm.magic);
+}
+
+// Raw single-block write of the whole `prm` struct back to EEPROM (address 0) - called after every
+// successful key/value change in handleConfigChanged(), so every settings tweak from the web UI does
+// a full-struct EEPROM commit (flash wear consideration: frequent single-field changes each cost a
+// full-struct flash write, not just the changed bytes).
+void saveEEPROM() {
+ EEPROM.put(EEPROM_addr, prm);
+ EEPROM.commit();
+ DPRINTLN("Settings saved to EEPROM!");
+}
+
+// Resets every prm.* field to its hard-coded default (falling back to clocks.h-provided compile-time
+// defaults like DEFAULT_SSID/AP_NAME/DATE_START/etc where available), then saves and re-syncs the
+// clock. Triggered by: EEPROM magic-number mismatch at boot (see setup()), the web admin's
+// POST /factoryreset, or (if wired) a held FACTORYRESET_PIN button at boot.
+void factoryReset() {
+ DPRINTLN("Factory Reset!!!");
+ prm.alarmEnable = false;
+ prm.alarmHour = 7;
+ prm.alarmMin = 0;
+ prm.alarmPeriod = 15;
+ prm.rgbEffect = 2;
+ prm.rgbBrightness = 80;
+ prm.rgbFixColor = 150;
+ prm.rgbSpeed = 50;
+ prm.rgbDir = 0;
+ prm.wifiMode = true;
+ #ifdef DEFAULT_SSID
+ strncpy(prm.wifiSsid,DEFAULT_SSID,sizeof(prm.wifiSsid));
+ #else
+ strcpy(prm.wifiSsid,"");
+ #endif
+ #ifdef DEFAULT_PSW
+ strncpy(prm.wifiPsw,DEFAULT_PSW,sizeof(prm.wifiPsw));
+ #else
+ strcpy(prm.wifiPsw,"");
+ #endif
+ for (int i=0;i<(int)strlen(prm.ApSsid);i++) { //repair bad chars in AP SSID
+ if ((prm.ApSsid[i]<32) || (prm.ApSsid[i]>126)) prm.ApSsid[i]='_';
+ }
+ strncpy(prm.ApSsid,AP_NAME,sizeof(prm.ApSsid));
+ strncpy(prm.ApPsw,AP_PASSWORD,sizeof(prm.ApPsw));
+ strncpy(prm.NtpServer,"pool.ntp.org",sizeof(prm.NtpServer));
+ strcpy(prm.mqttBrokerAddr,"10.10.0.202");
+ strcpy(prm.mqttBrokerUser,"mqtt");
+ strcpy(prm.mqttBrokerPsw,"mqtt");
+ strncpy(prm.firmwareServer,FIRMWARE_SERVER,sizeof(prm.firmwareServer));
+ prm.mqttEnable = false;
+ prm.mqttBrokerRefresh = 10; //sec
+ prm.utc_offset = 1;
+ prm.enableDST = false; // Flag to enable DST (summer time...)
+ prm.set12_24 = true; // Flag indicating 12 vs 24 hour time (false = 12, true = 24);
+ prm.showZero = true; // Flag to indicate whether to show zero in the hour ten's place
+ prm.enableBlink = true; // Flag to indicate whether center colon should blink
+ prm.interval = 15; // prm.interval in minutes, with 0 = off
+ prm.enableAutoShutoff = true; // Flag to enable/disable nighttime shut off
+ prm.dayHour = 7;
+ prm.dayMin = 0;
+ prm.nightHour = 20;
+ prm.nightMin = 0;
+ prm.dayBright = MAXBRIGHTNESS;
+ prm.nightBright = 3;
+ prm.animMode = 6;
+ prm.dateMode = 2; // 0:dd/mm/yyyy 1:mm/dd/yyyy 2:yyyy/mm/dd
+ prm.tempCF = false; //Temperature Celsius / Fahrenheit
+ prm.enableTimeDisplay = ENABLE_CLOCK_DISPLAY;
+ prm.dateStart = DATE_START; //Date is displayed start..end
+ prm.dateEnd = DATE_END;
+ prm.tempStart = TEMP_START; //Temperature display start..end
+ prm.tempEnd = TEMP_END;
+ prm.humidStart = HUMID_START; //Humidity% display start..end
+ prm.humidEnd = HUMID_END;
+ prm.dateRepeatMin = DATE_REPEAT_MIN; //show date only every xxx minute. If zero, the date is never displayed!
+ prm.tempRepeatMin = 1;
+ #ifdef DOUBLE_BLINK
+ prm.enableDoubleBlink = true; //both separator points are blinking (6 or 8 tubes VFD clock)
+ #else
+ prm.enableDoubleBlink = false;
+ #endif
+ prm.enableAutoDim = false; //Automatic dimming by luxmeter
+ prm.enableRadar = false; //Radar sensor
+ prm.radarTimeout = 5; //min
+ prm.corrT0 = 0;
+ prm.corrT1 = 0;
+ prm.corrH0 = 0;
+ prm.corrH1 = 0;
+ prm.magic = MAGIC_VALUE; //magic value to check the EEPROM version
+ saveEEPROM();
+ calcTime();
+}
+
+
+// Scheduled/automatic anti-cathode-poisoning animation: every tube counts rapidly through 0-9, then
+// tubes stop one-by-one (in the order controlled by `dir`) once they reach their real target digit
+// value (`tmp[]`, the display content saved at entry). dir<0 stops digits left-to-right (index 0
+// first), dir>0 stops them right-to-left (highest index first), dir==0 is meant to stop them in random
+// order - see the POSSIBLE BUG note below. Called automatically every prm.interval minutes from
+// timeProgram(), and also reachable via the commented-out `newCathodeProtect(5000,0);` line at the end
+// of setup(). Distinct from the simpler, manually-triggered doCathodeProtect() above.
+void newCathodeProtect(unsigned long t,int dir) { //t = time in msec, dir = direction -1,0,1 (0=random)
+ byte tmp[10];
+ boolean tmpDP[10];
+ unsigned long started = millis();
+ boolean finish, stopThis;
+ byte fin[10];
+ byte nextStoppedDigit;
+ int sum = 0;
+
+ cathodeProtRunning = true;
+ DPRINT("Cathode Protect running! dir:"); DPRINTLN(dir);
+ memcpy(tmp,digit,sizeof(tmp));
+ memcpy(tmpDP,digitDP,sizeof(tmpDP));
+ memset(fin,0,sizeof(fin));
+ switch (dir) {
+ case -1:
+ nextStoppedDigit = 0;
+ break;
+ case 0:
+ nextStoppedDigit = random(maxDigits);
+ break;
+ case 1:
+ nextStoppedDigit = maxDigits-1;
+ break;
+ }
+ // POSSIBLE BUG: this line unconditionally overwrites whatever the switch above just computed,
+ // including the `case 0` (dir==0, "random") result - so the FIRST digit to stop is never actually
+ // random, it's always index 0 (dir<=0) or maxDigits-1 (dir>0), identical to the dir<0/dir>0 cases.
+ // The random-order behavior only actually shows up LATER, in the `else if (sum<=maxDigits-2)` branch
+ // below that picks each SUBSEQUENT stop digit randomly - so dir==0 does look random from the second
+ // stopped digit onward, just not on the very first one, which is likely not what was intended.
+ nextStoppedDigit = dir>0 ? 0 : maxDigits-1;
+ while(true) {
+ for (int i=0;i= t);
+ stopThis = (i == nextStoppedDigit);
+
+ if (finish && stopThis) { //this digit stops
+ fin[i] = 1;
+ digitDP[i] = tmpDP[i]; //restore original DP value
+ digit[i] = tmp[i];
+ t +=1000;
+ if (dir>0) { //find next stop digit
+ nextStoppedDigit++;
+ if (nextStoppedDigit>=maxDigits) nextStoppedDigit =0;
+ }
+ else if (dir<0) {
+ nextStoppedDigit--;
+ if (nextStoppedDigit==255) nextStoppedDigit = maxDigits-1;
+ }
+ else if (sum<=maxDigits-2) {
+ while(true) {
+ nextStoppedDigit = random(maxDigits);
+ if (fin[nextStoppedDigit] ==0) break; //found a running digit
+ }
+ }
+ }
+ else if (fin[i]==0) { //get next number
+ if (dir>=0)
+ digit[i] = (digit[i]+1) % 10;
+ else {
+ digit[i]--; if (digit[i] == 255) digit[i] = 9;
+ }
+ digitDP[i] = ((digit[i]%2) == 0);
+ }
+ } //end for
+ //DPRINT("lastStoppedDigit:"); DPRINT(lastStoppedDigit);
+ //DPRINT(" nextStoppedDigit:"); DPRINTLN(nextStoppedDigit);
+
+ writeDisplaySingle();
+ Fdelay(100);
+ sum = 0;
+ for (int i=0;i= maxDigits) //all digits ready
+ break;
+ if ((millis()-started) >= (t+5000)) {
+ DPRINTLN("Safety exit");
+ break;
+ }
+ } //end while
+
+ memcpy(oldDigit, digit, sizeof(oldDigit));
+ lastCathodeProt = minute();
+ cathodeProtRunning = false;
+}
+
+// NOTE: earlier/simpler cathode-protect implementation, superseded by newCathodeProtect() above
+// (kept for reference/comparison - "original version"). Not called from anywhere else in this file;
+// appears to be dead code left in place.
+void cathodeProtect() { //original version
+ int hour12_24 = prm.set12_24 ? (byte)hour() : (byte)hourFormat12();
+ byte hourShow = (byte)hour12_24;
+ byte minShow = (byte)minute();
+ byte secShow = (byte)second();
+ byte dh1 = (hourShow / 10), dh2 = (hourShow % 10);
+ byte dm1 = (minShow / 10), dm2 = (minShow % 10);
+ byte ds1 = (secShow / 10), ds2 = (secShow % 10);
+
+ DPRINTLN("Cathode Protect running!");
+ // All four digits will increment up at 10 Hz.
+ // At T=2 sec, individual digits will stop at
+ // the correct time every second starting from
+ // the right and ending on the left
+ for (int i = 0; i <= 70; i++) {
+ if (i >= 20) ds2 = (secShow % 10);
+ if (i >= 30) ds1 = (secShow / 10);
+ if (i >= 40) dm2 = (minShow % 10);
+ if (i >= 50) dm1 = (minShow / 10);
+ if (i >= 60) dh2 = (hourShow % 10);
+ if (i == 70) dh1 = (hourShow / 10);
+ if (maxDigits >= 8) {
+ digit[7] = dh1;
+ digit[6] = dh2;
+ digit[5] = 11;
+ digit[4] = dm1;
+ digit[3] = dm2;
+ digit[2] = 11;
+ digit[1] = ds1;
+ digit[0] = ds2;
+ }
+ else if (maxDigits == 6) {
+ digit[5] = dh1;
+ digit[4] = dh2;
+ digit[3] = dm1;
+ digit[2] = dm2;
+ digit[1] = ds1;
+ digit[0] = ds2;
+ }
+ else {
+ digit[3] = dh1;
+ digit[2] = dh2;
+ digit[1] = dm1;
+ digit[0] = dm2;
+ }
+ incMod10(dh1); incMod10(dh2);
+ incMod10(dm1); incMod10(dm2);
+ incMod10(ds1); incMod10(ds2);
+ for (int d=0;d=0) newDigit[digPtr] = PLUS_CHARCODE;
+ if (t<0) newDigit[digPtr] = MINUS_CHARCODE;
+ digPtr--;
+ #endif
+ t = abs(t);
+ newDigit[digPtr] = t / 10;
+ if (newDigit[digPtr] == 0) newDigit[digPtr] = 10; //BLANK!!!
+ newDigit[--digPtr] = int(t) % 10;
+ digitDP[digPtr] = true;
+ newDigit[--digPtr] = int(t * 10) % 10;
+ if ((maxDigits > 5) && (GRAD_CHARCODE >= 0)) {
+ digPtr -=1;
+ newDigit[digPtr] = GRAD_CHARCODE; //grad
+ }
+ if (TEMP_CHARCODE >= 0) {
+ if (prm.tempCF) newDigit[--digPtr] = TEMP_CHARCODE+5; // 'F'
+ else newDigit[--digPtr] = TEMP_CHARCODE; // 'C'
+ }
+
+ if (prm.animMode == 0) memcpy(oldDigit, newDigit, sizeof(oldDigit)); //don't do animation
+ colonBlinkState = true;
+ decimalpointON = true;
+}
+#endif
+
+#ifdef DISPLAYHUMID_ONLY_2DIGITS
+void displayHumid(byte ptr) { //special version, 2 digits only
+ float h = humid[ptr];
+ if (ptr==0) h = round(h + prm.corrH0);
+ if (ptr==1) h = round(h + prm.corrH1);
+
+ int digPtr = 3; //tube number: first digit of minutes (hours:5,4 minutes:3,2 seconds: 1,0 )
+ #ifdef HUMID_START_POSITION
+ digPtr = HUMID_START_POSITION;
+ #endif
+ for (int i = 0; i < maxDigits; i++) { //clear display
+ digitDP[i] = false;
+ newDigit[i] = 10;
+ }
+
+ newDigit[digPtr] = h / 10;
+ if (newDigit[digPtr] == 0) newDigit[digPtr] = 10; //BLANK, if zero
+ newDigit[--digPtr] = int(h) % 10;
+
+ if (prm.animMode == 0) memcpy(oldDigit, newDigit, sizeof(oldDigit)); //don't do animation
+ colonBlinkState = false;
+ decimalpointON = false;
+}
+
+#else
+void displayHumid(byte ptr) {
+ int digPtr = maxDigits-1;
+ #ifdef HUMID_START_POSITION
+ digPtr = HUMID_START_POSITION;
+ #endif
+ for (int i = 0; i < maxDigits; i++) {
+ digitDP[i] = false;
+ newDigit[i] = 10;
+ }
+ float h = humid[ptr];
+ if (ptr==0) h += prm.corrH0;
+ if (ptr==1) h += prm.corrH1;
+ #if defined(PLUS_CHARCODE) || defined(MINUS_CHARCODE) //leading sign
+ digPtr--;
+ #endif
+ newDigit[digPtr] = int(h) / 10;
+ if (newDigit[digPtr] == 0) newDigit[digPtr] = 10; //BLANK if zero!!!
+ newDigit[--digPtr] = int(h) % 10;
+ digitDP[digPtr] = true;
+ newDigit[--digPtr] = int(h * 10) % 10;
+ if (maxDigits > 5) {
+ if (digitsOnly) {
+ newDigit[--digPtr] = 10; //empty character
+ newDigit[--digPtr] = PERCENT_CHARCODE; // "%"
+ }
+ else {
+ newDigit[--digPtr] = 10; //empty character
+ newDigit[--digPtr] = UPPER_CIRCLE_CHARCODE; //upper circle = 16
+ newDigit[--digPtr] = LOWER_CIRCLE_CHARCODE; // lower circle = 18
+ }
+ }
+ else { //4 tubes only
+ if (digitsOnly) {
+ newDigit[--digPtr] = PERCENT_CHARCODE; // "%"
+ }
+ else {
+ for (int i = 0; i < maxDigits; i++) {digitDP[i] = false;} //clear any decimal point
+ newDigit[digPtr] = UPPER_CIRCLE_CHARCODE; //upper circle = 16
+ newDigit[--digPtr] = LOWER_CIRCLE_CHARCODE; // lower circle = 18
+ }
+ }
+
+ if (prm.animMode == 0) memcpy(oldDigit, newDigit, sizeof(oldDigit)); //don't do animation
+ colonBlinkState = true;
+ decimalpointON = true;
+}
+#endif
+
+// Renders the current calendar date into newDigit[] according to prm.dateMode (0=dd/mm/yyyy,
+// 1=mm/dd/yyyy, 2=yyyy/mm/dd), truncating the year portion on clocks with fewer than 8 digit
+// positions (>6 digits shows a 2-digit year, <=6 shows none) - the `p[m][i]` lookup table encodes
+// which date component (0=day,1=month,2=year) goes in which position for each mode.
+void displayDate() {
+ byte m = prm.dateMode;
+ if (m>2) m=2;
+ byte p[3][8] =
+ { //tube# 76543210 543210 3210
+ {2,1,0}, //ddmmyyyy ddmmyy ddmm
+ {2,0,1}, //mmddyyyy mmddyy mmdd
+ {0,1,2} //yyyymmdd yymmdd mmdd
+ };
+
+ int t = 0;
+ if (thermometerClock) t = 1;
+ for (int i=0;i<3;i++) {
+ if (p[m][i] == 2) {
+ if (!thermometerClock) {
+ if (maxDigits>4) {
+ newDigit[t++] = year() % 10;
+ newDigit[t++] = (year() % 100) / 10;
+ }
+ if (maxDigits>6) {
+ newDigit[t++] = (year() % 1000) / 100;
+ newDigit[t++] = year() / 1000;
+ }
+ } //!thermometerClock
+ }
+ if (p[m][i] == 1) {
+ newDigit[t++] = month() % 10;
+ newDigit[t++] = month() / 10;
+ }
+ if (p[m][i] == 0) {
+ newDigit[t++] = day() % 10;
+ newDigit[t++] = day() / 10;
+ }
+ } //end for
+ if ((maxDigits>6) && (m<2)) {
+ digitDP[4] = true;
+ digitDP[6] = true;
+ }
+ else {
+ if (thermometerClock) {
+ digitDP[3] = true;
+ newDigit[0] = 10;
+ newDigit[5] = 10;
+ }
+ else {
+ digitDP[2] = true;
+ digitDP[4] = true;
+ }
+ }
+ colonBlinkState = true;
+ if (prm.animMode == 1) memcpy(oldDigit, newDigit, sizeof(oldDigit)); //don't do animation
+}
+
+// displayTime4/6/8() are the top-level "what goes on the display this second" dispatchers, selected by
+// timeProgram() based on maxDigits. Each one first blanks digitDP[], sets the colon-separator DP
+// positions, then dispatches by priority to temperature/humidity/date, falling through to the normal
+// H:M(:S) clock rendering only if none of those "special screens" is currently active AND
+// prm.enableTimeDisplay is set (a thermometer-only build can disable the clock face entirely).
+// showZero controls whether a leading-zero hour (e.g. "07") shows the 0 or blanks it (digit value 10).
+void displayTime4() {
+ for (int i = 0; i < maxDigits; i++) digitDP[i] = false;
+ digitDP[4] = true; digitDP[2] = true;
+ int hour12_24 = prm.set12_24 ? (byte)hour() : (byte)hourFormat12();
+ if (showTemp1) displayTemp(1);
+ else if (showTemp0) displayTemp(0);
+ else if (showHumid1) displayHumid(1);
+ else if (showHumid0) displayHumid(0);
+ else if (showDate && prm.enableTimeDisplay) displayDate();
+ else if (prm.enableTimeDisplay) {
+ showClock = true;
+ newDigit[3] = hour12_24 / 10;
+ if ((!prm.showZero) && (newDigit[3] == 0)) newDigit[3] = 10;
+ newDigit[2] = hour12_24 % 10;
+ newDigit[1] = minute() / 10;
+ newDigit[0] = minute() % 10;
+ if (prm.enableBlink && (second() % 2 == 0)) digitDP[2] = false;
+ }
+}
+
+
+// 6-digit variant of displayTime4() above - has an extra branch for `thermometerClock` builds (see
+// THERMOMETER_CLOCK in clocks.h: first digit reserved for +/- sign, last for C/F, middle 4 for HH:MM)
+// vs the normal 6-tube H:M:S layout.
+void displayTime6() {
+ for (int i = 0; i < maxDigits; i++) digitDP[i] = false;
+ digitDP[4] = true; digitDP[2] = true;
+ int hour12_24 = prm.set12_24 ? (byte)hour() : (byte)hourFormat12();
+ if (showTemp1) displayTemp(1);
+ else if (showTemp0) displayTemp(0);
+ else if (showHumid1) displayHumid(1);
+ else if (showHumid0) displayHumid(0);
+ else if (showDate && prm.enableTimeDisplay) displayDate();
+ else if (prm.enableTimeDisplay) {
+ showClock = true;
+ if (thermometerClock) {
+ newDigit[4] = hour12_24 / 10;
+ if ((!prm.showZero) && (newDigit[4] == 0)) newDigit[4] = 10;
+ newDigit[3] = hour12_24 % 10;
+ newDigit[2] = minute() / 10;
+ newDigit[1] = minute() % 10;
+ newDigit[0] = 10;
+ newDigit[5] = 10;
+ if (prm.enableBlink && (second() % 2 == 0)) digitDP[3] = false;
+ }
+ else { //normal clock
+ newDigit[5] = hour12_24 / 10;
+ if ((!prm.showZero) && (newDigit[5] == 0)) newDigit[5] = 10;
+ newDigit[4] = hour12_24 % 10;
+ newDigit[3] = minute() / 10;
+ newDigit[2] = minute() % 10;
+ if (prm.enableBlink && (second() % 2 == 0)){
+ digitDP[2] = false;
+ if (prm.enableDoubleBlink) {
+ digitDP[4] = false;
+ }
+ }
+ newDigit[1] = second() / 10;
+ newDigit[0] = second() % 10;
+ }
+ }
+}
+
+// 8-digit variant of displayTime4() above - full H:M:S with dash separators (digit value 11, see the
+// BUFSIZE encoding comment near the digit[] declaration) instead of decimal points between the groups.
+void displayTime8() {
+ for (int i = 0; i < maxDigits; i++) {
+ digitDP[i] = false;
+ newDigit[i] = 10;
+ }
+ int hour12_24 = prm.set12_24 ? (byte)hour() : (byte)hourFormat12();
+ if (showTemp1) displayTemp(1);
+ else if (showTemp0) displayTemp(0);
+ else if (showHumid1) displayHumid(1);
+ else if (showHumid0) displayHumid(0);
+ else if (showDate && prm.enableTimeDisplay) displayDate();
+ else if (prm.enableTimeDisplay) {
+ showClock = true;
+ newDigit[8] = 10; //sign digit = BLANK
+ newDigit[7] = hour12_24 / 10;
+ if ((!prm.showZero) && (newDigit[7] == 0)) newDigit[7] = 10;
+ newDigit[6] = hour12_24 % 10;
+ newDigit[5] = 11; //- sign
+ newDigit[4] = minute() / 10;
+ newDigit[3] = minute() % 10;
+ newDigit[2] = 11; //- sign
+ if (prm.enableBlink && (second() % 2 == 0)) {
+ newDigit[2] = 10; //BLANK
+ if (prm.enableDoubleBlink) {
+ newDigit[5] = 10; //BLANK
+ }
+ }
+ newDigit[1] = second() / 10;
+ newDigit[0] = second() % 10;
+ }
+}
+
+
+// The driver-independent animation engine: compares newDigit[] (the target the current displayTimeN()/
+// displayDate()/displayTemp()/displayHumid() call just computed) against oldDigit[] (what was on
+// screen before) and, if anything changed, plays the animation selected by prm.animMode (0-7, see
+// docs/DEVELOPER_GUIDE.md section 4.5):
+// 0 = no animation, instant switch
+// 1 = each changed tube "rolls" through the digits from its old value up to its new one, one tube at a time
+// 2 = all changed tubes spin through 0-9 together, in lockstep
+// 3/4 = whole digit-row slides in from the right/left (with a "----" gap between old and new content)
+// 5 = "wipe" - drives animMask[] with a ramp value per changed tube; interpretation is driver-specific
+// (some multiplex-family drivers get a finer 1..19 ramp, others a coarser 1..5-then-back-down ramp)
+// 6 = picks a random animation from 1-5 on every call
+// 7 = picks a random animation from 1-4 on every call (excludes the wipe animation)
+// Special characters (value >9, e.g. blank/dash/custom glyphs) are always applied immediately outside
+// the animation (never rolled through like a numeric digit change) since there's no sensible
+// "in-between" glyph to animate through. On clocks with >4 digits, the seconds tube (index 0) is
+// excluded from most animations (`j=1`) so a normal ticking seconds digit doesn't animate every second.
+void changeDigit() {
+ int j = 0;
+ byte tmp[50];
+ byte space = 4;
+ byte anim;
+
+ #if defined(TUBE1CLOCK) //no tube animation possible
+ writeDisplaySingle();
+ return;
+ #endif
+ anim = prm.animMode;
+ #if defined(DISABLE_NIGHT_ANIMATION)
+ if (!displayON) anim = 0; //At NIGHT switch off animation
+ #endif
+ #if defined(WORDCLOCK)
+ anim = 0;
+ #endif
+
+ if (anim == 6) anim = 1 + rand() % 5;
+ else if (anim == 7) anim = 1 + rand() % 4;
+
+ if (anim != 5) {
+ for (int i = 0; i < maxDigits; i++)
+ if ((newDigit[i] > 9) || ((oldDigit[i] > 9) && (newDigit[i] <= 9) )) {
+ digit[i] = newDigit[i]; //show special characters ASAP or if special char changes to numbers
+ oldDigit[i] = newDigit[i];
+ }
+ if ((maxDigits > 4) && (newDigit[0] != 0)) j = 1; //if 6 or 8 tube clock, don't play (animate) the seconds digit
+ }
+
+ if (memcmp(newDigit + j, oldDigit + j, maxDigits - j) != 0) {
+ switch (anim) {
+ case 1:
+ for (int tube = j; tube < maxDigits; tube++) {
+ if ((newDigit[tube] != oldDigit[tube]) && (newDigit[tube] <= 9)) {
+ for (int i = oldDigit[tube]; i <= int(newDigit[tube] + 10); i++) {
+ digit[tube] = i % 10;
+ writeDisplaySingle();
+ Fdelay(ANIMSPEED);
+ } //end for i
+ writeDisplaySingle();
+ } //endif
+ } //end for tube
+ break;
+ case 2:
+ for (int i = 0; i <= 9; i++) {
+ for (int tube = j; tube < maxDigits; tube++) {
+ if ((newDigit[tube] != oldDigit[tube]) && (newDigit[tube] <= 9))
+ digit[tube] = (oldDigit[tube] + i) % 10;
+ } //end for tube
+ writeDisplaySingle();
+ Fdelay(ANIMSPEED);
+ } //end for i
+ break;
+ case 3:
+ memcpy(tmp, oldDigit, maxDigits);
+ memcpy(tmp + maxDigits + space, newDigit, maxDigits);
+ memset(tmp + maxDigits, 11, space); //----
+ for (int i = 0; i < maxDigits + space; i++) {
+ for (int tube = 0; tube < maxDigits; tube++) {
+ digit[tube % maxDigits] = tmp[i + tube];
+ } //end for tube
+ writeDisplaySingle();
+ Fdelay(ANIMSPEED);
+ } //end for i
+ break;
+ case 4:
+ memcpy(tmp, newDigit, maxDigits);
+ memcpy(tmp + maxDigits + space, oldDigit, maxDigits);
+ memset(tmp + maxDigits, 11, space); //----
+ for (int i = maxDigits - 1 + space; i >= 0; i--) {
+ for (int tube = 0; tube < maxDigits; tube++) {
+ digit[tube % maxDigits] = tmp[i + tube];
+ } //end for tube
+ writeDisplaySingle();
+ Fdelay(ANIMSPEED);
+ } //end for i
+ break;
+ case 5:
+ // "Wipe" animation - two different ramp shapes depending on which driver is active, since
+ // different segment/multiplex hardware interprets the animMask[] progress value differently:
+ // the SN74141-family/VQC10/HV5122 drivers get a fine 1..19 monotonic ramp (their own
+ // writeDisplay*() decodes finer wipe steps), everything else gets a coarser 1..5-then-5..1
+ // "ramp up then back down" pattern, with digit[] snapped to newDigit[] partway through.
+ memset(animMask, 0, sizeof(animMask));
+#if defined(MULTIPLEX74141) || defined(NO_MULTIPLEX74141) || defined(MULTIPLEX74141_ESP32) || defined(MULE_V2) || defined(PCF_74141) || defined(NO_MULTIPLEX_ESP32) || defined(VQC10) || defined(NEWHV5122)
+ for (int i = 1; i < 20; i++) {
+ for (int tube = j; tube < maxDigits; tube++) {
+ if (oldDigit[tube] != newDigit[tube]) animMask[tube] = i; //digit is changed
+ } //end for tube
+ writeDisplaySingle();
+ Fdelay(ANIMSPEED);
+ } //end for i
+#else
+ for (int i = 1; i <= 5; i++) {
+ for (int tube = j; tube < maxDigits; tube++) {
+ if (oldDigit[tube] != newDigit[tube]) animMask[tube] = i; //digit is changed
+ } //end for tube
+ writeDisplaySingle();
+ Fdelay(ANIMSPEED);
+ } //end for i
+ memcpy(digit, newDigit, sizeof(digit));
+ for (int i = 1; i <= 5; i++) {
+ for (int tube = j; tube < maxDigits; tube++) {
+ if (oldDigit[tube] != newDigit[tube]) animMask[tube] = 6 - i; //digit is changed
+ } //end for tube
+ writeDisplaySingle();
+ Fdelay(ANIMSPEED);
+ } //end for i
+#endif
+ memset(animMask, 0, sizeof(animMask));
+ break;
+ }
+ } //endif memcmp
+
+ memcpy(digit, newDigit, sizeof(digit));
+ memcpy(oldDigit, newDigit, sizeof(oldDigit));
+ writeDisplaySingle();
+}
+
+// Drives ALARMSPEAKER_PIN, but only actually calls digitalWrite() when the requested state differs
+// from what's already set (oldState) - avoids redundant GPIO writes on every alarmSound() tick.
+void writeAlarmPin(boolean newState) {
+ #if ALARMSPEAKER_PIN>=0
+ static boolean oldState = !ALARM_ON;
+ if (oldState != newState) {
+ oldState = newState;
+ //if (newState == ALARM_ON) DPRINTLN("Alarm ON"); else DPRINTLN("Alarm OFF");
+ digitalWrite(ALARMSPEAKER_PIN, newState);
+ }
+ #endif
+}
+
+// Called every loop() iteration. Non-blocking alarm-tone state machine: when the wall-clock time
+// matches prm.alarmHour:prm.alarmMin (checked once, in the first 3 seconds of that minute, to avoid
+// re-triggering all through the minute), it starts a beep pattern defined by the `t[]` timestamp table
+// below (an escalating series of short beeps, in milliseconds relative to alarmStarted) that repeats
+// beyond the table's end as plain 500ms on/off beeping, for prm.alarmPeriod seconds total. Can be
+// silenced early via ALARMBUTTON_PIN if wired.
+void alarmSound(void) {
+ static const unsigned int t[] = {0, 3000, 6000, 6200, 9000, 9200, 9400, 12000, 12200, 12400, 15000, 15200, 15400};
+ static int count = 0;
+ static unsigned long nextEvent;
+ const int cMax = sizeof(t) / sizeof(t[0]); //number of time steps
+
+ if (prm.alarmEnable) {
+ if ( (!alarmON && prm.alarmHour == hour()) && (prm.alarmMin == minute()) && (second() <= 3)) { //switch ON alarm sound
+ DPRINTLN("Alarm started!");
+ alarmON = true;
+ alarmStarted = millis();
+ count = 0;
+ nextEvent = alarmStarted - 1;
+ }
+ }
+ else {
+ alarmON = false;
+ }
+
+ if (!alarmON) {
+ writeAlarmPin(!ALARM_ON);
+ return; //nothing to do
+ }
+
+ if ((millis() - alarmStarted) > 1000 * (long)prm.alarmPeriod) {
+ alarmON = false; //alarm period is over
+ darkenNeopixels();
+ DPRINTLN("Alarm ended.");
+ }
+
+ if (ALARMBUTTON_PIN >= 0) { //is button installed?
+ if (digitalRead(ALARMBUTTON_PIN) == LOW) { //stop alarm
+ DPRINTLN("Alarm stopped by button press.");
+ alarmON = false;
+ }
+ }
+
+ if (!prm.alarmEnable || !alarmON) { //no alarm, switch off
+ writeAlarmPin(!ALARM_ON);
+ return;
+ }
+
+ //-------- Generate sound --------------------------------
+
+ if (millis() > nextEvent) { //go to the next event
+ if (count % 2 == 0) {
+ nextEvent += 500;
+ writeAlarmPin(ALARM_ON);
+ //DPRINT(" Sound ON"); DPRINT(" Next:"); DPRINTLN(nextEvent);
+ }
+ else {
+ writeAlarmPin(!ALARM_ON);
+ nextEvent = (count / 2 < cMax) ? alarmStarted + t[count / 2] : nextEvent + 500;
+ //DPRINT(" OFF"); DPRINT(" Next:"); DPRINTLN(nextEvent);
+ }
+ count++;
+ }
+}
+
+
+// Called once per second from timeProgram(). Decides displayON (day/night brightness state) from the
+// prm.dayHour:dayMin / prm.nightHour:nightMin schedule, handling the case where the night window wraps
+// past midnight (mn_off > mn_on, e.g. night starts 22:00 and ends 07:00) vs. the (unusual) case where
+// it doesn't. manualOverride (set by the web UI's "manualOverride" key in handleConfigChanged()) lets
+// the user force displayON one way regardless of the schedule; that override is automatically cleared
+// again the moment the schedule's own computed state (prevShutoffState) naturally transitions to match
+// what the override had forced - i.e. a manual override lasts until the next scheduled day/night
+// boundary, not forever.
+void evalShutoffTime(void) { // Determine whether tubes should be turned to NIGHT mode
+
+ if (!prm.enableAutoShutoff) return;
+
+ int mn = 60 * hour() + minute();
+ int mn_on = prm.dayHour * 60 + prm.dayMin;
+ int mn_off = prm.nightHour * 60 + prm.nightMin;
+
+ static bool prevShutoffState = true;
+ if ( (((mn_off < mn_on) && (mn >= mn_off) && (mn < mn_on))) || // Nighttime
+ ((mn_off > mn_on) && ((mn >= mn_off) || (mn < mn_on)))) {
+ if (!manualOverride) displayON = false;
+ if (prevShutoffState == true) manualOverride = false;
+ prevShutoffState = false;
+ }
+ else { // Tubes should be on
+ if (!manualOverride) displayON = true;
+ if (prevShutoffState == false) manualOverride = false;
+ prevShutoffState = true;
+ }
+ return;
+ // POSSIBLE BUG / NOTE: unconditional `return` above makes the debug DPRINT block below permanently
+ // unreachable dead code - harmless (it only ever fed DPRINT, a no-op without DEBUG), but the
+ // early-return means these diagnostics can never be re-enabled just by turning DEBUG on; the `return`
+ // itself would also need removing.
+ DPRINT("mn="); DPRINT(mn);
+ DPRINT(" mn_on="); DPRINT(mn_on);
+ DPRINT(" mn_off="); DPRINT(mn_off);
+ DPRINT(" manOverride:"); DPRINT(manualOverride);
+ DPRINT(" displayON:"); DPRINTLN(displayON);
+ DPRINT(" enableBlink:"); DPRINTLN(prm.enableBlink);
+ DPRINT(" blinkState:"); DPRINTLN(colonBlinkState);
+}
+
+
+// Renders a single IP-address octet (0-255) as a 3-digit number, optionally preceded by "IP" glyphs
+// on displays with an alphabetic character set and >=6 digit positions, and shows it once via
+// changeDigit(). Called 4 times in a row by showMyIp() below, once per octet.
+void writeIpTag(byte iptag) {
+
+#ifdef SHIFT_TUBES_LEFT_BY_1
+ byte offset = 1;
+#else
+ byte offset = 0;
+#endif
+
+ memset(newDigit, 10, sizeof(newDigit));
+ if (!digitsOnly && (maxDigits >= 6)) {
+ newDigit[5] = 19; //'I'
+ newDigit[4] = 14; //'P'
+ }
+ //if (iptag >= 100)
+ newDigit[2 + offset] = iptag / 100;
+ newDigit[1 + offset] = (iptag % 100) / 10;
+ newDigit[0 + offset] = iptag % 10;
+ memcpy(digit,newDigit,sizeof(digit));
+ printDigits(0);
+ changeDigit();
+}
+
+// Called once at the end of setup(): displays the clock's IP address on the tubes, one octet at a
+// time (each shown for SPEED=1500ms via writeIpTag()), so the user knows what address to browse to
+// for the web admin UI without needing a router's client list or a phone app.
+void showMyIp(void) { //at startup, show the web page internet address
+ clearDigits();
+ #define SPEED 1500
+ for (int i=0;i<4;i++) {
+ ipShowRunning = true;
+ writeIpTag(ip[i]);
+ Fdelay(SPEED);
+ }
+ ipShowRunning = false;
+}
+
+
+// NOTE: converts a "number of 15-minute steps since midnight" value into hours+minutes - not called
+// anywhere in this file or elsewhere in the project; dead/unused helper.
+void fifteenMinToHM(int &hours, int &minutes, int fifteenMin)
+{
+ hours = fifteenMin / 4;
+ minutes = (fifteenMin % 4) * 15;
+}
+
+// Called from loop() whenever prm.wifiMode is true but WiFi.status()!=WL_CONNECTED. Self-throttled to
+// try at most once every 10 minutes: first attempts a lightweight WiFi.disconnect()+WiFi.reconnect()
+// (reuses previously stored credentials), and only falls back to the heavier startWifiMode() (which
+// can itself open the wifiManager() config portal) if that quick reconnect didn't succeed.
+void resetWiFi(void) {
+ static unsigned long lastTest = millis();
+ static unsigned int counter = 0;
+
+ if ((millis() - lastTest) < 600000) return; //check in every 10 min
+ lastTest = millis();
+ DPRINTLN("Lost WiFi. Reconnect.");
+ //#if defined(ESP8266)
+ WiFi.disconnect();
+ WiFi.reconnect();
+ //#endif
+ if (WiFi.status() == WL_CONNECTED) return;
+
+ //findBestWifi();
+ startWifiMode();
+ return;
+ // POSSIBLE BUG / NOTE: unconditional `return` above makes everything below permanently unreachable -
+ // including a 3-hour "give up and force a full reboot" counter/message. As written, resetWiFi() will
+ // retry forever (every 10 minutes) without ever calling doReset(), no matter how long WiFi stays
+ // down. Likely intentional (a full reboot loop might have been judged riskier than an endless retry),
+ // but flagged as dead code worth reviewing - see docs/CODE_REVIEW.md.
+ counter++;
+ DPRINT("Wifi lost! Minutes to restart:"); DPRINTLN(180 - counter);
+ if (counter < 180) return; //3 hours passed
+
+ DPRINTLN("Restart Clock...");
+ delay(1000);
+ doReset();
+}
+
+// NOTE: proper (always-non-negative) modulo helper - not called anywhere in this file or elsewhere in
+// the project; dead/unused helper.
+inline int mod(int a, int b) {
+ int r = a % b;
+ return r < 0 ? r + b : r;
+}
+
+
+// Power-on self-test animation: sweeps every tube through 0-9 in unison (called once from setup()).
+void testTubes(int dely) {
+ Fdelay(dely);
+ cathodeProtRunning = true;
+ DPRINT("Testing tubes: ");
+ for (int i = 0; i < 10; i++) {
+ DPRINT(i); DPRINT(" ");
+ for (int j = 0; j < maxDigits; j++) {
+ newDigit[j] = i;
+ // POSSIBLE BUG: likely meant to be `digit[j] = i;` (j is the tube-index loop variable here; i is
+ // the 0..9 character being swept through). As written this indexes digit[] by the CHARACTER
+ // value i (0..9) instead of the tube position j, so it writes into at most the first 10 entries
+ // of digit[] regardless of which tube is being iterated - harmless in practice only because
+ // changeDigit() (called right below, every outer-loop iteration) immediately overwrites the
+ // whole digit[] buffer from newDigit[] anyway, so this line's result is never actually rendered.
+ digit[i] = i;
+ digitDP[j] = i % 2;
+ }
+ changeDigit();
+ Fdelay(dely);
+ }
+ DPRINTLN(" ");
+ Fdelay(1000);
+ memset(digitDP, 0, sizeof(digitDP));
+ cathodeProtRunning = false;
+}
+
+// NOTE: cycles a rotating "walking digit" pattern across the tubes - not called from setup()/loop() or
+// anywhere else in this file; dead/unused (a leftover manual test helper, presumably invoked ad hoc
+// during development).
+void playTubes() {
+ static byte tube = 0;
+ memset(digitDP, 0, sizeof(digitDP));
+ for (int j = 0; j < maxDigits; j++) {
+ newDigit[j] = tube++ % maxDigits;
+ }
+ changeDigit();
+}
+
+// Debug helper: dumps all temperature/humidity sensor readings to the serial console, throttled to
+// once every 30s. Called from printDigits() below (itself a no-op unless DEBUG is defined).
+void printSensors(void) {
+ static unsigned long lastRun = millis();
+
+ if ((millis() - lastRun) < 30000) return;
+ lastRun = millis();
+ if (useTemp>0) {
+ DPRINT("Temperature ("); DPRINT(useTemp); DPRINT("): ");
+ for (int i=0;i0) {
+ DPRINT("Humidity ("); DPRINT(useHumid); DPRINT("): ");
+ for (int i=0;i= 0; i--) {printChar(digit[i]);} DPRINT("]");
+ DPRINT(colonBlinkState ? " * " : " ");
+ //DPRINT("\noldDigit: "); for (int i = maxDigits - 1; i >= 0; i--) {printChar(oldDigit[i]);}
+ //DPRINT("\nnewDigit: "); for (int i = maxDigits - 1; i >= 0; i--) {printChar(newDigit[i]);}
+
+ /*
+ if ((millis()/1000%10) == 1) { //show free memory for debugging memory leak
+ DPRINT("Heap:"); DPRINT(ESP.getFreeHeap()); DPRINT(" byte");
+ #if defined(ESP8266)
+ DPRINT(" Fragmentation:"); DPRINT(100 - ESP.getMaxFreeBlockSize() * 100.0 / ESP.getFreeHeap()); DPRINT("%");
+ #endif
+ }
+ */
+ //DPRINT("INT:"); DPRINT(intCounter); intCounter = 0; //show multiplex interrupt counter
+ //DPRINT(" ESaving:"); DPRINT(EEPROMsaving);
+ if (useLux>0) {
+ DPRINT(" Lux:"); DPRINT(lx);
+ }
+ if (RADAR_PIN>=0) {
+ //DPRINT(" RadarPin:"); DPRINT(digitalRead(RADAR_PIN)); DPRINT(" lastOn:"); DPRINT(radarLastOn); DPRINT(" sec:"); DPRINTLN((millis()-radarLastOn)/1000);
+ }
+ //DPRINT(" tON:"); DPRINT(timerON); DPRINT(" tOFF:"); DPRINT(timerOFF); //Multiplex timing values for testing
+ if (WiFi.status() != WL_CONNECTED) DPRINT(" no WIFI");
+ DPRINTLN(" ");
+ printSensors();
+ #endif
+}
+
+
+// Called every loop() iteration (self-throttled to 500ms). Drives TUBE_POWER_PIN (filament/HV supply
+// on/off) based on radar-detected presence: motion resets a timeout (radarLastOn), and if no motion is
+// seen for prm.radarTimeout minutes the tubes' power supply is switched off entirely (a harder cutoff
+// than just dimming brightness). The radar signal can come from a LOCAL RADAR_PIN sensor (if wired -
+// takes priority) or from mqttRadarON (a remote/MQTT-published radar state, see mqtt.ino's master/
+// slave radar sharing). If prm.enableRadar is off, radarON is forced true (always powered) regardless
+// of any radar input.
+void checkTubePowerOnOff(void) {
+ static unsigned long lastRun = 0;
+
+ if ((millis()-lastRun)<500) return;
+ lastRun = millis();
+
+ boolean newP = mqttRadarON;
+ if (RADAR_PIN >=0) { //if local RADAR is defined, it has priority
+ newP = digitalRead(RADAR_PIN);
+ }
+ if (!radarON && newP) { //check, is switching ON?
+ DPRINTLN("RADAR: Switching ON tubes.");
+ }
+ if (newP) { //restart timeout
+ radarLastOn = millis();
+ radarON = true;
+ }
+ if (radarON && (millis()-radarLastOn)>60000ul*uint32_t(prm.radarTimeout)) { //check if should switch off?
+ radarON = false;
+ DPRINTLN("RADAR: Switching OFF tubes.");
+ }
+ //DPRINT("RadarON:"); DPRINT(radarON); DPRINT(" timeout:"); DPRINTLN((millis()-radarLastOn));
+
+ if (!prm.enableRadar) radarON = true; //If Radar is disabled, always ON !
+ #if TUBE_POWER_PIN >=0
+ if (((displayON ? prm.dayBright : prm.nightBright) == 0) || !radarON) {
+ digitalWrite(TUBE_POWER_PIN,!TUBE_POWER_ON); //Switch OFF
+ //DPRINTLN("POWER OFF");
+ }
+ else {
+ digitalWrite(TUBE_POWER_PIN,TUBE_POWER_ON); //Switch ON
+ //DPRINTLN("POWER ON");
+ }
+ #endif
+}
+
+
+//Calculation parameters are defined in clocks.h
+//https://www.pcboard.ca/ldr-light-dependent-resistor
+
+// Converts a raw ADC reading from an LDR (light-dependent resistor) voltage divider into an
+// approximate lux value, using the LUX_CALC_SCALAR/LUX_CALC_EXPONENT calibration constants from
+// clocks.h (LDR characteristic curve) and a simple exponential smoothing filter (oldLux) to avoid
+// jumpy brightness changes from ADC noise. Returns 0 (a build with no LIGHT_SENSOR_PIN configured)
+// when there's no LDR wired at all - see getLightSensor() below for the BH1750 (digital lux sensor)
+// alternative, which takes priority over this analog LDR path when both are present.
+int luxMeter(void) {
+#if LIGHT_SENSOR_PIN >=0
+ static float oldLux = MAXIMUM_LUX;
+ float ADCdata = analogRead(LIGHT_SENSOR_PIN); //DPRINT("ADC:"); DPRINTLN(ADCdata);
+ float ldrResistance = (MAX_ADC_READING - ADCdata) / ADCdata * REF_RESISTANCE;
+ float ldrLux = LUX_CALC_SCALAR * pow(ldrResistance, LUX_CALC_EXPONENT);
+ if ((ldrLux>=MAXIMUM_LUX)||(ldrLux <0)) ldrLux = MAXIMUM_LUX; //Limit lux value to maximum
+ oldLux = oldLux + (ldrLux-oldLux)/10; //slow down Lux change
+ return (int)oldLux;
+#else
+ return (0);
+#endif
+}
+
+// Called from loop()/Fdelay() (self-throttled to 500ms) to refresh the global `lx` (ambient lux) and
+// `autoBrightness` (whether auto-dim is actually active) values. Prefers a BH1750 digital sensor if
+// present, falls back to an analog LDR (luxMeter()) if not, and if neither exists forces a fixed
+// MAXIMUM_LUX reading with autoBrightness permanently off (i.e. "auto-dim" silently has no hardware to
+// act on). autoBrightness itself is only ever actually true when prm.enableAutoDim is also set by the
+// user - this function just gates it on hardware presence.
+void getLightSensor(void) {
+ static unsigned long lastRun = 0;
+ static int oldLx = 0;
+ int tmp;
+ if ((millis()-lastRun)<500) return;
+ lastRun = millis();
+
+ if (BH1750exist) {
+ tmp = getBH1750();
+ if ((abs(tmp-oldLx)>1) || (tmp >= MAXIMUM_LUX)) {
+ lx=tmp; oldLx = lx;
+ }
+ if (lx>=MAXIMUM_LUX-2) lx = MAXIMUM_LUX;
+ autoBrightness = prm.enableAutoDim;
+ }
+ else if (LDRexist) {
+ tmp = luxMeter();
+ if ((abs(tmp-oldLx)>1) || (tmp >= MAXIMUM_LUX)) {
+ lx=tmp; oldLx = lx;
+ }
+ if (lx>=MAXIMUM_LUX-2) lx = MAXIMUM_LUX;
+ autoBrightness = prm.enableAutoDim;
+ }
+ else {
+ lx = MAXIMUM_LUX;
+ autoBrightness = false;
+ }
+}
+
+// NOTE: intended to let the web UI's "wifiMode" (STA vs AP) toggle switch the radio mode live, without
+// a reboot - would re-run startNewWifiMode()/startStandaloneMode() whenever prm.wifiMode changes. Its
+// only call site in loop() is commented out (`//checkWifiMode();`), so this is currently dead code;
+// changing prm.wifiMode from the web UI today only takes effect after the next reboot/doReset().
+void checkWifiMode() {
+static boolean oldMode = prm.wifiMode;
+
+ if (oldMode != prm.wifiMode) {
+ if (prm.wifiMode) {
+ startNewWifiMode();
+ }
+ else {
+ startStandaloneMode();
+ }
+ oldMode = prm.wifiMode;
+ }
+}
+
+// Debug helper: logs to serial whenever any byte of `prm` differs from the last time this was called -
+// a crude way to spot unexpected/unintended settings changes during development. Its only call site in
+// loop() is commented out (`//checkPrm();`), so it is currently dead code / not part of the live build.
+void checkPrm() {
+ static byte oldPrm[sizeof(prm)];
+ int tmp = memcmp(oldPrm,&prm,sizeof(prm));
+ if (tmp!=0){
+ DPRINT("Prm mismatch!"); DPRINTLN(tmp);
+ memcpy(oldPrm,&prm,sizeof(prm));
+ }
+}
+
+
+// Main loop. Deliberately delay()-free (see docs/DEVELOPER_GUIDE.md section 4.2 and the Fdelay()
+// comment above) - every subsystem gets serviced once per iteration: cooperative display refresh
+// (writeDisplay2(), only does anything for non-ISR/"smart chip" drivers), DNS captive-portal service
+// (AP mode only), the clock/sensor/display tick (timeProgram(), itself throttled to ~200ms), RGB/PWM
+// backlight animations, the alarm buzzer, radar-driven tube power, the light sensor, MQTT publish (STA
+// mode + WiFi connected) or WiFi reconnect attempts (STA mode + WiFi down), the RTC manual-editor
+// button scan, and finally any pending OTA-update/cathode-protect request queued by the web server
+// (makeFirmwareUpdate/makeCathodeProtect flags, set from server route handlers which must return
+// quickly and can't run these long operations inline).
+void loop(void) {
+ //checkPrm();
+ writeDisplay2();
+ if (WiFi.status() != WL_CONNECTED)
+ dnsServer.processNextRequest();
+ timeProgram();
+ //writeDisplaySingle();
+ writeDisplay2();
+ doAnimationMakuna();
+ doAnimationPWM();
+ alarmSound();
+ checkTubePowerOnOff();
+ getLightSensor();
+ //checkWifiMode();
+ if (prm.wifiMode) { //WiFi Clock Mode
+ if (WiFi.status() == WL_CONNECTED) {
+ if (prm.mqttEnable) mqttSend();
+ }
+ else {
+ resetWiFi();
+ }
+ }
+ editor();
+ if (makeFirmwareUpdate) {
+ makeFirmwareUpdate = false;
+ doFirmwareUpdate();
+ }
+ if (makeCathodeProtect) {
+ makeCathodeProtect = false;
+ doCathodeProtect();
+ }
+ yield();
+}
+
+// Clean reboot: drops the WiFi radio (and, on ESP8266, its TX power) before calling ESP.restart(),
+// which avoids leaving the radio associated to the AP mid-reset. Called after a successful OTA update,
+// from the web admin's POST /reset and /factoryreset routes, and (dead-code path only, see the
+// resetWiFi() NOTE above) from the unreachable end of resetWiFi().
+void doReset(void) {
+ DPRINTLN("Restart Clock...");
+ #if defined(ESP8266)
+ WiFi.setOutputPower(0);
+ #endif
+
+ WiFi.disconnect();
+ delay(1000);
+ ESP.restart();
+}
+
+// Default empty stand-in for writeDisplay2() - the cooperative (non-ISR) "give the display a chance to
+// refresh" hook called from loop()/Fdelay(). Only VQC10/TUBE1CLOCK/WORDCLOCK actually define a real
+// writeDisplay2() (in their own driver .ino files) because their hardware needs cooperative,
+// non-interrupt-safe timing (e.g. delayMicroseconds() during row scanning) that a hardware-timer ISR
+// can't safely provide; every other driver relies purely on the ISR-based writeDisplay(), so this
+// no-op keeps the call sites unconditional without needing per-driver #ifdef guards everywhere.
+#if defined(VQC10) || defined(TUBE1CLOCK) || defined(WORDCLOCK)
+#else
+ void writeDisplay2(void){} //not interrupt driven display handler
+#endif
diff --git a/firmware/uniclock/ESP32_UniClock2/HV5122.ino b/firmware/uniclock/ESP32_UniClock2/HV5122.ino
new file mode 100644
index 0000000..9daa4b3
--- /dev/null
+++ b/firmware/uniclock/ESP32_UniClock2/HV5122.ino
@@ -0,0 +1,303 @@
+//============================================================================
+// HV5122 Nixie tube driver (LEGACY / ORIGINAL VERSION)
+//----------------------------------------------------------------------------
+// Chip: Microchip/Supertex HV5122 - 32-channel high-voltage (up to ~170V)
+// serial-in shift register / latch, used here to drive Nixie tube
+// cathodes directly (no external cathode-driver transistors needed).
+// Wiring: Two HV5122 chips are cascaded (bit-banged, SPI-like, 3-wire bus):
+// PIN_DIN -> chip0 DIN, chip0 DOUT -> chip1 DIN (daisy-chained),
+// PIN_CLK -> both chips' CLK (shared),
+// PIN_OE -> both chips' output-enable (shared, active HIGH here
+// i.e. energizes/blanks the tubes - see writeDisplay()).
+// Together the two chips give 64 open-drain HV outputs, addressed
+// via the digitPins[][] table (pin numbers 1-32 = chip0, 101-132 =
+// chip1; see clocks.h for the actual per-clock wiring tables).
+// Status: THIS IS THE OLDER/LEGACY DRIVER. As of the current clocks.h, no
+// active CLOCK_NN config selects plain "HV5122" anymore - every
+// HV5122-based config (CLOCK_50, 51, 52, 53, 54, 55, 56, 57, 57A)
+// now defines NEWHV5122 and is compiled from HV5122new.ino instead.
+// This file is effectively unused/orphaned code, kept for
+// reference. It lacks the crossfade/wipe digit-change animation
+// (animMask/newDigit/oldDigit) support that HV5122new.ino adds -
+// see HV5122new.ino's header comment for details.
+//============================================================================
+#ifdef HV5122 //Nixie driver
+//P.S. 6x tubes PCB version
+
+char tubeDriver[] = "HV5122";
+//HV5122 control pins
+//#define PIN_DIN 17 // DataIn - chip0 DOUT pin is connected to chip1 DIN pin!
+//#define PIN_CLK 22 // Clock
+//#define PIN_OE 21 // OutputEnable
+
+//int maxDigits = 6;
+
+//Data pin numbers 100+ means: chip1 pins are used. Chip0-s DOUT is connected to chip1's DIN
+/* Example:
+ byte digitPins[maxDigits+1][10] = {
+ {2,10,9,8,7,6,5,4,3,1}, //sec 1 , chip0 (tube#0)
+ {11,32,20,19,18,17,16,15,14,13}, //sec 10 , chip0 (tube#1)
+ {22,31,29,30,27,28,25,26,23,24}, //min 1 , chip0 (tube#2)
+ {101,131,110,109,108,107,106,105,104,103}, //min 10 , chip1 (tube#3)
+ {111,132,120,119,118,117,116,115,114,113}, //hour 1 , chip1 (tube#4)
+ {122,129,130,127,128,125,126,123,124,121}, //hour 10 , chip1 (tube#5)
+ {0,12,21,102,112,0, 0,0,0,0} //extra decimal point (tube0...tube6)
+ };
+*/
+
+#define SHIFT_LSB_FIRST false //true= LSB first, false= MSB first
+int PWMrefresh = 10000; //Brightness PWM period (microseconds-ish timer ticks). Greater value => slower brightness PWM frequency
+//int PWMtiming[11] = {0,500,800,1200,2000,2500,3000,4500,6000,8000,10000};
+int PWM_min = 500; //shortest allowed ON time (floor, avoids a 0-length timer alarm)
+int PWM_max = 10000; //longest ON time = full brightness (== PWMrefresh, i.e. tube stays on the whole period)
+
+void setup_pins() {
+ DPRINTLN("Setup pins - HV5122 Nixie driver...");
+ DPRINT("- CLK : GPIO"); DPRINTLN(PIN_CLK);
+ DPRINT("- DATAIN: GPIO"); DPRINTLN(PIN_DIN);
+ DPRINT("- OUTPUT_ENABLE: GPIO"); DPRINTLN(PIN_OE);
+ pinMode(PIN_CLK, OUTPUT); regPin(PIN_CLK, "PIN_CLK");
+ pinMode(PIN_DIN, OUTPUT); regPin(PIN_DIN, "PIN_DIN");
+ pinMode(PIN_OE, OUTPUT); regPin(PIN_OE, "PIN_OE");
+ digitalWrite(PIN_CLK, HIGH);
+ digitalWrite(PIN_OE, LOW); //keep outputs blanked until clearTubes() below has pushed a known (all-zero) pattern into the shift registers
+ driverSetupStr = " HV5122 pin settings: ";
+ for (int i=0;i");
+ }
+ clearTubes();
+ startTimer();
+}
+
+
+// writeDisplay(): TIMER-ISR. Unlike most other drivers in this project, this
+// driver does NOT multiplex individual tube positions from here - the full
+// digit pattern for ALL tubes was already shifted out into the HV5122 latches
+// by writeDisplaySingle() (called from the main loop). This ISR only toggles
+// the shared PIN_OE line on/off to implement brightness as a simple
+// square-wave PWM (whole display blinks ON/OFF at a variable duty cycle -
+// there is no per-tube time-slicing here since HV5122 has a real static
+// latch per output, not a multiplexed matrix).
+#if defined(ESP32)
+void IRAM_ATTR writeDisplay() { //void IRAM_ATTR writeDisplay(){
+#else
+void ICACHE_RAM_ATTR writeDisplay() { //https://circuits4you.com/2018/01/02/esp8266-timer-ticker-example/
+#endif
+ static volatile int timer = PWMrefresh;
+ static volatile boolean state=true; //true = about to enter/currently in the ON phase of the PWM cycle
+ static volatile int brightness;
+ static int PWMtimeBrightness=PWM_min;
+
+ intCounter++;
+ if (EEPROMsaving) { //stop refresh, while EEPROM write is in progress!
+ #if defined(ESP8266)
+ timer1_write(PWMrefresh);
+ #endif
+ return;
+ }
+
+ #if defined(ESP32)
+ portENTER_CRITICAL_ISR(&timerMux);
+ noInterrupts();
+ #endif
+
+ brightness = displayON ? prm.dayBright : prm.nightBright;
+ if (brightness>MAXBRIGHTNESS) brightness = MAXBRIGHTNESS; //only for safety
+
+ if (autoBrightness && displayON) {
+ PWMtimeBrightness = max(PWM_min,PWM_max*lx/MAXIMUM_LUX); //day mode + ALS: scale ON-time by measured lux instead of the stored brightness setting
+ }
+ else
+ PWMtimeBrightness = max(PWM_min,PWM_max*brightness/MAXBRIGHTNESS);
+
+ //At full brightness, force a steady ON state every call instead of letting the
+ //ON/OFF alternation run its course - avoids a needless (and here pointless,
+ //since ON-time==PWMrefresh anyway) OFF sliver caused by scheduling/rounding jitter.
+ if ((!autoBrightness) && (brightness==MAXBRIGHTNESS))
+ state = true;
+
+ if (state) { //ON state
+ timer = PWMtimeBrightness;
+ timerON = timer;
+ }
+ else { //OFF state
+ timer = PWMrefresh-PWMtimeBrightness;
+ timerOFF = timer;
+ }
+ if (timer<500) timer = 500; //safety only... (timerAlarmWrite/timer1_write with 0 or a very small value can hang/misfire)
+
+ if ( (brightness == 0) || (!state) || (!radarON)) { //OFF state, blank digit
+ digitalWrite(PIN_OE, LOW); //OFF -- NOTE: on this board OE is active-HIGH (HIGH=tubes lit), opposite of the active-low /OE found on many shift-register ICs
+ }
+ else { //ON state
+ digitalWrite(PIN_OE, HIGH); //ON
+ }
+
+ state = !state; //flip for next ISR call (alternate ON phase / OFF phase); irrelevant while the full-brightness override above forces true every time
+
+#if defined(ESP8266)
+ timer1_write(timer);
+#elif defined(ESP32)
+ portEXIT_CRITICAL_ISR(&timerMux);
+ //ESP32timer = timerBegin(0, PRESCALER, true); //set prescaler, true = edge generated signal
+ //timerAttachInterrupt(ESP32timer, &writeDisplay, true);
+ timerAlarmWrite(ESP32timer, timer, true);
+ timerAlarmEnable(ESP32timer);
+ interrupts();
+#endif
+}
+
+
+
+// shift(): bit-bangs one 32-bit word into the HV5122 shift-register chain.
+// The ~20-NOP delays around each CLK/DIN edge are required because the
+// HV5122 is a high-voltage part (internal level-shifters to the ~170V rail)
+// and is much slower than a typical 5V logic shift register - driving it at
+// full CPU speed without these delays can corrupt shifted-in data.
+// Data is latched into the HV5122 on the falling edge of CLK.
+#if defined(ESP32)
+void IRAM_ATTR shift(uint32_t Data) {
+#else
+void ICACHE_RAM_ATTR shift(uint32_t Data) {
+#endif
+ static boolean b;
+
+ for (uint32_t i = 0; i < 32; i++) {
+ digitalWrite(PIN_CLK, HIGH);
+ for (int t=0;t<20;t++) asm volatile ("nop");
+ if (SHIFT_LSB_FIRST)
+ b = ((Data & (uint32_t(1) << i))) > 0; //LSB first
+ else
+ b = (Data & (uint32_t(1) << (31 - i))) > 0; //MSB first
+ digitalWrite(PIN_DIN, b);
+ for (int t=0;t<20;t++) asm volatile ("nop");
+ digitalWrite(PIN_CLK, LOW); //falling CLK to store DIN
+ for (int t=0;t<20;t++) asm volatile ("nop");
+ }
+ digitalWrite(PIN_CLK, HIGH);
+ for (int t=0;t<20;t++) asm volatile ("nop");
+}
+
+void clearTubes() {
+ shift(0);
+ shift(0);
+}
+
+// writeDisplaySingle(): builds the two 32-bit bit-buffers (one per cascaded
+// HV5122 chip) from the current digit[]/digitDP[] state and shifts them out.
+// Called repeatedly from the main loop (not from the ISR) and self-throttled
+// to run at most every 50ms, since the bit-bang shift() above is relatively
+// slow (32 bits x ~60 NOPs, twice) and re-shifting on every idle loop
+// iteration would be wasteful. NOTE: unlike HV5122new.ino, this version does
+// NOT check whether anything actually changed since the last call - it always
+// re-shifts full data every 50ms regardless, and it ignores newDigit[]/
+// oldDigit[]/animMask[] entirely, so it has no digit-change wipe/crossfade
+// animation support.
+void writeDisplaySingle() {
+ static unsigned long lastRun = 0;
+ uint32_t bitBuffer0 = 0;
+ uint32_t bitBuffer1 = 0;
+ byte num;
+
+ if ((millis() - lastRun) < 50) return; //slow down!!!
+ lastRun = millis();
+
+ bitBuffer0 = 0; bitBuffer1 = 0;
+ for (int i = 0; i < maxDigits; i++) { //Set the clock digits
+ num = digit[i];
+ if (num < 10) { //codes 10 (BLANK), 11 (dash), 12 (dp glyph) and 13+ (extra glyphs) have no cathode mapping on a Nixie tube -> leave bits cleared = tube blank
+ if (digitPins[i][num] < 100) {
+ bitBuffer0 |= (uint32_t)(1 << (digitPins[i][num]-1));
+ //DPRINT(digitPins[i][num]); DPRINT(":");
+ } //chip0
+ else {
+ bitBuffer1 |= (uint32_t)(1 << (digitPins[i][num] - 101));
+ //DPRINT(digitPins[i][num]);DPRINT(":");} //chip1
+ }
+ }
+ } //end for i
+
+ #ifdef MAKE_BLINKING_DOTS //it means, the extra datapins are used as 4 blinking dot instead of decimal points!
+ if (showClock) {
+ digitDP[1] = digitDP[2]; //left two dots
+ digitDP[3] = digitDP[4]; //right two dots
+ }
+ #endif
+
+ for (int i = 0; i < maxDigits-1; i++) { //Set the extra decimal point dots
+ if (digitDP[i] && digitPins[maxDigits][i]>0) {
+ if (digitPins[maxDigits][i] < 100)
+ bitBuffer0 |= (uint32_t)(1 << (digitPins[maxDigits][i]-1)); //chip0
+ else
+ bitBuffer1 |= (uint32_t)(1 << (digitPins[maxDigits][i] - 101)); //chip1
+ }
+ } //end for i
+
+ //bitBuffer0 = 1<32) cnt = 0; DPRINTLN(cnt);
+ //Shift chip1 (the far end of the DIN->DOUT daisy-chain) FIRST, then chip0 LAST:
+ //bits shifted in earliest travel furthest down the chain, so chip1's data must
+ //go in before chip0's, otherwise chip0's own data would get pushed on into chip1.
+ shift(bitBuffer1);
+ //delayMicroseconds(1); //for testing on oscilloscope
+ shift(bitBuffer0);
+ //showBits(bitBuffer1); showBits(bitBuffer0); DPRINTLN(" ");
+}
+
+//For testing -------------------
+/*
+void test() {
+ digitalWrite(PIN_OE, HIGH);
+ clearTubes();
+ for (int j=0;j<2;j++)
+ for (uint32_t i=0;i<32;i++) {
+ DPRINT("D"); DPRINT(i+1); DPRINT(": ");
+ shift((uint32_t)(1 << (i)));
+ DPRINTLN((uint32_t)(1 << (i)),HEX);
+ delay(3000);
+ }
+}
+
+void testTubes2() {
+while (true) {
+
+ DPRINTLN("Testing tubes: ");
+ for (int i = 0; i < 10; i++) {
+ DPRINT(i); DPRINT(" ");
+ for (int j = 0; j < maxDigits; j++) {
+ digit[j] = i;
+ digitDP[j] = i % 2;
+ }
+ //changeDigit();
+
+ writeDisplaySingle();
+ delay(3000);
+ yield();
+ }
+ } //end while
+}
+
+void showBits(uint32_t bits) {
+ boolean b;
+
+ for (uint32_t i = 0; i < 32; i++) {
+ if (SHIFT_LSB_FIRST)
+ b = ((bits & (uint32_t(1) << i))) > 0; //LSB first
+ else
+ b = (bits & (uint32_t(1) << (31 - i))) > 0; //MSB first
+ if (b) {
+ DPRINT("1");
+ }
+ else {
+ DPRINT("0");
+ }
+ } //end for
+ DPRINT("-");
+}
+
+*/
+#endif
diff --git a/firmware/uniclock/ESP32_UniClock2/HV5122new.ino b/firmware/uniclock/ESP32_UniClock2/HV5122new.ino
new file mode 100644
index 0000000..7acfa08
--- /dev/null
+++ b/firmware/uniclock/ESP32_UniClock2/HV5122new.ino
@@ -0,0 +1,422 @@
+//============================================================================
+// HV5122 Nixie tube driver (CURRENT / PREFERRED VERSION - "NEW")
+//----------------------------------------------------------------------------
+// Chip: Microchip/Supertex HV5122 - 32-channel high-voltage (up to ~170V)
+// serial-in shift register / latch, used here to drive Nixie tube
+// cathodes directly (no external cathode-driver transistors needed).
+// Wiring: Two HV5122 chips are cascaded (bit-banged, SPI-like, 3-wire bus):
+// PIN_DIN -> chip0 DIN, chip0 DOUT -> chip1 DIN (daisy-chained),
+// PIN_CLK -> both chips' CLK (shared),
+// PIN_OE -> both chips' output-enable (shared, active HIGH here
+// i.e. energizes/blanks the tubes - see writeDisplay()).
+// Together the two chips give 64 open-drain HV outputs, addressed
+// via the digitPins[][] table (pin numbers 1-32 = chip0, 101-132 =
+// chip1; see clocks.h for the actual per-clock wiring tables).
+// Status: This is the CURRENT driver: every HV5122-based config in
+// clocks.h (CLOCK_50, 51, 52, 53, 54, 55, 56, 57, 57A) defines
+// NEWHV5122, so this file is the one that actually gets compiled.
+// The plain "HV5122" macro (see HV5122.ino) is no longer selected
+// by any active config - that file is legacy/orphaned.
+// What's new vs. HV5122.ino: writeDisplay() (the timer ISR) now runs a
+// 3-phase state machine (old digit -> new digit -> blank) that
+// implements the digit-change "wipe" animation (animMask[] /
+// newDigit[] / oldDigit[], see changeDigit() mode 5 in the core
+// .ino) by time-slicing how long the OLD vs. NEW cathode pattern
+// is shown within one PWM refresh period. writeDisplaySingle() also
+// now only recomputes the shift-out bit buffers when digit/newDigit/
+// animMask actually changed (memcmp-cached), instead of
+// unconditionally re-shifting every 50ms.
+//============================================================================
+#ifdef NEWHV5122 //Nixie driver
+//P.S. 6x tubes PCB version
+
+char tubeDriver[] = "HV5122NEW";
+//HV5122 control pins
+//#define PIN_DIN 17 // DataIn - chip0 DOUT pin is connected to chip1 DIN pin!
+//#define PIN_CLK 22 // Clock
+//#define PIN_OE 21 // OutputEnable
+
+//int maxDigits = 6;
+
+//Data pin numbers 100+ means: chip1 pins are used. Chip0-s DOUT is connected to chip1's DIN
+/* Example:
+ byte digitPins[maxDigits+1][10] = {
+ {2,10,9,8,7,6,5,4,3,1}, //sec 1 , chip0 (tube#0)
+ {11,32,20,19,18,17,16,15,14,13}, //sec 10 , chip0 (tube#1)
+ {22,31,29,30,27,28,25,26,23,24}, //min 1 , chip0 (tube#2)
+ {101,131,110,109,108,107,106,105,104,103}, //min 10 , chip1 (tube#3)
+ {111,132,120,119,118,117,116,115,114,113}, //hour 1 , chip1 (tube#4)
+ {122,129,130,127,128,125,126,123,124,121}, //hour 10 , chip1 (tube#5)
+ {0,12,21,102,112,0, 0,0,0,0} //extra decimal point (tube0...tube6)
+ };
+*/
+
+// DRAM_ATTR: forces these globals to live in internal DRAM rather than being
+// placed somewhere the ISR might reach via the flash cache. On ESP32, if a
+// flash-cache miss/disable happens to coincide with a SPIFFS/OTA/flash write
+// while this IRAM_ATTR ISR (writeDisplay(), below) touches these variables,
+// an un-cached access could stall or crash; DRAM_ATTR keeps them reliably
+// accessible from the ISR at all times.
+uint32_t DRAM_ATTR bitBuffer0 = 0; //new digits - bit pattern for chip0, prepared by writeDisplaySingle(), consumed by the writeDisplay() ISR
+uint32_t DRAM_ATTR bitBuffer1 = 0; //new digits - chip1
+uint32_t DRAM_ATTR oldBitBuffer0 = 0; //old (pre-change) digits - chip0, shown briefly during the wipe/crossfade animation
+uint32_t DRAM_ATTR oldBitBuffer1 = 0; //old (pre-change) digits - chip1
+int DRAM_ATTR animPtr = 0; //0 = no animation in progress; otherwise the animMask[] value driving how the old/new time split is weighted (see writeDisplaySingle())
+
+#define SHIFT_LSB_FIRST false //true= LSB first, false= MSB first
+
+//Brightness PWM timing. Greater value => slower brightness PWM frequency
+// Suggested values: 100000 / 20000, for faster PWM: 50000 / 10000
+uint32_t DRAM_ATTR PWMrefresh = 100000; //full PWM period length, in timer ticks; also the time-budget shared between old-digit/new-digit/blank phases (see writeDisplay())
+uint32_t DRAM_ATTR PWM_min = 6000; //floor for the ON portion of the cycle, so very low brightness settings still produce a visible/stable flash instead of a near-zero-width pulse
+
+uint32_t DRAM_ATTR time1 = 2000; //how long (ticks) to show the OLD digit pattern this refresh cycle (state 0 below)
+uint32_t DRAM_ATTR time2 = 2000; //how long (ticks) to show the NEW digit pattern this refresh cycle (state 1 below)
+uint32_t DRAM_ATTR offTime = 2000; //how long (ticks) the display stays blanked (OE off) this refresh cycle (state 2 below) - this is what actually implements the brightness dimming
+uint32_t DRAM_ATTR brightness = 0;
+uint32_t DRAM_ATTR PWMtimeBrightness; //time1+time2 combined, i.e. total "lit" time per refresh cycle, derived from brightness/lux
+
+// shift(): bit-bangs one 32-bit word into the HV5122 shift-register chain.
+// The ~20-NOP delays around each CLK/DIN edge are required because the
+// HV5122 is a high-voltage part (internal level-shifters to the ~170V rail)
+// and is much slower than a typical 5V logic shift register - driving it at
+// full CPU speed without these delays can corrupt shifted-in data.
+// Data is latched into the HV5122 on the falling edge of CLK.
+// Runs from the ISR context here (called by writeDisplay() below), unlike in
+// HV5122.ino where it's only called from the main-loop writeDisplaySingle().
+#if defined(ESP32)
+void IRAM_ATTR shift(uint32_t Data) {
+#else
+void ICACHE_RAM_ATTR shift(uint32_t Data) {
+#endif
+ static boolean b;
+
+ for (uint32_t i = 0; i < 32; i++) {
+ digitalWrite(PIN_CLK, HIGH);
+ for (int t=0;t<20;t++) asm volatile ("nop");
+ if (SHIFT_LSB_FIRST)
+ b = ((Data & (uint32_t(1) << i))) > 0; //LSB first
+ else
+ b = (Data & (uint32_t(1) << (31 - i))) > 0; //MSB first
+ digitalWrite(PIN_DIN, b);
+ for (int t=0;t<20;t++) asm volatile ("nop");
+ digitalWrite(PIN_CLK, LOW); //falling CLK to store DIN
+ for (int t=0;t<20;t++) asm volatile ("nop");
+ }
+ digitalWrite(PIN_CLK, HIGH);
+ for (int t=0;t<20;t++) asm volatile ("nop");
+}
+
+void clearTubes() {
+ shift(0);
+ shift(0);
+}
+
+void setup_pins() {
+ DPRINTLN("Setup pins - HV5122 Nixie driver...");
+ DPRINT("- CLK : GPIO"); DPRINTLN(PIN_CLK);
+ DPRINT("- DATAIN: GPIO"); DPRINTLN(PIN_DIN);
+ DPRINT("- OUTPUT_ENABLE: GPIO"); DPRINTLN(PIN_OE);
+ pinMode(PIN_CLK, OUTPUT); regPin(PIN_CLK, "PIN_CLK");
+ pinMode(PIN_DIN, OUTPUT); regPin(PIN_DIN, "PIN_DIN");
+ pinMode(PIN_OE, OUTPUT); regPin(PIN_OE, "PIN_OE");
+ digitalWrite(PIN_CLK, HIGH);
+ digitalWrite(PIN_OE, LOW); //keep outputs blanked until clearTubes() below has pushed a known (all-zero) pattern into the shift registers
+ driverSetupStr = " HV5122 pin settings: ";
+ for (int i=0;i");
+ }
+ clearTubes();
+ startTimer();
+}
+
+
+// writeDisplay(): TIMER-ISR implementing a 3-phase state machine per refresh
+// cycle, re-armed with a different timer delay for each phase:
+// state 0: show the OLD (pre-change) digit pattern for time1 ticks
+// state 1: show the NEW digit pattern for time2 ticks
+// state 2: blank (OE off) for offTime ticks - this is the actual dimming mechanism
+// (state 3 is not a real switch case: it's set inline within state 2's
+// handling and consumed immediately, in the same ISR call, by the
+// "state==3" check below - it never survives to the next ISR invocation)
+// time1/time2/offTime/bitBuffer*/oldBitBuffer* are all precomputed once by
+// writeDisplaySingle() (main-loop side) whenever the digit/animation state
+// changes; this ISR just plays them back every refresh cycle. Showing the OLD
+// pattern briefly before the NEW one is what produces the digit-change wipe/
+// crossfade animation (animMask[]-driven) - when no animation is active,
+// writeDisplaySingle() sets oldBitBuffer==bitBuffer so phases 0 and 1 render
+// the identical (new) pattern and no visible transition occurs.
+// Each state's "if" falls through to the next case when its time is too short
+// (<1000 ticks) to bother with - e.g. if time1<1000 the old-digit phase is
+// skipped entirely and state 0 falls straight into state 1's code within the
+// same ISR call.
+#if defined(ESP32)
+void IRAM_ATTR writeDisplay() { //void IRAM_ATTR writeDisplay(){
+#else
+void ICACHE_RAM_ATTR writeDisplay() { //https://circuits4you.com/2018/01/02/esp8266-timer-ticker-example/
+#endif
+
+ static DRAM_ATTR byte state=0;
+ int timer = PWMrefresh; //fallback delay if neither state's time threshold is met below (e.g. both time1 and time2 < 1000 at very low brightness)
+
+ intCounter++;
+ if (EEPROMsaving) { //stop refresh, while EEPROM write is in progress!
+ #if defined(ESP8266)
+ timer1_write(PWMrefresh);
+ #endif
+ return;
+ }
+
+ #if defined(ESP32)
+ portENTER_CRITICAL_ISR(&timerMux);
+ noInterrupts();
+ #endif
+ digitalWrite(PIN_OE, LOW); //OFF - blank while shifting new data in, to avoid a brief flash of garbage/transitional segment patterns on the tubes
+ switch (state) { //state machine...
+ case 0: //show old character
+ if (time1>=1000) {
+ timer = time1;
+ shift(oldBitBuffer1);
+ shift(oldBitBuffer0);
+ state = 1;
+ break;
+ }
+ //time1 too short: fall through to state 1 without a break, skipping the old-digit phase for this cycle
+ case 1: //show new character
+ if (time2>=1000) {
+ timer = time2;
+ shift(bitBuffer1);
+ shift(bitBuffer0);
+ if (offTime<500) state = 0; //offTime negligible: skip the blank phase too, go straight back to old-digit next cycle
+ else state = 2;
+ break;
+ }
+ //time2 too short: fall through to state 2 without a break
+ case 2: //blank display
+ timer = offTime;
+ state = 3; //state 3 exists only to be caught by the "state==3" check just below, within this same ISR call - see comment above
+ break;
+ } //end switch
+
+ if (timer<500) timer = 500; //safety only... (timerAlarmWrite/timer1_write with 0 or a very small value can hang/misfire)
+
+ if ( (state==3) || (!radarON) || (brightness == 0) ) { //OFF state, blank digit
+ digitalWrite(PIN_OE, LOW); //OFF
+ state = 0; //resets the state machine to restart the old->new->blank cycle from state 0 on the next ISR call
+ }
+ else { //ON state
+ digitalWrite(PIN_OE, HIGH); //ON -- NOTE: active-HIGH OE on this board, opposite of the active-low /OE found on many shift-register ICs
+ }
+
+#if defined(ESP8266)
+ timer1_write(timer);
+#elif defined(ESP32)
+ portEXIT_CRITICAL_ISR(&timerMux);
+ timerAlarmWrite(ESP32timer, timer, true);
+ timerAlarmEnable(ESP32timer);
+ interrupts();
+#endif
+}
+
+// writeDisplaySingle(): recomputes the shift-out bit buffers (bitBuffer0/1,
+// oldBitBuffer0/1) and the per-phase timing (time1/time2/offTime) from the
+// current digit[]/newDigit[]/animMask[]/digitDP[] state, then hands them off
+// (via the shared globals above) to the writeDisplay() ISR to actually shift
+// out and time-multiplex. Called from the main loop; guarded by the memcmp
+// check below so the (relatively expensive) recompute only runs when
+// something actually changed, instead of unconditionally every call like
+// HV5122.ino's writeDisplaySingle() does.
+void writeDisplaySingle() {
+ static byte lastAnimMask[BUFSIZE];
+ static byte lastDigit[BUFSIZE];
+ static unsigned long lastRun = 0;
+ uint32_t b0,b1,ob0,ob1; //temp buffers
+ byte num;
+
+ // POSSIBLE BUG: this early-return only compares digit[]/newDigit[]/animMask[]
+ // against their cached values - it does NOT compare digitDP[] (the decimal
+ // point / separator-dot buffer). If only digitDP[] changes between calls
+ // (e.g. a blinking colon toggled by prm.enableBlink, or MAKE_BLINKING_DOTS
+ // mode - both used by this driver's configs, see clocks.h CLOCK_52/54)
+ // while digit/newDigit/animMask stay the same, this function returns early
+ // and bitBuffer0/1 are never recomputed with the new DP state, so the
+ // blink would not actually appear on the tubes. In most normal clock
+ // screens this is masked because the seconds digit (or minute digit) also
+ // changes in the same tick as the blink, but it could manifest as a
+ // frozen/stuck blinking dot on screens where only the dot toggles
+ // (e.g. while a static date/temperature reading is shown with blink enabled).
+ if ( (memcmp(digit,lastDigit,maxDigits) == 0) && (memcmp(lastDigit,newDigit,maxDigits) == 0) && (memcmp(animMask,lastAnimMask,maxDigits)==0) ) return;
+ //if ((millis()-lastRun)<5) return;
+ lastRun = millis();
+
+ memcpy(lastAnimMask,animMask,maxDigits);
+ memcpy(lastDigit,digit,maxDigits);
+
+ animPtr = 0;
+ b0 = 0; b1 = 0;
+ for (int i = 0; i < maxDigits; i++) { //Set the clock digits new values
+ num = digit[i];
+ if (animMask[i]>0) { //this tube position is mid-animation: use the incoming NEW value here (digit[i] is presumably still the transitional/old value at this point) and remember the mask so we know to build an "old" buffer below too
+ animPtr = animMask[i];
+ num = newDigit[i];
+ }
+ if (num < 10) { //codes 10 (BLANK), 11 (dash), 12 (dp glyph) and 13+ (extra glyphs) have no cathode mapping on a Nixie tube -> leave bits cleared = tube blank
+ if (digitPins[i][num] < 100) {
+ b0 |= (uint32_t)(1 << (digitPins[i][num]-1));
+ //DPRINT(digitPins[i][num]); DPRINT(":");
+ } //chip0
+ else {
+ b1 |= (uint32_t)(1 << (digitPins[i][num] - 101));
+ //DPRINT(digitPins[i][num]);DPRINT(":");} //chip1
+ }
+ }
+ } //end for i
+
+if (animPtr>0) { //at least one position is animating -> also build the OLD-pattern buffer, shown during the "state 0" phase in writeDisplay() for the crossfade/wipe effect
+ ob0 = 0; ob1 = 0;
+ for (int i = 0; i < maxDigits; i++) { //Set the clock digits old values
+ num = oldDigit[i];
+ if (num < 10) {
+ if (digitPins[i][num] < 100) {
+ ob0 |= (uint32_t)(1 << (digitPins[i][num]-1));
+ //DPRINT(digitPins[i][num]); DPRINT(":");
+ } //chip0
+ else {
+ ob1 |= (uint32_t)(1 << (digitPins[i][num] - 101));
+ //DPRINT(digitPins[i][num]);DPRINT(":");} //chip1
+ }
+ }
+ } //end for i
+}
+
+ #ifdef MAKE_BLINKING_DOTS //it means, the extra datapins are used as 4 blinking dot instead of decimal points!
+ if (showClock) {
+ digitDP[1] = digitDP[2]; //left two dots
+ digitDP[3] = digitDP[4]; //right two dots
+ }
+ #endif
+
+ for (int i = 0; i < maxDigits-1; i++) { //Set the extra decimal point dots
+ if (digitDP[i] && digitPins[maxDigits][i]>0) {
+ //DP bits are ORed into BOTH the new (b0/b1) and old (ob0/ob1) buffers, so
+ //decimal points/separator dots stay lit continuously through the old->new
+ //wipe animation instead of blinking off during the old-digit phase.
+ if (digitPins[maxDigits][i] < 100) {
+ b0 |= (uint32_t)(1 << (digitPins[maxDigits][i]-1)); //chip0
+ ob0 |= (uint32_t)(1 << (digitPins[maxDigits][i]-1)); //chip0
+ }
+ else {
+ b1 |= (uint32_t)(1 << (digitPins[maxDigits][i] - 101)); //chip1
+ ob1 |= (uint32_t)(1 << (digitPins[maxDigits][i] - 101)); //chip1
+ }
+ }
+ } //end for i
+
+
+
+ brightness = displayON ? prm.dayBright : prm.nightBright;
+ if (brightness>MAXBRIGHTNESS) brightness = MAXBRIGHTNESS; //only for safety
+
+ if (autoBrightness && displayON)
+ PWMtimeBrightness = max(PWM_min, (PWMrefresh*lx)/MAXIMUM_LUX); //day mode + ALS: scale total lit-time by measured lux instead of the stored brightness setting
+ else
+ PWMtimeBrightness = max(PWM_min, (PWMrefresh*brightness)/MAXBRIGHTNESS);
+
+ //Below a certain lit-time (PWMtimeBrightness<15000, i.e. brightness/lux is
+ //low), skip the wipe animation altogether: the old/new split would be too
+ //fast to see anyway, and at very short lit-times splitting it further into
+ //old+new sub-phases risks pulses too short for the HV5122/tubes to respond
+ //to cleanly. animPtr=10 forces an even old:new time split further below,
+ //but since ob0/ob1==b0/b1 here, phase 0 and phase 1 render identically
+ //(no visible transition) regardless of the split.
+ if ((animPtr==0) || (PWMtimeBrightness<15000)) { //no animation #5
+ ob0 = b0;
+ ob1 = b1;
+ animPtr = 10; //to make equal timing
+ }
+
+ //Split the total lit time (PWMtimeBrightness) between the OLD-digit phase
+ //(time1) and NEW-digit phase (time2) according to animPtr, which walks from
+ //a low value (just-started animation, mostly OLD digit still shown) up to
+ //~20 (animation complete, mostly/only NEW digit) as the wipe animation
+ //progresses across successive changeDigit() calls - see animMask[] in the
+ //core .ino. animPtr==10 (forced above when not animating) gives an even
+ //50/50 split, which is harmless there since ob0/ob1==b0/b1 already.
+ time1 = PWMtimeBrightness * (20-animPtr)/20;
+ if (time1<1000) time1=0; //too short to bother with a separate old-digit phase -> writeDisplay() will fall through and skip it
+ if (time1 > (PWMtimeBrightness-1000)) time1 = PWMtimeBrightness; //leave less than 1000 ticks for time2 -> just give the old phase the whole lit budget instead
+ time2 = PWMtimeBrightness - time1;
+ offTime = PWMrefresh - time1 - time2; //remaining budget of the refresh period is spent blanked - this (not time1/time2) is what actually produces the brightness dimming
+
+ bitBuffer0 = b0;
+ bitBuffer1 = b1;
+ oldBitBuffer0 = ob0;
+ oldBitBuffer1 = ob1;
+
+// DPRINT("Br:"); DPRINT(brightness); DPRINT(" A:"); DPRINT(animPtr);
+// DPRINT(" PTB"); DPRINT(PWMtimeBrightness);
+// DPRINT(" t1:"); DPRINT(time1); DPRINT(" t2:"); DPRINT(time2); DPRINT(" off:"); DPRINT(offTime);
+// DPRINT(" "); DPRINT(b0,HEX); DPRINT(" "); DPRINT(b1,HEX); DPRINT(" ");
+// DPRINT(ob0,HEX); DPRINT(" "); DPRINTLN(ob1,HEX);
+
+}
+
+
+//For testing -------------------
+/*
+void test() {
+ digitalWrite(PIN_OE, HIGH);
+ clearTubes();
+ for (int j=0;j<2;j++)
+ for (uint32_t i=0;i<32;i++) {
+ DPRINT("D"); DPRINT(i+1); DPRINT(": ");
+ shift((uint32_t)(1 << (i)));
+ DPRINTLN((uint32_t)(1 << (i)),HEX);
+ delay(3000);
+ }
+}
+
+void testTubes2() {
+while (true) {
+
+ DPRINTLN("Testing tubes: ");
+ for (int i = 0; i < 10; i++) {
+ DPRINT(i); DPRINT(" ");
+ for (int j = 0; j < maxDigits; j++) {
+ digit[j] = i;
+ digitDP[j] = i % 2;
+ }
+ //changeDigit();
+
+ writeDisplaySingle();
+ delay(3000);
+ yield();
+ }
+ } //end while
+}
+
+void showBits(uint32_t bits) {
+ boolean b;
+
+ for (uint32_t i = 0; i < 32; i++) {
+ if (SHIFT_LSB_FIRST)
+ b = ((bits & (uint32_t(1) << i))) > 0; //LSB first
+ else
+ b = (bits & (uint32_t(1) << (31 - i))) > 0; //MSB first
+ if (b) {
+ DPRINT("1");
+ }
+ else {
+ DPRINT("0");
+ }
+ } //end for
+ DPRINT("-");
+}
+
+*/
+#endif
diff --git a/firmware/uniclock/ESP32_UniClock2/I2Csensors.ino b/firmware/uniclock/ESP32_UniClock2/I2Csensors.ino
new file mode 100644
index 0000000..012af34
--- /dev/null
+++ b/firmware/uniclock/ESP32_UniClock2/I2Csensors.ino
@@ -0,0 +1,450 @@
+// ============================================================================
+// I2Csensors.ino
+// ----------------------------------------------------------------------------
+// Drivers for the I2C environmental sensors supported by this codebase.
+// All sensors below share ONE physical I2C bus (Wire, PIN_SDA/PIN_SCL, set
+// per-CLOCK_NN in clocks.h) and can be enabled in ANY combination for a given
+// clock build - several CLOCK_NN configs enable 3-5 of them at once:
+//
+// USE_BME280 - Bosch BME280, temperature + humidity + pressure (Adafruit_BME280_Library)
+// I2C address 0x76 (hardcoded here; chip also supports 0x77)
+// USE_BMP280 - Bosch BMP280, temperature + pressure only (Adafruit_BMP280_Library)
+// I2C address 0x77 (hardcoded here; chip also supports 0x76)
+// -> BME280 and BMP280 are deliberately pinned to different
+// default addresses so both can coexist on the same bus.
+// USE_AHTX0 - AHT10/AHT20, temperature + humidity (Adafruit_AHTX0)
+// I2C address 0x38 (fixed, not configurable on the chip)
+// USE_SHT21 - Sensirion SHT21, temperature + humidity (custom SHT21 lib)
+// I2C address 0x40 (7-bit); see getSHT21()/detection code below
+// USE_BH1750 - Ambient light / lux meter (BH1750 lib)
+// I2C address 0x23 (hardcoded; chip also supports 0x5C)
+//
+// Which CLOCK_NN configs use which sensor (see clocks.h):
+// USE_SHT21 alone : CLOCK_23, CLOCK_23A, CLOCK_23B
+// BME280+BMP280+AHTX0+SHT21 : CLOCK_26
+// All five (incl. BH1750) : CLOCK_46, CLOCK_48, CLOCK_50 (and, commented
+// out / inactive by default, CLOCK_51, 52, 54,
+// 55, 56, 57, 57A), CLOCK_99
+// BME280+AHTX0+SHT21 : CLOCK_80, CLOCK_81, CLOCK_82
+//
+// USE_I2CSENSORS (the #ifdef guarding this whole file) is NOT set directly by
+// any CLOCK_NN block - it is auto-derived in clocks.h:
+// #if defined(USE_BME280) || defined(USE_BMP280) || defined(USE_AHTX0) ||
+// defined(USE_SHT21) || defined(USE_BH1750) || defined(USE_RTC)
+// #define USE_I2CSENSORS
+// i.e. it turns on automatically as soon as any one sensor (or the RTC) is
+// requested for the selected clock.
+//
+// MULTI-SENSOR INDEXING: temperature[6]/humid[6]/pressur[6] (declared in the
+// core .ino) are shared result arrays. Each sensor type that measures a given
+// quantity claims the NEXT FREE slot in that quantity's array at boot, using
+// three independent counters (useTemp/useHumid/usePress) - see the detailed
+// comment in setupI2Csensors() below for exactly how collisions are avoided
+// when several sensors are active simultaneously (e.g. CLOCK_26).
+//
+// Calibration offsets (prm.corrT0/corrT1/corrH0/corrH1) are NOT applied in
+// this file - they are added later, at display time, in displayTemp()/
+// displayHumid() in the core .ino. This file only stores raw (rounded)
+// readings, so there is no risk of double-applying the correction here.
+// ============================================================================
+#ifdef USE_I2CSENSORS
+
+// NOTE: these are commented-out and inert by design - the real per-build
+// enabling of each sensor happens in clocks.h inside the relevant CLOCK_NN
+// block. Since this .ino is compiled into every build regardless of which
+// clock is selected, uncommenting one of these lines here would force that
+// sensor ON for EVERY clock (as long as USE_I2CSENSORS ends up defined by
+// some other means), not just the one you're working on. Leave commented;
+// they exist only as a quick reference of the five supported macros.
+//#define USE_BME280
+//#define USE_BMP280
+//#define USE_AHTX0
+//#define USE_SHT21
+//#define USE_BH1750
+
+#include
+#include
+
+#ifdef USE_BME280
+ #include
+ Adafruit_BME280 bme; // use I2C interface address = 0x76 or 0x77
+ Adafruit_Sensor *bme_temp = bme.getTemperatureSensor();
+ Adafruit_Sensor *bme_pressure = bme.getPressureSensor();
+ Adafruit_Sensor *bme_humidity = bme.getHumiditySensor();
+ // *Ptr variables below hold this sensor's assigned slot (index) in the
+ // shared temperature[]/humid[]/pressur[] arrays. They are filled in once,
+ // in setupI2Csensors(), and stay 0 (default global init) if the sensor
+ // isn't found - harmless, since the getBME280() reader is only ever
+ // called when BME280exist is true.
+ byte BME280tempPtr;
+ byte BME280humidPtr;
+ byte BME280pressPtr;
+#endif
+
+#ifdef USE_BMP280
+ #include
+ // Hardcoded to 0x77 (BME280 above is hardcoded to 0x76) so that both chips
+ // can be wired to the same bus at once without an address clash - several
+ // CLOCK_NN configs (e.g. CLOCK_26, CLOCK_46/48/50, CLOCK_99) enable both
+ // USE_BME280 and USE_BMP280 simultaneously.
+ Adafruit_BMP280 bmp; // use I2C interface address = 0x76 or 0x77
+ Adafruit_Sensor *bmp_temp = bmp.getTemperatureSensor();
+ Adafruit_Sensor *bmp_pressure = bmp.getPressureSensor();
+ byte BMP280tempPtr;
+ byte BMP280humidPtr; // NOTE: BMP280 has no humidity sensor - this pointer is declared for symmetry but never used/assigned (BMP280 never touches humid[]).
+ byte BMP280pressPtr;
+#endif
+
+#ifdef USE_AHTX0
+ #include //I2C address: 0x38
+ Adafruit_AHTX0 aht;
+ Adafruit_Sensor *aht_humidity, *aht_temp;
+ byte AHTX0tempPtr;
+ byte AHTX0humidPtr;
+#endif
+
+#ifdef USE_SHT21
+ // POSSIBLE BUG (see detection code in setupI2Csensors() below): this header
+ // comment says the 8-bit write/read addresses are 0x80/0x81, which
+ // correspond to 7-bit address 0x40 (the SHT21's actual, standard I2C
+ // address). The Arduino Wire library used here takes a 7-bit address
+ // (0-127), so 0x80 is out of range for Wire.beginTransmission() - see the
+ // detection code further down which probes both 0x40 (correct) and 0x80
+ // (likely meaningless/dead check for this Wire API).
+ #include //I2C address: 0x80 and 0x81
+ SHT21 sht;
+ float SHT21temp,SHT21humid =0;
+ byte SHT21tempPtr;
+ byte SHT21humidPtr;
+#endif
+
+#ifdef USE_BH1750
+ #include
+ BH1750 lightMeter(0x23); // fixed address 0x23 (ADDR pin low/floating); chip's alternate address is 0x5C
+#endif
+
+void setupI2Csensors() {
+ DPRINTLN("Starting I2Csensors sensors...");
+
+ // pinMode(OUTPUT) here is just for regPin() bookkeeping (pin-conflict
+ // registration/debug); Wire.begin() below reconfigures both pins for I2C
+ // (open-drain + pull-ups) regardless of this pinMode call.
+ pinMode(PIN_SDA,OUTPUT); regPin(PIN_SDA,"PIN_SDA");
+ pinMode(PIN_SCL,OUTPUT); regPin(PIN_SCL,"PIN_SCL");
+ Wire.begin(PIN_SDA,PIN_SCL);
+ delay(100);
+ I2Cscanner(); // debug-only bus scan; logs every ACKing address, does not affect detection below
+
+#ifdef USE_RTC
+ DPRINTLN("Starting RTC Clock...");
+ while(millis()<2000) {yield();} //waiting for 2 sec from startup
+ //I2C_ClearBus();
+ delay(100);
+ #if defined(PIN_SDA) && defined(PIN_SCL)
+ Wire.beginTransmission(0x68);
+ byte error = Wire.endTransmission();
+ if (error == 0) { //DS3231_get_addr(0x68)
+ RTCexist = true;
+ DPRINTLN("RTC found on 0x68.");
+ }
+ else {
+ DPRINTLN("!!!No RTC found on 0x68!!!");
+ }
+ #else
+ RTCexist = false;
+ DPRINTLN("PIN_SDA or PIN_SCL not defined!");
+ #endif
+#endif
+
+
+ // ---------------------------------------------------------------------
+ // MULTI-SENSOR INDEXING SCHEME (applies to every #ifdef block below):
+ // temperature[]/humid[]/pressur[] are shared 6-slot arrays that can hold
+ // readings from several sensors at once. There are THREE INDEPENDENT
+ // counters - useTemp, useHumid, usePress - one per array/quantity, each
+ // incremented only by sensors that actually measure that quantity.
+ // Each sensor, when found, "reserves" the array slot equal to the CURRENT
+ // value of the relevant counter (its own xxxPtr), then increments
+ // that counter so the NEXT detected sensor of that quantity gets the next
+ // free slot. Because each array has its own counter, two sensors can
+ // never collide/overwrite each other's slot in the SAME array, even when
+ // several are active together (e.g. CLOCK_26: BME280+BMP280+AHTX0+SHT21):
+ // temperature[]: BME280->0, BMP280->1, AHTX0->2, SHT21->3 (useTemp=4)
+ // humid[] : BME280->0, AHTX0->1, SHT21->2 (useHumid=3, BMP280 has none)
+ // pressur[] : BME280->0, BMP280->1 (usePress=2, AHTX0/SHT21 have none)
+ // Detection order below (BME280, BMP280, AHTX0, SHT21, BH1750) therefore
+ // determines slot order. Downstream, only slots 0 and 1 of temperature[]/
+ // humid[] receive a manual calibration offset (prm.corrT0/corrT1/corrH0/
+ // corrH1, applied in displayTemp()/displayHumid() in the core .ino) - a
+ // 3rd+ simultaneous temperature/humidity sensor (e.g. SHT21 at slot 2/3
+ // above) is displayed uncorrected. That is a limitation of the core
+ // display code, not of the indexing here, but worth knowing when wiring
+ // up configs with 3+ temp/humid sensors.
+ // ---------------------------------------------------------------------
+#ifdef USE_BME280
+ if (!bme.begin(0x76)) {
+ DPRINTLN("Could not find a valid BME280 sensor on address 0x76, check wiring!");
+ }
+ else {
+ BME280exist = true;
+ DPRINTLN("BME280 Found!");
+ bme_temp->printSensorDetails();
+ bme_pressure->printSensorDetails();
+ bme_humidity->printSensorDetails();
+ BME280tempPtr = useTemp; //remember my ID-s
+ BME280humidPtr = useHumid;
+ BME280pressPtr = usePress;
+ useTemp++; //increase the number of sensors
+ useHumid++;
+ usePress++;
+ }
+ #endif
+
+#ifdef USE_BMP280
+ if (!bmp.begin(0x77)) {
+ DPRINTLN("Could not find a valid BMP280 sensor on address 0x77, check wiring!");
+ }
+ else {
+ BMP280exist = true;
+ /* BMP280 default settings from datasheet. */
+ DPRINTLN("BMP280 Found!");
+ bmp.setSampling(Adafruit_BMP280::MODE_NORMAL, /* Operating Mode. */
+ Adafruit_BMP280::SAMPLING_X2, /* Temp. oversampling */
+ Adafruit_BMP280::SAMPLING_X16, /* Pressure oversampling */
+ Adafruit_BMP280::FILTER_X16, /* Filtering. */
+ Adafruit_BMP280::STANDBY_MS_500); /* Standby time. */
+ bmp_temp->printSensorDetails();
+ BMP280tempPtr = useTemp; //remember my ID-s
+ BMP280pressPtr = usePress; //remember my ID-s
+ useTemp++; //increase the number of sensors
+ usePress++; //increase the number of sensors
+ }
+#endif
+
+#ifdef USE_AHTX0
+ if (!aht.begin()) {
+ DPRINTLN("Could not find a valid AHT10/AHT20 sensor, check wiring!");
+ }
+ else {
+ AHTX0exist = true;
+ DPRINTLN("AHT10/AHT20 Found!");
+ aht_temp = aht.getTemperatureSensor();
+ aht_temp->printSensorDetails();
+ aht_humidity = aht.getHumiditySensor();
+ aht_humidity->printSensorDetails();
+ AHTX0tempPtr = useTemp; //remember my ID-s
+ AHTX0humidPtr = useHumid;
+ useTemp++; //increase the number of sensors
+ useHumid++;
+ }
+#endif
+ DPRINTLN(" ");
+
+#ifdef USE_SHT21
+ int error1,error2;
+ Wire.beginTransmission(0x40); // correct 7-bit SHT21 address
+ error1 = Wire.endTransmission();
+ // POSSIBLE BUG: 0x80 is not a valid 7-bit I2C address for Wire.beginTransmission()
+ // (Wire expects 0-127; 0x80/128 is the 8-bit write address, i.e. 0x40<<1).
+ // This second probe is likely a no-op/dead check left over from confusing
+ // 7-bit vs 8-bit addressing (see comment above the #include line).
+ // Harmless as written because of the "&&" below (SHT21exist still gets set
+ // correctly whenever the real 0x40 probe succeeds), but error2 does not
+ // reliably indicate anything about a real device.
+ Wire.beginTransmission(0x80);
+ error2 = Wire.endTransmission();
+ if ((error1 !=0) && (error2!=0)) {
+ DPRINTLN("Could not find a valid SHT21 sensor on 0x40 / 0x80, check wiring!");
+ }
+ else {
+ SHT21exist = true;
+ DPRINTLN("SHT21 Found!");
+ SHT21tempPtr = useTemp; //remember my ID-s
+ SHT21humidPtr = useHumid;
+ useTemp++; //increase the number of sensors
+ useHumid++;
+ }
+#endif
+
+#ifdef USE_BH1750
+ // BH1750 doesn't share temperature[]/humid[]/pressur[] - it has its own
+ // dedicated useLux counter/flow (see getBH1750() at the bottom of this
+ // file, called from getLightSensor() in the core .ino), so no slot/Ptr
+ // bookkeeping is needed here.
+ if (lightMeter.begin(BH1750::CONTINUOUS_HIGH_RES_MODE)) {
+ DPRINTLN("BH1750 luxmeter sensor found");
+ BH1750exist = true;
+ useLux++;
+ }
+ else {
+ DPRINTLN("Error initialising BH1750 on address 0x23");
+ }
+#endif
+
+ DPRINTLN(" ");
+} //end of SetupI2Csensors()
+
+
+// getBME280()/getBMP280()/getAHTX0()/getSHT21() below each write ONLY into
+// their own reserved slot (the *Ptr index assigned in setupI2Csensors(), see
+// the indexing scheme comment there) and store the RAW rounded reading - no
+// calibration offset (prm.corrT0/corrT1/corrH0/corrH1) is applied here; that
+// happens later, at display time, in displayTemp()/displayHumid() (core
+// .ino), so there's no double-correction between this file and that one.
+void getBME280() {
+#ifdef USE_BME280
+ temperature[BME280tempPtr] = round1(bme.readTemperature());
+ humid[BME280humidPtr] = round1(bme.readHumidity());
+ pressur[BME280pressPtr] = round1(bme.readPressure()/100);
+ DPRINT("BME280: ");
+ DPRINT("Temperature = ");
+ DPRINT(temperature[BME280tempPtr]);
+ DPRINT(" *C");
+
+ DPRINT(" Humidity = ");
+ DPRINT(humid[BME280humidPtr]);
+ DPRINT(" %");
+
+ DPRINT(" Pressure = ");
+ DPRINT(pressur[BME280pressPtr]);
+ DPRINTLN(" hPa");
+#endif
+}
+
+
+void getBMP280() {
+#ifdef USE_BMP280
+ temperature[BMP280tempPtr] = round1(bmp.readTemperature());
+ pressur[BMP280pressPtr] = round1(bmp.readPressure()/100);
+ DPRINT("BMP280: ");
+ DPRINT(F("Temperature = "));
+ DPRINT(temperature[BMP280tempPtr]);
+ DPRINT(" *C");
+
+ DPRINT(F(" Pressure = "));
+ DPRINT(pressur[BMP280pressPtr]);
+ DPRINTLN(" hPa");
+#endif
+}
+
+void getAHTX0() {
+#ifdef USE_AHTX0
+ // /* Get a new normalized sensor event */
+ sensors_event_t humidity;
+ sensors_event_t temp;
+ aht_humidity->getEvent(&humidity);
+ aht_temp->getEvent(&temp);
+ temperature[AHTX0tempPtr] = round1(temp.temperature);
+ DPRINT("AHT sensor: ");
+ DPRINT(" Temperature ");
+ DPRINT(temperature[AHTX0tempPtr]);
+ DPRINT(" deg C");
+
+ DPRINT(" Humidity: "); /* Display the results (humidity is measured in % relative humidity (% rH) */
+ humid[AHTX0humidPtr] = round1(humidity.relative_humidity);
+ DPRINT(humid[AHTX0humidPtr]);
+ DPRINTLN(" % rH");
+#endif
+}
+
+void getSHT21() {
+#ifdef USE_SHT21
+ temperature[SHT21tempPtr] = round1(sht.getTemperature());
+ DPRINT("SHT21 sensor: ");
+ DPRINT(" Temperature ");
+ DPRINT(temperature[SHT21tempPtr]);
+ DPRINT(" deg C");
+
+ DPRINT(" Humidity: "); /* Display the results (humidity is measured in % relative humidity (% rH) */
+ humid[SHT21humidPtr] = round1(sht.getHumidity());
+ DPRINT(humid[SHT21humidPtr]);
+ DPRINTLN(" % rH");
+#endif
+}
+
+
+
+void getI2Csensors() {
+ static unsigned long lastRun = 0;
+
+ // Called every ~200ms from timeProgram() (core .ino), but the actual I2C
+ // reads only happen once every 30s per sensor - this throttle avoids
+ // hammering the bus/sensors and is independent of the caller's cadence.
+ // lastRun==0 lets the very first call (at millis()==0..~30000) through
+ // immediately so readings are available right after boot.
+ if (((millis()-lastRun)<30000) && (lastRun !=0)) return; // || (second()!=TEMP_START)
+ lastRun = millis();
+ if (BME280exist) getBME280();
+ if (BMP280exist) getBMP280();
+ if (AHTX0exist) getAHTX0();
+ if (SHT21exist) getSHT21();
+ if (BH1750exist) getBH1750();
+}
+
+
+void I2Cscanner() {
+ byte error, address;
+ int nDevices=0;
+ DPRINTLN(F("________________________________"));
+ DPRINTLN("Scanning I2C devices...");
+
+ for(address = 1; address < 127; address++ ) {
+ // The i2c_scanner uses the return value of
+ // Wire.endTransmission() to see if
+ // a device acknowledged the address.
+ Wire.beginTransmission(address);
+ error = Wire.endTransmission();
+
+ if (error == 0) {
+ DPRINT("I2C device found at address 0x");
+ if (address<16)
+ DPRINT("0");
+ DPRINT(address,HEX);
+ DPRINTLN(" !");
+
+ nDevices++;
+ }
+ else if (error==4) {
+ DPRINT("Unknown error at address 0x");
+ if (address<16)
+ DPRINT("0");
+ DPRINTLN(address,HEX);
+ }
+ } //end for
+
+ if (nDevices == 0)
+ DPRINTLN("No I2C devices found\n");
+ DPRINTLN(F("_________________________________"));
+}
+
+
+#else
+void setupI2Csensors() {}
+void getI2Csensors() {}
+#endif
+
+#ifdef USE_BH1750
+// Called directly from getLightSensor() (core .ino) whenever BH1750exist is
+// true - unlike the other get*() functions this is NOT gated through
+// getI2Csensors()'s 30s throttle, so it can be polled as often as needed;
+// the BH1750 library itself paces new measurements (measurementReady()).
+int getBH1750() {
+ static float oldLux = MAXIMUM_LUX;
+
+ if (lightMeter.measurementReady()) {
+ float lux = lightMeter.readLightLevel();
+ //DPRINT("BH1750 Light: "); DPRINT(lux); DPRINTLN(" lx");
+
+ if (lux>MAXIMUM_LUX) lux = MAXIMUM_LUX; //Limited //Limit lux value to maximum
+ if (lux<0) lux = oldLux; // negative reading = bus glitch/error from the library; ignore and keep last good value
+ oldLux = oldLux + (lux-oldLux)/10; //slow down Lux change (simple exponential smoothing, ~10-sample time constant)
+ }
+ return (int)oldLux;
+}
+#else
+// Stub used when USE_BH1750 is not defined for the selected clock, so
+// getLightSensor() (core .ino) can call getBH1750() unconditionally.
+int getBH1750() {
+ return(0);
+}
+#endif
diff --git a/firmware/uniclock/ESP32_UniClock2/MAX6921.ino b/firmware/uniclock/ESP32_UniClock2/MAX6921.ino
new file mode 100644
index 0000000..67768cf
--- /dev/null
+++ b/firmware/uniclock/ESP32_UniClock2/MAX6921.ino
@@ -0,0 +1,305 @@
+#ifdef MAX6921
+//===========================================================================================
+// Driver for the MAXIM MAX6921 20-output VFD/vacuum-tube driver chip -- ESP8266 variant.
+//
+// Hardware / protocol:
+// The MAX6921 is NOT a segment/digit-specific chip -- it is a generic 20-bit serial-in,
+// parallel-out latch driving 20 high-voltage open-drain outputs (OUT19..OUT0). Which
+// physical output pin ends up wired to a VFD segment (a..g, DP) and which to a tube's
+// grid/anode (digit select) is decided purely by the PCB wiring, described per-clock in
+// clocks.h via the segmentEnablePins[8] and digitEnablePins[] arrays. Every refresh, this
+// driver ORs together "which digit-select output is active" with "which segment outputs
+// should light up for the current character" into a single 20-bit word and shifts the
+// whole word out.
+// Serial protocol (3 wires, similar to SPI but no MISO/CS in the usual sense):
+// DATA (PIN_DATA) - serial bit, sampled on CLK rising edge, MSB (OUT19) first
+// CLK (PIN_CLK) - one pulse per bit, 20 pulses per full frame
+// LE (PIN_LE) - Latch Enable: after the 20 bits are clocked in, a LOW->HIGH->LOW
+// pulse transfers the shift register into the (separate) output
+// latch, so outputs change atomically instead of glitching mid-shift
+// BL (PIN_BL) - BLANK: MAX6921's active-HIGH output disable. HIGH = all 20 outputs
+// forced off (tube dark) regardless of latch content, LOW = outputs
+// follow the latch. Used here both for hard blanking and, via
+// ON/OFF time-slicing in writeDisplay(), for brightness control.
+//
+// Used by (see clocks.h, search for "#define MAX6921"):
+// CLOCK_3 - PCB-less clock, single IV-18 VFD tube
+// CLOCK_4 - custom clock, 4x IV-11 VFD tubes
+// CLOCK_5 - UNFI PCB clock, IVL-2-5/7 tube with RTC
+// CLOCK_14 - 6x LD8035 VFD-clock
+// CLOCK_15 / CLOCK_15a - 6x IV-6 VFD-clock RGB
+// CLOCK_16 - 4x IV-6, 1x LD8035
+// CLOCK_18 - IV-18 VFD-clock RGB
+// (all of the above are ESP8266 boards -- the ESP32 counterpart configs use
+// MAX6921_ESP32.ino instead, see that file's header for their CLOCK_NN list.)
+//===========================================================================================
+//VFD driver for ESP8266
+char tubeDriver[] = "MAX6921";
+//------------------abcdefgDP---------------- definition of different characters
+// Each byte below is one glyph's segment pattern, bit7..bit0 = a,b,c,d,e,f,g,DP (a=MSB).
+// A '1' bit means "this segment should be lit" for that glyph; generateBitTable() later
+// translates these abstract a..g/DP bit positions into the actual MAX6921 OUTx bits using
+// segmentEnablePins[] (the PCB-specific wiring), producing charTable[] used in writeDisplay().
+// Indices 0-9 are the digits; 10-20 are special glyphs referenced via the digit[] buffer
+// (see ESP32_UniClock2.ino digit[] value convention: 10=BLANK,11=minus,12=DP,13+=extra glyphs).
+byte charDefinition[] = {
+ B11111100, //0: abcdef
+ B01100000, //1: bc
+ B11011010, //2: abdeg
+ B11110010, //3: abcdg
+ B01100110, //4: bcfg
+ B10110110, //5: acdfg
+ B10111110, //6: acdefg
+ B11100000, //7: abc
+ B11111110, //8: abcdefg
+ B11110110, //9: abcdfg
+ B00000000, // : BLANK (10)
+ B00000010, //-: minus (11)
+ B00000001, // decimal point (12)
+ B11101110, // A abcefg (13)
+ B11001110, // P abefg (14)
+ B10011100, // C adef (15)
+ B11000110, //grad (upper circle) abfg (16)
+ B10110100, //% acdf (17)
+ B00111010, //lower circle cdeg (18)
+ B01100000, //I bc (19)
+ B10001110 //F aefg (20)
+
+};
+
+uint32_t animationMaskBits[5]; //precomputed AND-masks for the "wipe" transition animation (animMask[]), see generateBitTable()
+
+// NOTE: MAXCHARS/MAXSEGMENTS/maxDigits rely on sizeof() over `byte` arrays (1 byte/element),
+// so sizeof() == element count here. segmentEnablePins[] must always have exactly 8 entries
+// (a,b,c,d,e,f,g,DP) per the clocks.h convention; digitEnablePins[] length = number of tubes.
+#define MAXCHARS sizeof(charDefinition)
+#define MAXSEGMENTS sizeof(segmentEnablePins)
+int maxDigits = sizeof(digitEnablePins);
+
+uint32_t charTable[MAXCHARS]; //generated pin table from segmentDefinitions
+uint32_t segmentEnableBits[MAXSEGMENTS]; //bitmaps, generated from EnablePins tables
+// FIXED (was fixed-size [10] regardless of the actual maxDigits): sized to BUFSIZE (12, see the
+// digit[]/newDigit[]/animMask[] buffers in ESP32_UniClock2.ino), the same ceiling the rest of the
+// codebase uses for tube-position buffers, so this can no longer silently overflow in
+// generateBitTable() for any config the system otherwise supports.
+uint32_t digitEnableBits[12];
+
+// Multiplex/brightness timing. Despite the variable names, these are NOT milliseconds -- they
+// are raw ESP8266 hardware timer1 tick counts (see startTimer() in ESP32_UniClock2.ino: timer1
+// runs with TIM_DIV16, i.e. 80MHz/16 = 5MHz, so 1 tick = 0.2us). So PWMrefresh=5500 ticks is
+// actually ~1.1ms per full ON/OFF half-cycle, and PWM_min/PWM_max (500..5000 ticks) is ~100us..1ms.
+// Greater PWMrefresh => slower multiplex/refresh frequency (more visible flicker risk).
+int PWMrefresh=5500; //Multiplex time period, in timer1 ticks (not milliseconds, despite the variable name -- see note above). Greater value => slower multiplex frequency
+int PWM_min = 500;
+int PWM_max = 5000;
+
+//int PWMtiming[11] = {0,500,800,1200,2000,2500,3000,3500,4000,4500,5000};
+//-----------------------------------------------------------------------------------------
+
+//https://sub.nanona.fi/esp8266/timing-and-ticks.html
+//One tick is 1us / 80 = 0.0125us = 12.5ns on 80MHz
+//One tick is 1us / 80 = 0.0125us = 6.25ns on 160MHz
+// 1 NOP is 1 tick
+
+
+// Dead/no-op helper: `d` is ignored, so this always executes exactly one NOP regardless of the
+// requested delay -- effectively a leftover no-op. All call sites below are commented out
+// (superseded by relying on digitalWrite()'s own latency to satisfy MAX6921 timing), so this
+// function currently has no effect on the build.
+void inline delayMS(int d) {
+ //for (int i=0;iMAX6921 digitEnablePins:";
+ for (int j=0;j");
+
+ generateBitTable(); //precompute charTable/digitEnableBits/animationMaskBits once, so the ISR (writeDisplay) never has to do this math
+ digitsOnly = false; //this driver can show letters/symbols too (A,P,C,deg,%,I,F...), not just 0-9
+ startTimer(); //arms ESP8266 timer1 and attaches writeDisplay() as its ISR -- multiplexing runs entirely in the background from here on
+}
+
+// Timer1 ISR (ESP8266, TIM_SINGLE mode -- fires once, must be rescheduled by calling
+// timer1_write() again at the very end of every invocation, which is what drives the whole
+// multiplex loop). This implements brightness via a simple two-phase (ON/OFF) time-slicing
+// scheme rather than true PWM: each call toggles `state` between an "ON phase" (tube lit,
+// duration proportional to brightness) and an "OFF phase" (tube dark, remaining time of the
+// PWMrefresh period), i.e. classic duty-cycle dimming spread across one multiplex step.
+// https://circuits4you.com/2018/01/02/esp8266-timer-ticker-example/
+void ICACHE_RAM_ATTR writeDisplay(){ //https://circuits4you.com/2018/01/02/esp8266-timer-ticker-example/
+ static volatile int timer = PWMrefresh;
+ static volatile uint32_t val;
+ static volatile byte pos = 0;
+ static volatile boolean state=true;
+ static volatile int brightness;
+ static int PWMtimeBrightness=PWM_min;
+
+ if (EEPROMsaving) { //stop refresh, while EEPROM write is in progress! (EEPROM writes are blocking/slow, and touching them concurrently with flash-adjacent SPI activity risks corruption)
+ digitalWrite(PIN_BL,HIGH); //OFF -- blank the display so it doesn't hang mid-brightness while the ISR is skipped
+ timer1_write(PWMrefresh); //timer1 (TIM_SINGLE) does NOT auto-repeat, so it must be explicitly rearmed even while "paused" here, or the ISR would never fire again
+ return;
+ }
+
+ intCounter++;
+ brightness = displayON ? prm.dayBright : prm.nightBright; //day/night dimming level, selected by the core's displayON flag
+ if (brightness>MAXBRIGHTNESS) brightness = MAXBRIGHTNESS; //only for safety
+
+ if ((!autoBrightness) && (brightness==MAXBRIGHTNESS)) state = true; //at max fixed brightness, always stay in the "ON" phase (skip the OFF/dimming phase entirely)
+
+ if (state) { //ON state: advance to the next tube position and compute what to shift out
+ pos++; if (pos>maxDigits-1) { //go to the tube#0
+ pos = 0; //go to the first tube
+ // Brightness (PWM ON time) is only recalculated once per full multiplex sweep (at tube#0),
+ // not per-tube, so all tubes share the same duty cycle within one refresh cycle.
+ if (autoBrightness && displayON) { //change brightness only on the tube#0
+ PWMtimeBrightness = max(PWM_min,PWM_max*lx/MAXIMUM_LUX); //ambient-light-driven brightness (auto-dimming), scaled by measured lux vs MAXIMUM_LUX
+ }
+ else
+ PWMtimeBrightness = max(PWM_min,PWM_max*brightness/MAXBRIGHTNESS); //manual brightness setting, scaled 0..MAXBRIGHTNESS into PWM_min..PWM_max ticks
+ }
+ // MAX6921 has no separate "digit select" vs "segment" concept -- both are just output
+ // bits. Combine the bit for this tube's grid/anode-enable output with the bits for the
+ // segments that should light up for the current character into one 20-bit frame.
+ val = (digitEnableBits[pos] | charTable[digit[pos]]); //the full bitmap to send to MAX chip
+ if (digitDP[pos]) val = val | charTable[12]; //Decimal Point (charTable[12] = the DP-only glyph, OR'd in independently of the digit glyph)
+
+ timer = PWMtimeBrightness;
+ #ifdef CLOCK_4
+ if (pos==2) timer = 3*timer; //Weak IV11 tube#2 brightness compensation, some hacking -- this particular physical tube is dimmer than its neighbours, so its ON time is tripled
+ #endif
+ timerON = timer; //exposed for debug/diagnostics only (see ESP32_UniClock2.ino)
+ timerOFF = PWMrefresh-PWMtimeBrightness;
+ }
+ else { //OFF state: nothing to shift out this call, just wait out the rest of the duty cycle
+ timer = PWMrefresh-PWMtimeBrightness;
+ }
+
+ if (timer<500) timer = 500; //safety only... avoid an excessively short/zero timer1 reload that could starve the CPU with ISR re-entry
+
+ if ( (brightness == 0) || (!state) || (!radarON)) { //OFF state, blank digit (radarON gates the whole display off when a presence/radar sensor says nobody is around)
+ digitalWrite(PIN_BL,HIGH); //OFF
+ }
+ else { //ON state
+
+ if (animMask[pos]>0) val &= animationMaskBits[animMask[pos]-1]; //animationMode 6, mask characters from up to down and back -- animMask[] holds a 1-based progressive-reveal stage per tube; the precomputed masks blank segments in a..g order to fake a vertical wipe
+ // Bit-bang 20 bits out MSB-first (OUT19 first). No explicit delay is inserted between
+ // digitalWrite() calls: the GPIO register write + digitalWrite() call overhead on the
+ // ESP8266 already exceeds the MAX6921's minimum DIN setup/CLK pulse-width requirement, so
+ // the commented-out nop/delayMS() calls below are intentionally left disabled.
+ for (int i=0; i<20; i++) {
+ if (val & uint32_t(1 << (19 - i))) {
+ digitalWrite(PIN_DATA, HIGH);
+ //asm volatile ("nop");
+ }
+ else {
+ digitalWrite(PIN_DATA, LOW);
+ //asm volatile ("nop");
+ }
+
+ digitalWrite(PIN_CLK,HIGH); //rising edge shifts the current DATA bit into the MAX6921 internal shift register
+ //asm volatile ("nop"); //delayMS(1);
+ digitalWrite(PIN_CLK,LOW);
+ //asm volatile ("nop"); //delayMS(1);
+ } //end for
+
+ // Latch pulse: transfers the 20-bit shift register into the MAX6921's output latch in one
+ // shot, so the outputs (and thus the tube) change atomically rather than rippling bit by
+ // bit while still shifting (which would show visible garbage/flicker on the tube).
+ digitalWrite(PIN_LE,HIGH ); asm volatile ("nop");
+ digitalWrite(PIN_LE,LOW);
+ digitalWrite(PIN_BL,LOW ); //ON -- unblank now that the new frame is safely latched
+ } //end else
+
+ state = !state;
+ timer1_write(timer); //reschedule this one-shot ISR for the next phase (ON or OFF) -- required every call since timer1 here runs in TIM_SINGLE mode
+}
+
+// One-time setup helper (called from setup_pins()): translates the PCB-specific pin-index
+// tables from clocks.h (segmentEnablePins[], digitEnablePins[]) and the abstract a..g/DP
+// glyph patterns (charDefinition[]) into ready-to-OR 32-bit MAX6921 output bitmaps. Doing
+// this heavy lifting once at boot keeps the writeDisplay() ISR itself lightweight (just a
+// couple of array lookups + OR + bit-shift loop).
+void generateBitTable() {
+uint32_t out;
+
+ DPRINTLN("--- Generating segment pins bitmap ---");
+ for (int i=0;idown->up" reveal
+ // used when a digit changes). Each stage progressively adds more segments in a fixed visual
+ // order (a -> b+f -> g -> e+c -> d), i.e. top segment, then the two middle-height segments,
+ // then the center bar, then the two lower-middle segments, then the bottom segment --
+ // roughly mimicking a wipe sweeping down the 7-segment glyph.
+ animationMaskBits[0] = uint32_t(1<LSB to match that ordering)
+ if ((charDefinition[i] & 1<<(7-j)) != 0) {
+ out = out | segmentEnableBits[j]; //DPRINT("1");
+ }
+ //else DPRINT("0");
+ //DPRINT(" >> ");
+
+ charTable[i] = out; //final per-glyph MAX6921 output bitmap, ready to OR with digitEnableBits[pos] in writeDisplay()
+ //DPRINTLN(charTable[i],BIN);
+ } //end for
+}
+
+
+void clearTubes() {
+ // Only asserts BLANK (forces all MAX6921 outputs off) -- it does NOT clear the shift
+ // register/latch contents or the digit[]/newDigit[] buffers. The stale frame stays latched
+ // in the chip and will reappear instantly the moment BLANK is deasserted again (e.g. next
+ // writeDisplay() ISR tick), so this is a cheap/fast "go dark" rather than a true reset.
+ digitalWrite(PIN_BL,HIGH);
+}
+
+// Intentionally empty: this driver's entire refresh/multiplex logic runs inside the
+// timer1 ISR (writeDisplay()), so there is nothing left for a synchronous single-shot write
+// to do. The function still needs to exist because the core file (ESP32_UniClock2.ino) calls
+// writeDisplaySingle() unconditionally from several places (e.g. after changing a setting).
+void writeDisplaySingle() {}
+#endif
diff --git a/firmware/uniclock/ESP32_UniClock2/MAX6921_ESP32.ino b/firmware/uniclock/ESP32_UniClock2/MAX6921_ESP32.ino
new file mode 100644
index 0000000..9c32b8b
--- /dev/null
+++ b/firmware/uniclock/ESP32_UniClock2/MAX6921_ESP32.ino
@@ -0,0 +1,309 @@
+#ifdef MAX6921_ESP32
+//===========================================================================================
+// Driver for the MAXIM MAX6921 20-output VFD/vacuum-tube driver chip -- ESP32 variant.
+// This is the ESP32 port of MAX6921.ino; the protocol/chip behaviour is identical, only the
+// platform-specific timer/interrupt plumbing differs (ESP32 hardware timer + spinlock instead
+// of ESP8266 timer1; IRAM_ATTR/DRAM_ATTR instead of ICACHE_RAM_ATTR). See MAX6921.ino's header
+// for the full protocol writeup (SPI-like DATA/CLK/LE shift register + BLANK).
+//
+// Hardware / protocol summary:
+// 20-bit serial shift register + latch, MSB (OUT19) first on DATA, one CLK pulse per bit,
+// LE (Latch Enable) pulses HIGH->LOW after the 20 bits to atomically transfer the shift
+// register into the output latch, BL (BLANK, active HIGH) forces all outputs off. Which
+// output pin drives a segment vs. a tube's digit-select grid is purely PCB wiring, described
+// by segmentEnablePins[8]/digitEnablePins[] in clocks.h.
+//
+// Used by (see clocks.h, search for "#define MAX6921_ESP32"):
+// CLOCK_26 - ESP32 D1 mini, UNFI 6x IV-11 VFD tubes clock, SHT21 sensor
+// CLOCK_30 - ESP32, UNFI PCB clock, 6x IV-11 VFD tubes
+// CLOCK_32 - ESP32, UNFI 6x IV-11 VFD tubes clock, DHT22 sensor
+// CLOCK_40 - V1 ESP32, UNFI 6x IV-11 VFD tubes clock
+// CLOCK_41 - V2 ESP32, UNFI 6x IV-11 VFD tubes clock (jumper wire)
+// CLOCK_42 - V3 ESP32, UNFI 6x IV-11 VFD tubes clock
+// CLOCK_47 - V3 ESP32, UNFI 10-LT-50G VFD tubes clock
+// (all ESP32 boards -- the ESP8266 counterpart configs use MAX6921.ino instead, see that
+// file's header for their CLOCK_NN list.)
+//===========================================================================================
+//VFD driver for ESP32
+ char tubeDriver[] = "MAX6921_ESP32";
+ #if defined(ESP32)
+ #else
+ #error "Only ESP32 board is supported by this driver!"
+ #endif
+
+// POSSIBLE BUG (minor / dead code): declared but never assigned or referenced anywhere else in
+// this file (or the project, unlike the same-named `maxDig` pattern used by e.g.
+// multiplex74141_ESP32.ino / Numitron_4511N.ino, where it actually caches maxDigits and is used
+// in the ISR). Here it is vestigial and can be removed; `maxDigits` is used directly instead.
+int DRAM_ATTR maxDig; //memory variable version of maxDigits
+//------------------abcdefgDP---------------- definition of different characters
+// Each byte below is one glyph's segment pattern, bit7..bit0 = a,b,c,d,e,f,g,DP (a=MSB).
+// See generateBitTable(): these abstract a..g/DP bits get remapped through segmentEnablePins[]
+// (the PCB-specific wiring) into the actual MAX6921 OUTx bits, producing charTable[].
+// Indices 0-9 are digits; 10-20 are special glyphs referenced via the digit[] buffer.
+byte DRAM_ATTR charDefinition[] = {
+ B11111100, //0: abcdef
+ B01100000, //1: bc
+ B11011010, //2: abdeg
+ B11110010, //3: abcdg
+ B01100110, //4: bcfg
+ B10110110, //5: acdfg
+ B10111110, //6: acdefg
+ B11100000, //7: abc
+ B11111110, //8: abcdefg
+ B11110110, //9: abcdfg
+ B00000000, // : BLANK (10)
+ B00000010, //-: minus (11)
+ B00000001, // decimal point (12)
+ B11101110, // A abcefg (13)
+ B11001110, // P abefg (14)
+ B10011100, // C adef (15)
+ B11000110, //grad (upper circle) abfg (16)
+ B10110100, //% acdf (17)
+ B00111010, //lower circle cdeg (18)
+ B01100000, //I bc (19)
+ B10001110 //F aefg (20)
+};
+
+uint32_t DRAM_ATTR animationMaskBits[5]; //precomputed AND-masks for the "wipe" transition animation (animMask[]), see generateBitTable(). DRAM_ATTR pins these in internal RAM so the IRAM-resident ISR can access them without touching flash-cached memory.
+
+// NOTE: MAXCHARS/MAXSEGMENTS/maxDigits rely on sizeof() over `byte` arrays (1 byte/element),
+// so sizeof() == element count here. segmentEnablePins[] must always have exactly 8 entries
+// (a,b,c,d,e,f,g,DP) per the clocks.h convention; digitEnablePins[] length = number of tubes.
+#define MAXCHARS sizeof(charDefinition)
+#define MAXSEGMENTS sizeof(segmentEnablePins)
+int maxDigits = sizeof(digitEnablePins);
+
+uint32_t DRAM_ATTR charTable[MAXCHARS]; //generated pin table from segmentDefinitions
+uint32_t DRAM_ATTR segmentEnableBits[MAXSEGMENTS]; //bitmaps, generated from EnablePins tables
+// FIXED (was fixed-size [10] regardless of the actual maxDigits): sized to BUFSIZE (12, see the
+// digit[]/newDigit[]/animMask[] buffers in ESP32_UniClock2.ino), the same ceiling the rest of the
+// codebase uses for tube-position buffers, so this can no longer silently overflow in
+// generateBitTable() for any config the system otherwise supports.
+uint32_t DRAM_ATTR digitEnableBits[12];
+
+// Multiplex/brightness timing. Despite the variable names, these are NOT milliseconds -- they
+// are raw ESP32 hardware-timer tick counts (see startTimer() in ESP32_UniClock2.ino: PRESCALER=15
+// divides the 80MHz APB clock down to roughly a few MHz, so each tick is on the order of 0.2us).
+// So PWMrefresh=5500 ticks is roughly ~1ms per ON/OFF half-cycle, PWM_min/PWM_max similarly small.
+// Greater PWMrefresh => slower multiplex/refresh frequency (more visible flicker risk).
+// All DRAM_ATTR here for the same reason as above: read from inside an IRAM-resident ISR.
+int DRAM_ATTR PWMrefresh=5500; //Multiplex time period, in timer ticks (not milliseconds, despite the variable name -- see note above). Greater value => slower multiplex frequency
+int DRAM_ATTR PWM_min = 500;
+int DRAM_ATTR PWM_max = 5000;
+//int DRAM_ATTR PWMtiming[11] = {0,500,800,1200,2000,2500,3000,3500,4000,4500,5000};
+
+//-----------------------------------------------------------------------------------------
+
+//https://sub.nanona.fi/esp8266/timing-and-ticks.html
+//One tick is 1us / 80 = 0.0125us = 12.5ns on 80MHz
+//One tick is 1us / 80 = 0.0125us = 6.25ns on 160MHz
+// 1 NOP is 1 tick
+
+// Dead/no-op helper: `d` is ignored, so this always executes exactly three NOPs regardless of
+// the requested delay. Not called anywhere in this file (see the commented-out delayMS(1) call
+// sites in writeDisplay() below) -- relies instead on digitalWrite()'s own call overhead to
+// satisfy MAX6921 minimum pulse-width timing.
+void inline delayMS(int d) {
+ asm volatile ("nop");
+ asm volatile ("nop");
+ asm volatile ("nop");
+}
+
+
+void setup_pins() {
+ DPRINTLN("VFD Clock - setup MAX6921 pins");
+ pinMode(PIN_LE, OUTPUT); regPin(PIN_LE,"PIN_LE");
+ pinMode(PIN_BL, OUTPUT); regPin(PIN_BL,"PIN_BL");
+ digitalWrite(PIN_BL,LOW); //brightness
+ pinMode(PIN_DATA,OUTPUT); regPin(PIN_DATA,"PIN_DATA");
+ pinMode(PIN_CLK, OUTPUT); regPin(PIN_CLK,"PIN_CLK");
+
+ // Build a human-readable dump of the pin mapping for the web UI's diagnostics page (not
+ // used by the driver logic itself).
+ driverSetupStr = "MAX6921 segmentEnablePins:";
+ for (int j=0;jMAX6921 digitEnablePins:";
+ for (int j=0;j");
+
+ generateBitTable(); //precompute charTable/digitEnableBits/animationMaskBits once, so the ISR (writeDisplay) never has to do this math
+ digitsOnly = false; //this driver can show letters/symbols too (A,P,C,deg,%,I,F...), not just 0-9
+ startTimer(); //arms the ESP32 hardware timer and attaches writeDisplay() as its ISR -- multiplexing runs entirely in the background from here on
+}
+
+// Hardware-timer ISR (ESP32). Unlike the ESP8266 timer1 (one-shot, must be manually
+// rearmed every call), timerAlarmWrite(..., true) below enables auto-reload, so the timer
+// keeps firing periodically on its own; the repeated timerAlarmWrite()/timerAlarmEnable()
+// calls at the end of every invocation just update the *next* period's length (for the
+// following ON or OFF phase) rather than being strictly required to keep it running.
+// Implements brightness via a simple two-phase (ON/OFF) time-slicing scheme rather than true
+// PWM: each call toggles `state` between an "ON phase" (tube lit, duration proportional to
+// brightness) and an "OFF phase" (tube dark, remaining time of the PWMrefresh period).
+void IRAM_ATTR writeDisplay(){ //void IRAM_ATTR writeDisplay(){
+ static DRAM_ATTR int timer = PWMrefresh;
+ static DRAM_ATTR uint32_t val;
+ static DRAM_ATTR byte pos = 0;
+ static DRAM_ATTR boolean state=true;
+ static DRAM_ATTR int brightness;
+ static DRAM_ATTR int PWMtimeBrightness=PWM_min;
+
+ if (EEPROMsaving) { //stop refresh, while EEPROM write is in progress! (EEPROM writes are blocking/slow, and touching them concurrently with flash-adjacent SPI activity risks corruption)
+ //digitalWrite(PIN_BL,HIGH); //OFF
+ // NOTE: unlike MAX6921.ino (ESP8266), BL is NOT forced HIGH here, so the display is left
+ // showing whatever frame was last latched (frozen, not blanked) for the duration of the
+ // EEPROM write. Also, no timerAlarmWrite()/timerAlarmEnable() call is needed here (unlike
+ // the ESP8266 timer1_write() call in the sibling file) because this hardware timer's
+ // auto-reload keeps it firing on its own even while this ISR body returns early.
+ return;
+ }
+
+ // portENTER_CRITICAL_ISR + noInterrupts()/interrupts() together ensure this whole bit-bang
+ // sequence (and the shared `val`/`pos`/`state` statics) cannot be interrupted or preempted
+ // by the other CPU core or a higher-priority ISR mid-frame, which would otherwise risk
+ // shifting out a half-updated/torn frame to the MAX6921.
+ portENTER_CRITICAL_ISR(&timerMux);
+ noInterrupts();
+ intCounter++;
+
+ brightness = displayON ? prm.dayBright : prm.nightBright; //day/night dimming level, selected by the core's displayON flag
+ if (brightness>MAXBRIGHTNESS) brightness = MAXBRIGHTNESS; //only for safety
+
+ if ((!autoBrightness) && (brightness==MAXBRIGHTNESS)) state = true; //at max fixed brightness, always stay in the "ON" phase (skip the OFF/dimming phase entirely)
+
+ if (state) { //ON state: advance to the next tube position and compute what to shift out
+ pos++; if (pos>maxDigits-1) { //go to the tube#0
+ pos = 0;
+ // Brightness (PWM ON time) is only recalculated once per full multiplex sweep (at tube#0),
+ // not per-tube, so all tubes share the same duty cycle within one refresh cycle.
+ if (autoBrightness && displayON) { //change brightness only on the tube#0
+ PWMtimeBrightness = max(PWM_min,PWM_max*lx/MAXIMUM_LUX); //ambient-light-driven brightness (auto-dimming), scaled by measured lux vs MAXIMUM_LUX
+ }
+ else
+ PWMtimeBrightness = max(PWM_min,PWM_max*brightness/MAXBRIGHTNESS); //manual brightness setting, scaled 0..MAXBRIGHTNESS into PWM_min..PWM_max ticks
+ }
+
+ // MAX6921 has no separate "digit select" vs "segment" concept -- both are just output
+ // bits. Combine the bit for this tube's grid/anode-enable output with the bits for the
+ // segments that should light up for the current character into one 20-bit frame.
+ val = (digitEnableBits[pos] | charTable[digit[pos]]); //the full bitmap to send to MAX chip
+ if (digitDP[pos]) val = val | charTable[12]; //Decimal Point (charTable[12] = the DP-only glyph, OR'd in independently of the digit glyph)
+
+ timer = PWMtimeBrightness;
+ //if (pos==2) timer = 3*timer; //Weak IV11 tube#2 brightness compensation -- see the CLOCK_4-guarded equivalent in MAX6921.ino; no ESP32 clocks.h config currently needs this hack, left disabled here
+ timerON = timer; //exposed for debug/diagnostics only (see ESP32_UniClock2.ino)
+ timerOFF = PWMrefresh-PWMtimeBrightness;
+ }
+ else { //OFF state: nothing to shift out this call, just wait out the rest of the duty cycle
+ timer = PWMrefresh-PWMtimeBrightness;
+ }
+
+ if (timer<500) timer = 500; //safety only... avoid an excessively short/zero timer reload that could starve the CPU with ISR re-entry
+
+ if ( (brightness == 0) || (!state) || (!radarON)) { //OFF state, blank digit (radarON gates the whole display off when a presence/radar sensor says nobody is around)
+ digitalWrite(PIN_BL,HIGH); //OFF
+ }
+ else { //ON state
+ if (animMask[pos]>0) val &= animationMaskBits[animMask[pos]-1]; //animationMode 6, mask characters from up to down and back -- animMask[] holds a 1-based progressive-reveal stage per tube; the precomputed masks blank segments in a..g order to fake a vertical wipe
+ // Bit-bang 20 bits out MSB-first (OUT19 first). No explicit delay is inserted between
+ // digitalWrite() calls: the GPIO register write + digitalWrite() call overhead already
+ // exceeds the MAX6921's minimum DIN setup/CLK pulse-width requirement, so the
+ // commented-out nop/delayMS() calls below are intentionally left disabled.
+ for (int i=0; i<20; i++) {
+ if (val & uint32_t(1 << (19 - i)))
+ {digitalWrite(PIN_DATA, HIGH); //asm volatile ("nop");
+ }
+ else
+ {digitalWrite(PIN_DATA, LOW); //asm volatile ("nop");
+ }
+
+ digitalWrite(PIN_CLK,HIGH); //asm volatile ("nop"); //delayMS(1); //rising edge shifts the current DATA bit into the MAX6921 internal shift register
+ digitalWrite(PIN_CLK,LOW); //asm volatile ("nop"); //delayMS(1);
+ } //end for
+
+ // Latch pulse: transfers the 20-bit shift register into the MAX6921's output latch in one
+ // shot, so the outputs (and thus the tube) change atomically rather than rippling bit by
+ // bit while still shifting (which would show visible garbage/flicker on the tube).
+ digitalWrite(PIN_LE,HIGH ); asm volatile ("nop");
+ digitalWrite(PIN_LE,LOW);
+ digitalWrite(PIN_BL,LOW ); //ON -- unblank now that the new frame is safely latched
+ } //end else
+
+ portEXIT_CRITICAL_ISR(&timerMux);
+ state = !state;
+ //ESP32timer = timerBegin(0, PRESCALER, true); //set prescaler, true = edge generated signal
+ //timerAttachInterrupt(ESP32timer, &writeDisplay, true);
+ timerAlarmWrite(ESP32timer, timer, true); //schedule the next phase's (ON or OFF) duration; `true` keeps auto-reload enabled
+ timerAlarmEnable(ESP32timer);
+ interrupts();
+}
+
+// One-time setup helper (called from setup_pins()): translates the PCB-specific pin-index
+// tables from clocks.h (segmentEnablePins[], digitEnablePins[]) and the abstract a..g/DP
+// glyph patterns (charDefinition[]) into ready-to-OR 32-bit MAX6921 output bitmaps. Doing
+// this heavy lifting once at boot keeps the writeDisplay() ISR itself lightweight (just a
+// couple of array lookups + OR + bit-shift loop).
+void generateBitTable() {
+uint32_t out;
+
+ DPRINTLN("--- Generating segment pins bitmap ---");
+ for (int i=0;idown->up" reveal
+ // used when a digit changes). Each stage progressively adds more segments in a fixed visual
+ // order (a -> b+f -> g -> e+c -> d), i.e. top segment, then the two middle-height segments,
+ // then the center bar, then the two lower-middle segments, then the bottom segment --
+ // roughly mimicking a wipe sweeping down the 7-segment glyph.
+ animationMaskBits[0] = uint32_t(1<LSB to match that ordering)
+ if ((charDefinition[i] & 1<<(7-j)) != 0) {
+ out = out | segmentEnableBits[j]; //DPRINT("1");
+ }
+ //else DPRINT("0");
+ //DPRINT(" >> ");
+
+ charTable[i] = out; //final per-glyph MAX6921 output bitmap, ready to OR with digitEnableBits[pos] in writeDisplay()
+ //DPRINTLN(charTable[i],BIN);
+ } //end for
+}
+
+
+void clearTubes() {
+ // Only asserts BLANK (forces all MAX6921 outputs off) -- it does NOT clear the shift
+ // register/latch contents or the digit[]/newDigit[] buffers. The stale frame stays latched
+ // in the chip and will reappear instantly the moment BLANK is deasserted again (e.g. next
+ // writeDisplay() ISR tick), so this is a cheap/fast "go dark" rather than a true reset.
+ digitalWrite(PIN_BL,HIGH);
+}
+
+// Intentionally empty: this driver's entire refresh/multiplex logic runs inside the
+// hardware-timer ISR (writeDisplay()), so there is nothing left for a synchronous single-shot
+// write to do. The function still needs to exist because the core file (ESP32_UniClock2.ino)
+// calls writeDisplaySingle() unconditionally from several places (e.g. after changing a setting).
+void writeDisplaySingle() {}
+#endif
diff --git a/firmware/uniclock/ESP32_UniClock2/MAX7219CNG.ino b/firmware/uniclock/ESP32_UniClock2/MAX7219CNG.ino
new file mode 100644
index 0000000..d9b647b
--- /dev/null
+++ b/firmware/uniclock/ESP32_UniClock2/MAX7219CNG.ino
@@ -0,0 +1,158 @@
+/*==============================================================================
+ MAX7219CNG.ino - driver for the MAX7219 (or compatible, e.g. MAX7221/CNG clone)
+ LED display driver IC.
+
+ Hardware: unlike the MM5450 driver in this project, the MAX7219 has its OWN
+ internal digit scan/multiplexing (up to 8 digits) and its own PWM intensity
+ control (REG_INTENSITY, 16 steps), so this driver does NOT use the periodic
+ hardware-timer ISR / startTimer() at all - there is no writeDisplay() ISR
+ here, only a blocking writeDisplaySingle() that pushes one full frame (all
+ digits) whenever the displayed content changes. The chip keeps refreshing
+ the LEDs by itself between calls.
+
+ Wiring/protocol: standard MAX72xx 3-wire serial bus (DIN/CLK/LOAD-CS), bit-
+ banged with shiftOut() (MSBFIRST) rather than hardware SPI. Each 16-bit
+ transfer is {8-bit register/digit address, 8-bit data}, latched into the
+ chip's internal register on the LOAD/CS_ rising edge. This driver runs the
+ chip in "No-Decode" mode (REG_DECODE=0) so it can drive arbitrary custom
+ 7-segment patterns (including the extra glyphs A,P,C,deg,%,I,F) instead of
+ the chip's built-in BCD/font decoder, which only knows digits 0-9 and a few
+ symbols.
+
+ Used by (see clocks.h): CLOCK_10 - "8266 LED Clock with MAX7219 chip"
+ (clocks.h defines MAX7219CNG only inside the #ifdef CLOCK_10 block).
+==============================================================================*/
+#ifdef MAX7219CNG
+char tubeDriver[] = "MAX7219CNG";
+int maxDigits = 6;
+byte tubes[] = {0,1,2,3,4,5}; //change it, if needed for the correct tube sequence
+// tubes[i] maps logical digit position i (0..maxDigits-1, left to right as wired to the
+// MAX7219's DIG0..DIG7 addresses) to the index into the shared digit[]/digitDP[] buffers -
+// lets the PCB wire digits in any physical order without changing the core clock logic.
+byte brightConvert[] = {0,1,2,3,4,6,8,10,12,14,15}; //convert brightness from 0..10 to 0..15
+// Non-linear lookup table: the firmware-wide brightness scale is 0..10 (MAXBRIGHTNESS=10
+// for CLOCK_10), but REG_INTENSITY wants 0..15 - this table also biases the mapping
+// (skipping value 5/7/etc.) so perceived brightness steps feel more even than a plain x1.5 scale.
+
+//MAX7219CNG control pins
+#define PIN_LOAD 15 // D8 LOAD/CS_
+#define PIN_CLK 14 // D5 Clock
+#define PIN_DIN 13 // D7 DataIN
+
+//MAX7219 Registers (see MAX7219/MAX7221 datasheet "Table 2. Register Address Map")
+#define REG_SHUTDOWN 0x0C // 0 = shutdown (blank, low power), 1 = normal operation
+#define REG_SCANLIMIT 0x0B // how many digits (0..7, i.e. count-1) the chip actively multiplexes
+#define REG_DECODE 0x09 // per-digit BCD-decode enable bitmask; 0 = No-Decode (raw segment bits, used here)
+#define REG_INTENSITY 0x0A // global PWM duty cycle for all digits, 0x00 (dimmest) .. 0x0F (brightest)
+#define REG_TEST 0x0F // 1 = display-test mode (forces all segments on at full brightness), 0 = normal
+
+//------------------abcdefgDP---------------- definition of different characters
+byte charDefinition[] = {
+ B11111100, //0: abcdef
+ B01100000, //1: bc
+ B11011010, //2: abdeg
+ B11110010, //3: abcdg
+ B01100110, //4: bcfg
+ B10110110, //5: acdfg
+ B10111110, //6: acdefg
+ B11100000, //7: abc
+ B11111110, //8: abcdefg
+ B11110110, //9: abcdfg
+ B00000000, // : BLANK (10)
+ B00000010, //-: minus (11)
+ B00000001, // decimal point (12)
+ B11101110, // A abcefg (13)
+ B11001110, // P abefg (14)
+ B10011100, // C adef (15)
+ B11000110, //grad (upper circle) abfg (16)
+ B10110100, //% acdf (17)
+ B00111010, //lower circle cdeg (18)
+ B01100000, //I bc (19)
+ B10001110 //F aefg (20)
+};
+
+//Fill this table with the output-bit numbers of the MAX7219 chip! Only No-Decode mode is used, for flexible hardware
+// segmentEnablePins[j] = which bit position (0..7) within the MAX7219's per-digit data
+// byte drives logical segment j (a,b,c,d,e,f,g,DP) - lets the segment-to-pin wiring on the
+// PCB differ from the charDefinition[] bit order without touching the font table itself.
+byte segmentEnablePins[] = {6,5,4,3,2,1,0,7}; //segment enable bits of MAX7219 (a,b,c,d,e,f,g,DP) (You MUST always define 8 bits!!!)
+
+#define MAXCHARS sizeof(charDefinition) // (currently unused in this file - kept for reference/consistency with other drivers)
+#define MAXSEGMENTS 8 // (currently unused in this file)
+
+// Sends one 16-bit MAX7219 command: {address (register or digit 1..8), data byte}.
+// LOAD/CS_ must be held LOW for the whole 16-bit transfer and pulsed HIGH afterward to
+// latch it - the chip ignores clock edges while LOAD is high, so partial/torn writes
+// are not possible even though this is a blocking (non-ISR) call.
+void ICACHE_RAM_ATTR sendBits(byte address,byte val){
+ digitalWrite(PIN_LOAD, LOW);
+ shiftOut(PIN_DIN, PIN_CLK, MSBFIRST, address);
+ shiftOut(PIN_DIN, PIN_CLK, MSBFIRST, val);
+ digitalWrite(PIN_LOAD, HIGH);
+ delay(3); // conservative settle/latch delay; MAX7219 datasheet only requires ~tCSW ~ 50ns,
+ // so this is far more generous than strictly needed (see optimization note in report)
+ //DPRINT(address,HEX); DPRINT(","); DPRINTLN(val,HEX);
+}
+
+void setup_pins() {
+#if defined(ESP8266)
+#else
+ #error "Only 8266 Board is supported!"
+#endif
+
+ DPRINTLN("MAX7219 Clock - setup pins...");
+ pinMode(PIN_LOAD,OUTPUT); regPin(PIN_LOAD,"PIN_LOAD");
+ pinMode(PIN_DIN, OUTPUT); regPin(PIN_DIN,"PIN_DIN");
+ pinMode(PIN_CLK, OUTPUT); regPin(PIN_CLK,"PIN_CLK");
+ digitsOnly = false; // this driver's charDefinition[] also supports letters (A,P,C,deg,%,I,F) besides 0-9
+ // Note: no startTimer() call here - this driver relies on the MAX7219's own internal
+ // digit multiplexing, so no software multiplex/PWM ISR is needed for this chip.
+
+ sendBits(REG_SHUTDOWN,1); //Set to Normal (not Shutdown) mode
+ sendBits(REG_TEST,0); //Set to Normal, (Non-Test) mode
+ sendBits(REG_SCANLIMIT,maxDigits-1); //Set scanlimit (chip multiplexes digits 0..maxDigits-1 only)
+ sendBits(REG_DECODE,0 ); //Set Non-Decode Mode - segment data is sent as raw bit patterns, not BCD
+ sendBits(REG_INTENSITY,0x0F); //Set maximum brightness (actual runtime brightness is set per-frame in writeDisplaySingle())
+}
+
+// Blocking, one-shot push of the ENTIRE current frame (all digits + brightness) to the
+// MAX7219. Called directly from the main sketch (not from an ISR - despite the
+// ICACHE_RAM_ATTR tag, likely inherited from other drivers' ISR-based writeDisplay()
+// pattern; harmless here, just places the function in IRAM instead of flash).
+// Because the MAX7219 keeps displaying the last-latched frame by itself (on-chip
+// multiplexing), this only needs to run when digit[]/digitDP[]/brightness actually change -
+// there is no periodic refresh ISR for this driver (see setup_pins() note).
+void ICACHE_RAM_ATTR writeDisplaySingle() {
+static byte oldBright = 0;
+static byte newBright = 0;
+int bitBuffer;
+
+//https://sub.nanona.fi/esp8266/timing-and-ticks.html
+//One tick is 1us / 80 = 0.0125us = 12.5ns on 80MHz
+//One tick is 1us / 80 = 0.0125us = 6.25ns on 160MHz
+// 1 NOP is 1 tick
+// (NOP-timing reference is a leftover from the bit-banged drivers - this file uses
+// shiftOut()/delay() instead and doesn't rely on cycle-counted NOPs)
+
+
+ newBright = brightConvert[displayON ? prm.dayBright : prm.nightBright]; //convert 0..10 brightness levels to 0..15
+ if (!radarON) newBright = 0; // no presence detected -> force display off (still fully addressed/latched, just at 0 intensity)
+ if (newBright != oldBright) { // only touch the INTENSITY register when the value actually changed, to save a 16-bit transfer + 3ms delay
+ sendBits(REG_INTENSITY, newBright); //Set Brightness
+ oldBright = newBright;
+ }
+ for (int i=0;i MM5450 output pin number (1..34).
+// Digit groups 0,1 belong to display "side" 0, groups 2,3 belong to "side" 1
+// (see sideEnablePins[] and the side-switching logic in writeDisplay()).
+byte segmentEnablePins[4][8] = {
+ {16, 15, 14, 13, 12, 11, 10, 9}, //segment enable pins of MM5450 (a,b,c,d,e,f,g,DP)
+ {8, 7, 6, 5, 4, 3, 2, 1},
+ {17, 18, 19, 20, 21, 22, 23, 24},
+ {25, 26, 27, 28, 29, 30, 31, 32}
+};
+
+
+/* Old version - kept for reference only, NOT compiled (different pin-to-output mapping,
+ presumably from an earlier PCB revision with different MM5450 output wiring).
+ byte segmentEnablePins[4][8] = {
+ {25,26,27,28,29,30,31,32}, //segment enable pins of MM5450 (a,b,c,d,e,f,g,DP)
+ {17,18,19,20,21,22,23,24},
+ {9,10,11,12,13,14,15,16},
+ {1,2,3,4,5,6,7,8}
+ };
+*/
+
+
+byte sideEnablePins[] = {34, 33}; //enable pins for the two groups of 4 digits (2x4 LEDs)
+// sideEnablePins[0] = MM5450 output 34 = enable line for "side 0" (digit groups 0,1)
+// sideEnablePins[1] = MM5450 output 33 = enable line for "side 1" (digit groups 2,3)
+// Only one side's enable line is driven HIGH at a time -> the two 4-digit
+// groups share the same 32 segment lines and are multiplexed in time.
+
+//MM5450 control pins (GPIO numbers - see clocks.h D5pin/D6pin/etc for the NodeMCU Dx aliases:
+// GPIO14=D5, GPIO12=D6, GPIO13=D7, GPIO15=D8)
+#define PIN_LE 14 // D5 Shift Register Latch Enable
+#define PIN_CLK 12 // D6 Shift Register Clock
+#define PIN_DATA 13 // D7 Shift Register Data
+#define PIN_BR 15 // D8 Shift Register Brightness (chip-enable/blank output, active HIGH)
+
+// Bitmasks for the same pins, used with WRITE_PERI_REG(PIN_OUT_SET/CLEAR, ...) for fast
+// direct GPIO register access inside the ISR (digitalWrite() is too slow/has too much
+// call overhead to meet the tight bit-banging timing below).
+#define PIN_LE_BIT 1<<14 // D5 Shift Register Latch Enable
+#define PIN_CLK_BIT 1<<12 // D6 Shift Register Clock
+#define PIN_DATA_BIT 1<<13 // D7 Shift Register Data
+#define PIN_BR_BIT 1<<15 // D8 Shift Register Brightness
+
+//------------------abcdefgDP---------------- definition of different characters
+byte charDefinition[] = {
+ B11111100, //0: abcdef
+ B01100000, //1: bc
+ B11011010, //2: abdeg
+ B11110010, //3: abcdg
+ B01100110, //4: bcfg
+ B10110110, //5: acdfg
+ B10111110, //6: acdefg
+ B11100000, //7: abc
+ B11111110, //8: abcdefg
+ B11110110, //9: abcdfg
+ B00000000, // : BLANK (10)
+ B00000010, //-: minus (11)
+ B00000001, // decimal point (12)
+ B11101110, // A abcefg (13)
+ B11001110, // P abefg (14)
+ B10011100, // C adef (15)
+ B11000110, //grad (upper circle) abfg (16)
+ B10110100, //% acdf (17)
+ B00111010, //lower circle cdeg (18)
+ B01100000, //I bc (19)
+ B10001110 //F aefg (20)
+};
+
+#define MAXCHARS sizeof(charDefinition) // (currently unused in this file - kept for reference/consistency with other drivers)
+
+int PWMrefresh = 10000; //usec, Multiplex time period (one full ON+OFF brightness cycle). Greater value => slower multiplex/PWM frequency
+// PWM_min/PWM_max bound the ON-time portion of PWMrefresh: since the MM5450 has no
+// hardware intensity register, brightness is entirely software PWM - writeDisplay()
+// re-arms the hw timer with a shorter or longer "ON" interval depending on the
+// requested brightness (see PWMtimeBrightness below).
+int PWM_min = 500;
+int PWM_max = 10000;
+//int PWMtiming[11] = {0,500,800,1200,2000,2500,3000,4500,6000,8000,10000};
+
+// bitBuffer holds the full 34-bit MM5450 serial frame plus its leading start bit
+// (bitBuffer[0] = start bit, bitBuffer[1..34] = one byte per output pin, 0/1 = LED off/on).
+// One array element is used per bit (not packed) to make indexing by MM5450 pin number
+// trivial for writeBits(); size 36 leaves a little headroom beyond the 35 bits actually sent.
+byte bitBuffer[36];
+
+void setup_pins() {
+#if defined(ESP8266)
+#else
+#error "Only 8266 Board is supported!"
+#endif
+
+ DPRINTLN("MM5450 - Setup pins...");
+ pinMode(PIN_LE, OUTPUT); regPin(PIN_LE, "PIN_LE");
+ pinMode(PIN_BR, OUTPUT); regPin(PIN_BR, "PIN_BR");
+ pinMode(PIN_DATA, OUTPUT); regPin(PIN_DATA, "PIN_DATA");
+ pinMode(PIN_CLK, OUTPUT); regPin(PIN_CLK, "PIN_CLK");
+ digitalWrite(PIN_BR, HIGH); //brightness
+
+ digitsOnly = false; // this driver's charDefinition[] also supports letters (A,P,C,deg,%,I,F) besides 0-9
+ startTimer(); // arms the periodic hw timer -> writeDisplay() becomes the multiplex/PWM ISR
+
+}
+
+// Bit-bangs one full 35-bit frame (start bit + 34 output bits) into the MM5450 shift
+// register and latches it. Runs inside the timer ISR (called from writeDisplay()), so it
+// uses direct GPIO SET/CLEAR peripheral-register writes instead of digitalWrite() - the
+// latter's call/pin-lookup overhead would blow the tight clock pulse widths needed here.
+void ICACHE_RAM_ATTR writeBits() {
+ //https://sub.nanona.fi/esp8266/timing-and-ticks.html
+ //One tick is 1us / 80 = 0.0125us = 12.5ns on 80MHz
+ //One tick is 1us / 80 = 0.0125us = 6.25ns on 160MHz
+ // 1 NOP is 1 tick
+
+ //noInterrupts(); // not needed: this code already runs inside the timer ISR itself
+ WRITE_PERI_REG( PIN_OUT_CLEAR, PIN_LE_BIT ); //DATA_ENABLE - LE must be LOW while shifting data in
+ for (int t = 0; t < 5; t++) asm volatile ("nop"); //100nsec
+
+ for (int i = 0; i < 36; i++) { // shift bitBuffer out LSB-index-first (index 0 = start bit, sent first)
+ if (bitBuffer[i]) {
+ WRITE_PERI_REG( PIN_OUT_SET, PIN_DATA_BIT ); //digitalWrite(PIN_DATA,HIGH);
+ }
+ else {
+ WRITE_PERI_REG( PIN_OUT_CLEAR, PIN_DATA_BIT ); //digitalWrite(PIN_DATA,LOW);
+ }
+
+ for (int t = 0; t < 12; t++) asm volatile ("nop"); //300nsec - data setup time before clock edge
+
+ WRITE_PERI_REG( PIN_OUT_SET, PIN_CLK_BIT ); // MM5450 samples DATA on this rising CLK edge
+ for (int t = 0; t < 12; t++) asm volatile ("nop"); //300nsec
+ WRITE_PERI_REG( PIN_OUT_CLEAR, PIN_CLK_BIT );
+ for (int t = 0; t < 12; t++) asm volatile ("nop"); //300nsec
+ } //end for
+
+ WRITE_PERI_REG( PIN_OUT_SET, PIN_LE_BIT ); // rising edge on LE latches the shifted-in frame to the outputs
+ //interrupts();
+}
+
+// Main multiplex/PWM ISR, attached to the ESP8266 hw timer1 by startTimer().
+// timer1 is configured TIM_SINGLE (one-shot), so this handler must re-arm itself via
+// timer1_write() at the very end of every call - the display refresh rate/PWM duty
+// cycle is entirely driven by how long each re-armed interval is (see 'timer' below).
+// Each call does ONE of: (a) blank the display for the computed OFF duration (brightness
+// PWM low phase), or (b) blank for the ON duration but first shift out a fresh frame for
+// whichever "side" (group of 4 digits) is up next, alternating sides on every ON call.
+void ICACHE_RAM_ATTR writeDisplay() { // Writes to the MM5450 driver for LEDS
+ static volatile int timer = PWMrefresh;
+ static volatile boolean state = true; // true = ON phase of the PWM cycle, false = OFF phase
+ static volatile int brightness;
+ static int PWMtimeBrightness;
+ static volatile uint32_t val;
+ static volatile byte pos = 0;
+ static byte side = 0;
+ byte num = 0;
+
+ if (EEPROMsaving) { //stop refresh, while EEPROM write is in progress!
+ // Flash writes on ESP8266 briefly disable interrupts/stall the CPU; touching the
+ // display bus during that window could corrupt the SPI-like bit-bang timing, so we
+ // just keep re-arming the timer with a fixed period and skip the actual refresh.
+ timer1_write(PWMrefresh);
+ return;
+ }
+
+ intCounter++;
+ brightness = displayON ? prm.dayBright : prm.nightBright;
+ if (brightness > MAXBRIGHTNESS) brightness = MAXBRIGHTNESS; //only for safety
+
+ if (autoBrightness && displayON) {
+ // Ambient-light (lx) driven brightness: brighter room -> longer ON time within PWMrefresh
+ PWMtimeBrightness = max(PWM_min, PWM_max * lx / MAXIMUM_LUX);
+ }
+ else
+ // Manual brightness setting (0..MAXBRIGHTNESS) mapped linearly onto the ON-time window
+ PWMtimeBrightness = max(PWM_min, PWM_max * brightness / MAXBRIGHTNESS);
+
+ if ((!autoBrightness) && (brightness == MAXBRIGHTNESS))
+ // At max manual brightness PWMtimeBrightness == PWMrefresh, so the OFF phase would be
+ // ~0us; forcing state=true here keeps the ISR permanently in the ON branch instead of
+ // wastefully toggling into a near-zero-length OFF phase every other call.
+ state = true;
+
+ if (state) { //ON state
+ timer = PWMtimeBrightness;
+ timerON = timer;
+ timerOFF = PWMrefresh - PWMtimeBrightness;
+ }
+ else { //OFF state
+ timer = PWMrefresh - PWMtimeBrightness;
+ }
+ if (timer < 500) timer = 500; //safety only... (avoid re-arming the hw timer with too short/zero an interval)
+
+ if ( (brightness == 0) || (!state) || (!radarON)) { //OFF state, blank digit
+ WRITE_PERI_REG( PIN_OUT_CLEAR, PIN_BR_BIT ); //OFF - BR pin blanks all MM5450 outputs regardless of shift-register content
+ // radarON reflects presence-detection (radar/PIR): when nobody is around the display
+ // is blanked here (power saving / tube-life) without touching the multiplex state.
+ }
+ else { //ON state
+ WRITE_PERI_REG( PIN_OUT_SET, PIN_BR_BIT ); //ON
+
+ memset(bitBuffer, 0, sizeof(bitBuffer)); //clear array
+ bitBuffer[0] = 1; //starting bit required to frame the 34 data bits (see MM5450 datasheet)
+
+ bitBuffer[sideEnablePins[side]] = 0;
+ writeBits(); // 1st transfer: push an all-blank frame (except start bit) so BOTH sides are
+ // momentarily off before the new side's segments are shifted in - avoids
+ // ghosting/cross-talk between the outgoing and incoming digit group.
+ for (int t = 0; t < 100; t++) asm volatile ("nop"); //clear display and wait between switching the two sides
+ bitBuffer[sideEnablePins[side]] = 0; //disable old side..
+ // NOTE: bitBuffer was just memset to 0 a few lines above and this index was already
+ // cleared before the previous writeBits() call, so this re-assignment is a redundant
+ // no-op (POSSIBLE dead/duplicated code - likely meant to clear something else, e.g.
+ // the *other* side, but 'side' has not been toggled yet at this point).
+
+ if (side == 0) side = 1; else side = 0; //change side...
+
+ for (int i = 0; i < 4; i++) {
+ pos = 7 - (4 * side + i);
+ // POSSIBLE BUG: pos always ranges 0..7 (8 digit positions) regardless of maxDigits
+ // (=6 above). digit[6] and digit[7] are never written by the core display-update
+ // logic when maxDigits==6, and being plain (non-initialized-to-BLANK) globals they
+ // default to 0 -> segment pattern for '0'. If the physical PCB for CLOCK_11 really
+ // only has 6 digits wired, positions 6/7 are presumably just unused MM5450 outputs
+ // (harmless); if it has 8 digits, the top 2 would never be updated by the clock logic.
+ num = digit[pos];
+ //if (animMask[pos]>0) val &= animationMaskBits[animMask[pos]-1]; //animationMode 6, mask characters from up to down and back
+ // (wipe/mask animation support - referenced 'val' and 'animationMaskBits' are unused/
+ // undefined in this file, so this feature is not implemented for the MM5450 driver)
+ for (int j = 0; j <= 7; j++) //from a to g
+ if ((charDefinition[num] & 1 << (7 - j)) != 0) {
+ bitBuffer[segmentEnablePins[i][j]] = 1;
+ }
+ if (digitDP[pos]) bitBuffer[segmentEnablePins[i][7]] = 1; //Decimal Point
+ } //end for
+
+ bitBuffer[sideEnablePins[side]] = 1; //enable new side
+ writeBits(); // 2nd transfer: actual segment data + new side's enable bit
+ } //end else ON
+
+ state = !state;
+ timer1_write(timer); // re-arm the one-shot hw timer for the next ON or OFF phase
+}
+
+
+// Both required by the common driver interface but intentionally no-ops here: this driver
+// does all display updates from the writeDisplay() ISR (brightness PWM + side multiplex
+// depend on continuous timer-driven refresh), so there is nothing useful for a one-shot
+// blank (clearTubes) or a one-shot blocking full-frame write (writeDisplaySingle) to do -
+// the next ISR tick will simply pick up the current digit[]/digitDP[] contents.
+void clearTubes() {}
+void writeDisplaySingle() {}
+#endif
diff --git a/firmware/uniclock/ESP32_UniClock2/Numitron_4511N.ino b/firmware/uniclock/ESP32_UniClock2/Numitron_4511N.ino
new file mode 100644
index 0000000..86691e3
--- /dev/null
+++ b/firmware/uniclock/ESP32_UniClock2/Numitron_4511N.ino
@@ -0,0 +1,354 @@
+// ============================================================================
+// Numitron_4511N driver
+// ----------------------------------------------------------------------------
+// Drives IV-16 (and similar) Numitron filament (resistive, incandescent-style
+// 7-segment) tubes through one CD4511/74HC4511 BCD-to-7-segment latch/decoder
+// chip PER TUBE. Numitron tubes are NOT gas-discharge (like Nixies) or
+// vacuum-fluorescent (like VFDs): each segment is a tiny resistive filament,
+// so "brightness" here is achieved purely by PWM-blanking the whole panel's
+// power rail (via LTBIpin), not by varying per-segment current the way a
+// current-driven display might.
+//
+// Wiring / protocol:
+// - ABCDPins[4] : 4 GPIOs = the shared BCD data bus (bits A..D),
+// wired in parallel to the BCD inputs of every
+// 4511 chip on the panel (one chip per digit).
+// - digitEnablePins[] : one GPIO per digit/tube position, each wired to
+// the LE (Latch Enable) pin of that digit's own
+// 4511. Because the BCD bus is shared, only ONE
+// digit's latch may be allowed to "see" the bus
+// at a time while the bus is being changed - see
+// the per-chip strobe sequence in
+// writeDisplaySingle() below.
+// - LTBIpin : "Lamp Test / Blanking Input" - shared across the
+// whole panel. Driving it LOW blanks all digits
+// instantly regardless of latched BCD data; this
+// is exploited as a cheap panel-wide PWM dimming
+// switch (see writeDisplay()) instead of touching
+// each digit's latch content.
+// - PCB_VERSION : identifies which physical GP Numitron carrier PCB
+// revision (1, 3, or 32) the code is running on;
+// see the panelVersion note near the ESP8266
+// writeDisplay() below - currently this only
+// affects code that is commented out, so on the
+// live code path PCB_VERSION has no effect.
+//
+// Used by (see clocks.h, search "Numitron_4511N"):
+// - CLOCK_8 : ESP8266, GP PCB v3, 4x IV-16 Numitron tubes (plexi box)
+// - CLOCK_9 : ESP8266, GP PCB v1, 4x IV-16 Numitron tubes (brown box)
+// - CLOCK_13 : ESP8266, GP PCB v1/v3, 4x IV-16 Numitron tubes + GPS timesync
+// - CLOCK_46 : ESP32, UNFI board, 6x IV-9 Numitron tubes
+// ============================================================================
+
+#ifdef Numitron_4511N
+char tubeDriver[] = "Numitron_4511N";
+//used by GP Numitron v3 panel
+
+// digitEnablePins[], ABCDPins[] and LTBIpin are actually #defined/declared
+// per-clock in clocks.h (see CLOCK_8/9/13/46 blocks) - the lines below are
+// only a commented-out reference/example of what those definitions look like.
+//#define LTBIpin 5
+//byte digitEnablePins[] = {13,12,14,16}; //define here the digit enable pins from 4 to 8
+//byte ABCDPins[4] = {4,0,2,15};
+
+// digitEnablePins[] is a byte array, so sizeof() here correctly yields the
+// element count (number of tube positions), not a byte size in a larger type.
+int maxDigits = sizeof(digitEnablePins);
+
+// NOTE: unlike position-multiplexed displays, this driver does not cycle
+// through digit positions in the timer ISR (see writeDisplay() below) - all
+// digits are latched simultaneously and stay lit continuously. PWMrefresh/
+// PWM_min/PWM_max instead define a slow (sub-kHz) panel-wide ON/OFF blanking
+// cycle (via LTBIpin) used purely for brightness dimming, not for scanning
+// digits. DRAM_ATTR pins these vars in internal RAM on ESP32 so the ISR
+// (running from IRAM) can access them without touching flash-backed memory.
+#if defined(ESP32)
+ int DRAM_ATTR PWMrefresh=5500; //msec, Multiplex time period. Greater value => slower multiplex frequency
+ //int DRAM_ATTR PWMtiming[11] = {0,500,800,1200,2000,2500,3000,3500,4000,4500,5000};
+ int DRAM_ATTR PWM_min = 500;
+ int DRAM_ATTR PWM_max = 5000;
+ int DRAM_ATTR maxDig=maxDigits;
+#else
+ int PWMrefresh=5500; //msec, Multiplex time period. Greater value => slower multiplex frequency
+ int PWM_min = 500;
+ int PWM_max = 5000;
+ int maxDig=maxDigits;
+ //int PWMtiming[11] = {0,500,800,1200,2000,2500,3000,3500,4000,4500,5000};
+#endif
+
+void setup_pins() {
+ char tmp[30];
+
+ DPRINTLN("Numitron Clock - setup pins");
+ delay(1000);
+ pinMode(LTBIpin, OUTPUT); regPin(LTBIpin,"LTBIpin");
+ digitalWrite(LTBIpin,HIGH); //panel enabled (not blanked) by default until the first PWM tick decides otherwise
+ for (int i=0;iMAXBRIGHTNESS) brightness = MAXBRIGHTNESS; //only for safety
+
+ if (autoBrightness && (displayON )) {
+ // Ambient-light (lux) driven brightness: scale the ON-time fraction of the
+ // PWMrefresh period by measured lux against MAXIMUM_LUX, clamped to PWM_min
+ // so the panel is never driven at a 0% duty cycle (would look "dead").
+ PWMtimeBrightness = max(PWM_min,PWM_max*lx/MAXIMUM_LUX);
+ }
+ else
+ PWMtimeBrightness = max(PWM_min,PWM_max*brightness/MAXBRIGHTNESS);
+
+ if ((!autoBrightness) && (brightness==MAXBRIGHTNESS))
+ state = true; // at full fixed brightness, force the next phase to start as ON so the panel doesn't sit in a leftover OFF phase
+
+ if (state) { //ON state
+ timer = PWMtimeBrightness;
+ timerON = timer;
+ timerOFF = PWMrefresh-PWMtimeBrightness;
+ }
+ else { //OFF state
+ timer = PWMrefresh-PWMtimeBrightness;
+ }
+ if (timer<500) timer = 500; //safety only... avoids re-arming the hardware timer with too short/zero a period, which could stall the ISR cadence
+ if ( (brightness == 0) || (!state) || (!radarON)) { //OFF state, blank digit
+ digitalWrite(LTBIpin,LOW); //disable display -- global blank: LTBIpin overrides all latched BCD data on every 4511 at once, regardless of what's latched
+ #if COLON_PIN >=0
+ digitalWrite(COLON_PIN,LOW); // Blink colon pin
+ #endif
+ }
+ else { //ON state
+ digitalWrite(LTBIpin,HIGH); //enable display
+ #if COLON_PIN >=0
+ digitalWrite(COLON_PIN,colonBlinkState);
+ #endif
+ }
+ state = !state; //flip ON/OFF phase for the next ISR invocation
+ portEXIT_CRITICAL_ISR(&timerMux);
+ timerAlarmWrite(ESP32timer, timer, true); //re-arm with the just-computed (variable) period - this is what makes the PWM period self-scheduling instead of a fixed tick
+ timerAlarmEnable(ESP32timer);
+ interrupts();
+}
+
+// Blocking, one-shot write of the whole digit[] buffer into the tube latches.
+// Because ABCDPins is a single bus shared by every digit's 4511, digits must
+// be written one at a time: pulse that digit's own LE line while the bus
+// carries its value, so only that digit's latch captures it.
+void IRAM_ATTR writeDisplaySingle() {
+ int num;
+
+ // FIXED (was "pos<=maxDig", an off-by-one that read digitEnablePins[maxDig] - one element past
+ // the end of that array, from clocks.h - and called digitalWrite() with whatever garbage byte
+ // happened to sit there in memory). Valid indices are 0..maxDig-1 (maxDig == maxDigits == the
+ // actual array length, e.g. 4 or 6).
+ for (int pos=0; pos drive ABCD with the new value -> LE LOW (latch
+ // it in, freezing it against subsequent bus changes for the next digit).
+ // This code instead does LE LOW -> drive ABCD -> LE HIGH, which leaves
+ // every already-processed digit's latch in the *transparent* state, so it
+ // may keep following the shared bus as later positions are written -
+ // potentially causing all/most tubes to end up showing the last digit's
+ // value, or visible cross-talk between positions. If this matches the
+ // actual physical panel behavior it may be intentional/compensated in
+ // hardware, but it contradicts the datasheet's normal usage - flagging
+ // for hardware verification rather than "fixing" blind.
+ digitalWrite(digitEnablePins[pos],LOW); //latch enable
+ for (int j=0;j<4;j++) {digitalWrite(ABCDPins[j],num & (1<=3 (only v3 panels apparently supported blanking this way).
+// It also multiplexed the decimal-point/dot output (dpCounter cycling
+// through digitDP[]) and inverted the colon blink polarity relative to the
+// active implementation below. All of this was superseded by the simpler
+// LTBIpin-based panel-wide PWM blanking used today, which is why dpCounter,
+// brightCounter, DP_PIN and digitDP[] are effectively unused by this driver
+// now - decimal points/dots are NOT currently displayed by Numitron_4511N.
+/*
+ if (((panelVersion>=3) && ((displayON ? prm.dayBright : prm.nightBright)MAXBRIGHTNESS) brightCounter = 1;
+ if (brightCounter == 1) {
+ dpCounter++; if (dpCounter>maxDig) dpCounter = 0;
+ #if defined(DP_PIN) && (DP_PIN>=0)
+ digitalWrite(DP_PIN,digitDP[dpCounter]);
+ #endif
+ }
+ #if COLON_PIN >=0
+ digitalWrite(COLON_PIN,!colonBlinkState); // Blink colon pin
+ #endif
+*/
+
+ // Same panel-wide PWM-blanking scheme as the ESP32 writeDisplay() above:
+ // compute the ON-time (PWMtimeBrightness) out of the PWMrefresh period from
+ // either lux (auto) or the configured day/night brightness, then toggle
+ // LTBIpin and reschedule the ESP8266 hardware timer (timer1) for the next
+ // phase's duration. See the ESP32 branch for the fuller rationale.
+ brightness = displayON ? prm.dayBright : prm.nightBright;
+ if (brightness>MAXBRIGHTNESS) brightness = MAXBRIGHTNESS; //only for safety
+
+ if (autoBrightness && (displayON )) {
+ PWMtimeBrightness = max(PWM_min,PWM_max*lx/MAXIMUM_LUX);
+ }
+ else
+ PWMtimeBrightness = max(PWM_min,PWM_max*brightness/MAXBRIGHTNESS);
+
+ if ((!autoBrightness) && (brightness==MAXBRIGHTNESS))
+ state = true;
+
+ if (state) { //ON state
+ timer = PWMtimeBrightness;
+ timerON = timer;
+ timerOFF = PWMrefresh-PWMtimeBrightness;
+ }
+ else { //OFF state
+ timer = PWMrefresh-PWMtimeBrightness;
+ }
+ if (timer<500) timer = 500; //safety only... avoid a near-zero/zero timer1 reload value
+ if ( (brightness == 0) || (!state) || (!radarON)) { //OFF state, blank digit
+ digitalWrite(LTBIpin,LOW); //disable display
+ #if COLON_PIN >=0
+ digitalWrite(COLON_PIN,LOW); // Blink colon pin
+ #endif
+ }
+ else { //ON state
+ digitalWrite(LTBIpin,HIGH); //enable display
+ #if COLON_PIN >=0
+ digitalWrite(COLON_PIN,colonBlinkState);
+ #endif
+ }
+ state = !state;
+
+ timer1_write(timer); //ESP8266 hardware timer1 reload, analogous to timerAlarmWrite() on the ESP32 branch
+}
+
+// Blocking, one-shot write of the whole digit[] buffer into the tube latches
+// (ESP8266 twin of the ESP32 writeDisplaySingle() above - see that copy for
+// the detailed strobe-order/latch-semantics discussion, which applies here
+// identically).
+void writeDisplaySingle() {
+ int num;
+ // FIXED: same off-by-one as the ESP32 writeDisplaySingle() above was ("pos<=maxDig", one element
+ // past the end of digitEnablePins[]) - see that copy for the detailed explanation.
+ for (int pos=0; posset bus->HIGH leaves the latch transparent instead of latching
+ // it) - see the detailed comment in the ESP32 writeDisplaySingle().
+ digitalWrite(digitEnablePins[pos],LOW); //latch enable
+ for (int j=0;j<4;j++) {digitalWrite(ABCDPins[j],num & (1< */
+ 0b10010100000000111, /* ? */
+ 0b00000101011110111, /* @ */
+ 0b01000100011001111, /* A */
+ 0b00010101000111111, /* B */
+ 0b00000000011110011, /* C */
+ 0b00010001000111111, /* D */
+ 0b01000000011110011, /* E */
+ 0b01000000011000011, /* F */
+ 0b00000100011111011, /* G */
+ 0b01000100011001100, /* H */
+ 0b00010001000110011, /* I */
+ 0b00000000001111100, /* J */
+ 0b01001010011000000, /* K */
+ 0b00000000011110000, /* L */
+ 0b00000010111001100, /* M */
+ 0b00001000111001100, /* N */
+ 0b00000000011111111, /* O */
+ 0b01000100011000111, /* P */
+ 0b00001000011111111, /* Q */
+ 0b01001100011000111, /* R */
+ 0b01000100010111011, /* S */
+ 0b00010001000000011, /* T */
+ 0b00000000011111100, /* U */
+ 0b00100010011000000, /* V */
+ 0b00101000011001100, /* W */
+ 0b00101010100000000, /* X */
+ 0b01000100010111100, /* Y */
+ 0b00100010000110011, /* Z */
+ 0b00010001000010010, /* [ */
+ 0b00001000100000000, /* \ */
+ 0b00010001000100001, /* ] */
+ 0b00101000000000000, /* ^ */
+ 0b00000000000110000, /* _ */
+ 0b00000000100000000, /* ` */
+ 0b01010000001110000, /* a */
+ 0b01010000011100000, /* b */
+ 0b01000000001100000, /* c */
+ 0b00010100000011100, /* d */
+ 0b01100000001100000, /* e */
+ 0b01010101000000010, /* f */
+ 0b01010001010100001, /* g */
+ 0b01010000011000000, /* h */
+ 0b00010000000000000, /* i */
+ 0b00010001001100000, /* j */
+ 0b00011011000000000, /* k */
+ 0b00000000011000000, /* l */
+ 0b01010100001001000, /* m */
+ 0b01010000001000000, /* n */
+ 0b01010000001100000, /* o */
+ 0b01000001011000001, /* p */
+ 0b01010001010000001, /* q */
+ 0b01000000001000000, /* r */
+ 0b01010000010100001, /* s */
+ 0b01000000011100000, /* t */
+ 0b00010000001100000, /* u */
+ 0b00100000001000000, /* v */
+ 0b00101000001001000, /* w */
+ 0b00101010100000000, /* x */
+ 0b00000101000011100, /* y */
+ 0b01100000000100000, /* z */
+ 0b01010001000010010, /* { */
+ 0b00010001000000000, /* | */
+ 0b00010101000100001, /* } */
+ 0b01100110000000000, /* ~ */
+ 0b00000000000000000, /* (del) */
+};
+
+ //byte segmentEnablePins[] = {16,7,0,1,2,3,4,5,6,8,9,11,10,12,13,14,15}; //segment enable bits
+#endif
+
+
+// Maps the special glyph codes (BLANK, minus, DP, A,P,C,deg,%,circle,I,F -- digit[] values
+// 10..20) onto printable ASCII characters, used only when ASCIImode is true (16-segment
+// branch) so the digit[i] index can be turned into an ASCII char before it is used to look up
+// charDefinition[]. Note this string is 1:1 positional with the 10..20 glyph codes, i.e.
+// asciiConvert[0] corresponds to digit value 0 ('0'), asciiConvert[10] (' ') to BLANK, etc.
+char asciiConvert[] = "0123456789 -.APC~%oIF";
+
+#define MAXCHARS sizeof(charDefinition)/sizeof(charDefinition[0])
+
+// Bit-bangs one byte out on PIN_DIN/PIN_CLK. NOTE: LSB-first (i starts at 0, tests bit 1<10) newBright = 10; //clamp: DISPLAY_MODE[] only has 11 entries (indices 0..10)
+
+ digitalWrite(PIN_LOAD, LOW); //safety only
+ delayMicroseconds(2);
+
+ // C2_DATA_SETTING switches the chip into "write display data / auto-increment address"
+ // mode; DISPLAY_MODE[newBright] is then a second command byte (not display data) setting
+ // brightness/on-off -- both are sent back-to-back inside the same PIN_LOAD-low window
+ // opened just above.
+ sendCommand(C2_DATA_SETTING);
+ sendCommand(DISPLAY_MODE[newBright]);
+
+for (int i=0;i= sizeof(asciiConvert), or dispChar==21
+ // landing on the string's NUL terminator) - that produced a negative index into
+ // charDefinition[] a few lines down (out-of-bounds read). Now the subtraction only happens
+ // together with a successful lookup; anything unmapped falls back to charDefinition[0], the
+ // blank/space glyph, instead of indexing out of bounds.
+ if (dispChar blank glyph (charDefinition[0])
+ }
+ }
+ bitBuffer = 0;
+ digitalWrite(PIN_LOAD,LOW);
+ delayMicroseconds(2);
+ // Address increments by 3 per digit (not 1) -- the PT6311 reserves 3 RAM bytes per digit
+ // position internally (enough for up to 24 segment bits), even though this build only
+ // uses 1-2 of them (MAXSEGMENTS is 8 or 17, see charDefinition table above).
+ shift(C3_ADDRESS_SETTING + i*3); //set display address
+ for (int j=0;j>8);
+ if (MAXSEGMENTS>16) shift(bitBuffer >>16);
+ delayMicroseconds(2);
+ digitalWrite(PIN_LOAD,HIGH);
+ delayMicroseconds(2);
+ } //end for i
+
+/* //TEST mode
+ digitalWrite(PIN_LOAD, LOW); //upload data
+ delayMicroseconds(2);
+ shift(C3_ADDRESS_SETTING);
+ for (int i=0;i<48;i++) {
+ shift(0xff); // Data to fill table 5*16 = 80 bits
+ //shift(0x00); // Data to fill table 5*16 = 80 bits
+ //shift(0xff); // Data to fill table 5*16 = 80 bits
+ }
+ digitalWrite(PIN_LOAD, HIGH);
+ delay(2);
+*/
+}
+
+/*********************** Send command without strobe/chip select******************************************/
+
+// No-op: this driver has no timer-ISR multiplex loop (see file header) -- the PT6311 refreshes
+// its own tubes internally, so there is nothing for the periodic writeDisplay() ISR hook to do.
+void writeDisplay() {}
+// clearTubes() is a required part of the common driver interface, but nothing in the core .ino
+// calls it any more (its only former caller, disableDisplay(), has been removed entirely as dead
+// code) - kept as a no-op stub purely for interface compliance. If a synchronous immediate-blank is
+// ever needed again, this would have to actively send an all-blank frame instead.
+void clearTubes() {}
+#endif
diff --git a/firmware/uniclock/ESP32_UniClock2/PT6355.ino b/firmware/uniclock/ESP32_UniClock2/PT6355.ino
new file mode 100644
index 0000000..ac63d14
--- /dev/null
+++ b/firmware/uniclock/ESP32_UniClock2/PT6355.ino
@@ -0,0 +1,354 @@
+#ifdef PT6355
+//===========================================================================================
+// Driver for the Princeton Technology PT6355 VFD controller/driver IC.
+//
+// Hardware / protocol:
+// Like the PT6311 (see PT6311.ino), the PT6355 is a "smart" VFD grid/segment controller
+// with its own display RAM and internal multiplex/scan generator -- the MCU only pushes a
+// full frame after something changes, the chip keeps scanning the tubes on its own. That is
+// why writeDisplay() (the timer-ISR multiplex hook used by "dumb" shift-register drivers)
+// is an empty stub here and setup_pins() never calls startTimer(): all display updates go
+// through the blocking writeDisplaySingle() instead of an ISR-driven refresh loop.
+// 3-wire serial bus (similar to SPI, chip-specific framing):
+// PIN_CS - LOW for the duration of one whole command or one whole per-frame data burst
+// (see startBits()/stopBits()), HIGH otherwise. Unlike the PT6311 driver, this
+// chip's frame is NOT re-strobed byte-by-byte: PIN_CS stays low across the
+// entire multi-digit data burst in writeDisplaySingle().
+// PIN_CLK - one pulse per bit.
+// PIN_SIN - serial data, LSB-first per byte (see shift(): loop starts at bit 0).
+// Note this file structurally mirrors PT6311.ino very closely (same DISPLAY_MODE-less
+// brightness idea, same 8-segment/16-segment charDefinition tables, same ASCIImode /
+// asciiConvert[] translation) even though the two chips are different parts with different
+// command sets and bus framing -- treat any future edits to the shared-looking tables in
+// one file as NOT automatically applicable to the other without checking its own datasheet.
+//
+// Used by (see clocks.h, search for "#define PT6355"):
+// CLOCK_12 - ESP8266 VFD clock with PT6355 chip
+//===========================================================================================
+char tubeDriver[] = "PT6355";
+//int maxDigits = 6;
+int wt = 5; //Serial timing
+
+//#define PIN_CS 22 //12 or 15 // D6 CS_
+//#define PIN_CLK 16 // D7 Clock
+//#define PIN_SIN 21 // D5 DataIN
+
+//#define SEGMENT8
+//#define TESTMODE //for testing the segments of a new type VFD module
+
+//PT6355 Commands
+#define C1_DISPLAY_STATE_SETTING B11000000 // Display duty setting=15 + Display ON
+#define C3_PORT_DATA_SETTINGS B10000000 //Port selection + output data
+
+#ifdef SEGMENT8
+//------------------abcdefgDP---------------- definition of different characters
+// C0 selects "10 grids, <=16 segments/grid" data-write mode; C2 sets which physical output
+// pin the grid (digit-select) scan starts from (D17..D7 range) -- both are chip-mode setup
+// commands, sent once per full frame in writeDisplaySingle(), not per digit.
+#define C0_DISPLAY_DATA_SETTING B11101100 //grid=10, segments<=16
+#define C2_GRID_SELECTION B10101000 //Grid start pin setting D17..D7
+// 8-bit segment mask per glyph, MSB-first as a,b,c,d,e,f,g,DP (bit7=a .. bit0=DP). Indices
+// 0-9 = digits, 10 = blank, 11 = minus, 12 = decimal point, 13+ = extra glyphs (A,P,C,deg,%,
+// lower-circle,I,F) -- matches the digit[]/newDigit[] encoding used by the core firmware, so
+// digit[] values index straight into this table (see also the identical table in PT6311.ino).
+#define MAXSEGMENTS 8
+boolean ASCIImode = false;
+byte charDefinition[] = {
+ B11111100, //0: abcdef
+ B01100000, //1: bc
+ B11011010, //2: abdeg
+ B11110010, //3: abcdg
+ B01100110, //4: bcfg
+ B10110110, //5: acdfg
+ B10111110, //6: acdefg
+ B11100000, //7: abc
+ B11111110, //8: abcdefg
+ B11110110, //9: abcdfg
+ B00000000, // : BLANK (10)
+ B00000010, //-: minus (11)
+ B00000001, // decimal point (12)
+ B11101110, // A abcefg (13)
+ B11001110, // P abefg (14)
+ B10011100, // C adef (15)
+ B11000110, //grad (upper circle) abfg (16)
+ B10110100, //% acdf (17)
+ B00111010, //lower circle cdeg (18)
+ B01100000, //I bc (19)
+ B10001110 //F aefg (20)
+};
+
+//byte segmentEnablePins[] = {9,8,5,3,4,7,6,2}; //segment enable bits (a,b,c,d,e,f,g,DP) (You MUST define always 8 bits!!!)
+
+#else //16 segment + DP ---------------------------------------------------------------------------------------------
+#define C0_DISPLAY_DATA_SETTING B11101111 //grid=10, segments>17
+#define C2_GRID_SELECTION B10101000 //Grid start pin setting D17..D7
+#define MAXSEGMENTS 17
+boolean ASCIImode = true;
+// Full 16-segment-star + DP font, indexed by ASCII code - 32 (index 0 = space/0x20). digit[]
+// values are translated through asciiConvert[] first (see below) since they are not ASCII.
+uint32_t charDefinition[96] = {
+ 0b00000000000000000, /* (space) */
+ 0b10000000000001100, /* ! */
+ 0b00000001000000100, /* " */ // 100000001
+ 0b01010101000111100, /* # */
+ 0b01010101010111011, /* $ */
+ 0b01110111010011001, /* % */
+ 0b01001001101110001, /* & */
+ 0b00000001000000000, /* ' */
+ 0b00001010000000000, /* ( */
+ 0b00100000100000000, /* ) */
+ 0b01111111100000000, /* * */
+ 0b01010101000000000, /* + */
+ 0b00100000000000000, /* , */
+ 0b01000100000000000, /* - */
+ 0b10000000000000000, /* . */
+ 0b00100010000000000, /* / */
+ 0b00100010011111111, /* 0 */
+ 0b00000010000001100, /* 1 */
+ 0b01000100001110111, /* 2 */
+ 0b00000100000111111, /* 3 */
+ 0b01000100010001100, /* 4 */
+ 0b01001000010110011, /* 5 */
+ 0b01000100011111011, /* 6 */
+ 0b00000000000001111, /* 7 */
+ 0b01000100011111111, /* 8 */
+ 0b01000100010111111, /* 9 */
+ 0b00010001000000000, /* : */
+ 0b00100001000000000, /* ; */
+ 0b01001010000000000, /* < */
+ 0b01000100000110000, /* = */
+ 0b00100100100000000, /* > */
+ 0b10010100000000111, /* ? */
+ 0b00000101011110111, /* @ */
+ 0b01000100011001111, /* A */
+ 0b00010101000111111, /* B */
+ 0b00000000011110011, /* C */
+ 0b00010001000111111, /* D */
+ 0b01000000011110011, /* E */
+ 0b01000000011000011, /* F */
+ 0b00000100011111011, /* G */
+ 0b01000100011001100, /* H */
+ 0b00010001000110011, /* I */
+ 0b00000000001111100, /* J */
+ 0b01001010011000000, /* K */
+ 0b00000000011110000, /* L */
+ 0b00000010111001100, /* M */
+ 0b00001000111001100, /* N */
+ 0b00000000011111111, /* O */
+ 0b01000100011000111, /* P */
+ 0b00001000011111111, /* Q */
+ 0b01001100011000111, /* R */
+ 0b01000100010111011, /* S */
+ 0b00010001000000011, /* T */
+ 0b00000000011111100, /* U */
+ 0b00100010011000000, /* V */
+ 0b00101000011001100, /* W */
+ 0b00101010100000000, /* X */
+ 0b01000100010111100, /* Y */
+ 0b00100010000110011, /* Z */
+ 0b00010001000010010, /* [ */
+ 0b00001000100000000, /* \ */
+ 0b00010001000100001, /* ] */
+ 0b00101000000000000, /* ^ */
+ 0b00000000000110000, /* _ */
+ 0b00000000100000000, /* ` */
+ 0b01010000001110000, /* a */
+ 0b01010000011100000, /* b */
+ 0b01000000001100000, /* c */
+ 0b00010100000011100, /* d */
+ 0b01100000001100000, /* e */
+ 0b01010101000000010, /* f */
+ 0b01010001010100001, /* g */
+ 0b01010000011000000, /* h */
+ 0b00010000000000000, /* i */
+ 0b00010001001100000, /* j */
+ 0b00011011000000000, /* k */
+ 0b00000000011000000, /* l */
+ 0b01010100001001000, /* m */
+ 0b01010000001000000, /* n */
+ 0b01010000001100000, /* o */
+ 0b01000001011000001, /* p */
+ 0b01010001010000001, /* q */
+ 0b01000000001000000, /* r */
+ 0b01010000010100001, /* s */
+ 0b01000000011100000, /* t */
+ 0b00010000001100000, /* u */
+ 0b00100000001000000, /* v */
+ 0b00101000001001000, /* w */
+ 0b00101010100000000, /* x */
+ 0b00000101000011100, /* y */
+ 0b01100000000100000, /* z */
+ 0b01010001000010010, /* { */
+ 0b00010001000000000, /* | */
+ 0b00010101000100001, /* } */
+ 0b01100110000000000, /* ~ */
+ 0b00000000000000000, /* (del) */
+};
+
+ //byte segmentEnablePins[] = {16,7,0,1,2,3,4,5,6,8,9,11,10,12,13,14,15}; //segment enable bits
+#endif
+
+
+// Maps the special glyph codes (BLANK, minus, DP, A,P,C,deg,%,circle,I,F -- digit[] values
+// 10..20) onto printable ASCII characters, used only when ASCIImode is true (16-segment
+// branch), positionally 1:1 with those glyph codes (asciiConvert[10]==' '==BLANK, etc.) --
+// see the fuller explanation of this same table in PT6311.ino.
+char asciiConvert[] = "0123456789 -.APC~%oIF";
+
+#define MAXCHARS sizeof(charDefinition)/sizeof(charDefinition[0])
+
+// PIN_CS framing helpers: unlike PT6311 (which re-strobes its STB pin every single byte),
+// this driver holds PIN_CS low for an entire command or an entire frame -- callers bracket
+// one or more shift() calls between startBits()/stopBits() themselves (see sendCommand() and
+// writeDisplaySingle() below, where the whole per-frame digit loop sits inside one CS-low
+// window).
+void inline startBits(byte CS) {
+ digitalWrite(CS, LOW);
+ delayMicroseconds(wt);
+}
+
+void inline stopBits(byte CS) {
+ delayMicroseconds(wt);
+ digitalWrite(CS, HIGH);
+ delayMicroseconds(wt);
+}
+
+// Bit-bangs one byte out on SIN/CLK. NOTE: LSB-first (i starts at 0, tests bit 1<5. So the effective brightness only has
+ // three distinct hardware states (off / low duty / high duty) even though prm.dayBright /
+ // prm.nightBright can range 0..10. This may be a deliberate simplification for this
+ // chip's duty-setting bits, but it has not been verified against the PT6355 datasheet.
+ dispState = C1_DISPLAY_STATE_SETTING;
+ if (newBright>0) dispState |= 1;
+ if (newBright>5) dispState |= 6;
+ sendCommand(dispState); //Set Brightness
+ oldBright = newBright;
+ }
+ startBits(PIN_CS);
+ // Unlike PT6311 (which addresses each digit individually via C3_ADDRESS_SETTING + i*3 and
+ // re-strobes CS per digit), this driver sends ONE C0_DISPLAY_DATA_SETTING command and then
+ // streams all maxDigits worth of segment data continuously while CS stays low the whole
+ // time (stopBits() only happens once, after the loop) -- the chip auto-advances through its
+ // internal grid RAM as bytes arrive.
+ shift(PIN_SIN, PIN_CLK, C0_DISPLAY_DATA_SETTING);
+ for (int i=0;i=
+ // sizeof(asciiConvert), or dispChar==21 landing on the string's NUL terminator), producing
+ // a negative index into charDefinition[] below. Now the subtraction only happens together
+ // with a successful lookup; anything unmapped falls back to charDefinition[0] (blank glyph).
+ if (dispChar blank glyph (charDefinition[0])
+ }
+ }
+ bitBuffer = 0;
+ for (int j=0;j>8);
+ if (MAXSEGMENTS>16) shift(PIN_SIN, PIN_CLK, bitBuffer >>16);
+ } //end for i
+ stopBits(PIN_CS);
+ count++;
+}
+
+// No-op: this driver has no timer-ISR multiplex loop (see file header) -- the PT6355 refreshes
+// its own tubes internally, so there is nothing for the periodic writeDisplay() ISR hook to do.
+void writeDisplay() {
+}
+
+// clearTubes() is a required part of the common driver interface, but nothing in the core .ino
+// calls it any more (its only former caller, disableDisplay(), has been removed entirely as dead
+// code) - kept as a no-op stub purely for interface compliance.
+void clearTubes() {}
+#endif
diff --git a/firmware/uniclock/ESP32_UniClock2/SN75512.ino b/firmware/uniclock/ESP32_UniClock2/SN75512.ino
new file mode 100644
index 0000000..f96f228
--- /dev/null
+++ b/firmware/uniclock/ESP32_UniClock2/SN75512.ino
@@ -0,0 +1,251 @@
+#ifdef SN75512
+//=============================================================================================
+// Driver for VFD (Vacuum Fluorescent Display) tubes driven by 2x cascaded Texas Instruments
+// SN75512 serial-in / high-voltage parallel-out shift-register + segment/grid driver ICs.
+//
+// Wiring / protocol:
+// - DATA/CLK: plain serial shift-register bit-banging, MSB first (see writeDisplay()).
+// - The two SN75512 chips are daisy-chained (12 outputs each) into one 24-bit (DATABITS)
+// shift register: shift all 24 bits in, then pulse LE (Latch Enable) once to transfer the
+// shifted-in bits to the chips' parallel high-voltage outputs (glitch-free update - the
+// outputs only change on the LE pulse, not while shifting).
+// - STROBE is the chip's display blank/enable input (per the pin comment below: 1=display
+// OFF, 0=display ON) and is toggled once per digit refresh to implement brightness dimming
+// (see the brightCounter[] dithering trick in writeDisplay()).
+// - Platform: ESP8266 only - uses direct WRITE_PERI_REG() GPIO register access instead of
+// digitalWrite() because bit-banging 24 bits per digit, at the multiplex refresh rate,
+// needs to be much faster than the Arduino digitalWrite() call overhead allows.
+//
+// NOTE: no CLOCK_NN block currently active in clocks.h defines SN75512 (grep of clocks.h for
+// "SN75512" / "#define SN75512" comes up empty) - it only appears as a commented-out option in
+// the driver-selection list in ESP32_UniClock2.ino. This driver therefore currently looks like
+// a legacy/orphan module not wired to any active clock configuration.
+//=============================================================================================
+// 2 x SN75512
+char tubeDriver[] = "SN75512";
+#define VFDrefresh 1200 //msec, Multiplex time period. Greater value => slower multiplex frequency
+ //NOTE: comment says "msec" but this value is passed straight to timer1_write() (ESP8266
+ //hw timer1 in TIM_DIV16/TIM_EDGE/TIM_SINGLE mode, i.e. ~5MHz tick rate => ~0.2us/tick,
+ //see startTimer() in ESP32_UniClock2.ino) - so the real unit is timer ticks (~0.2us
+ //each), not milliseconds. The label is misleading but functionally harmless.
+
+#define DATABITS 24 //total length of the shift registers (2 x 12-bit SN75512, daisy-chained)
+
+//Fill this table with the bit positions of the shift registers!
+//Bit position = index (0-based) of that output within the 24-bit combined (2-chip) shift
+//register, in the order the bits are shifted out (see writeDisplay(): bit DATABITS-1 goes
+//out first). segmentEnablePins order must match the a,b,c,d,e,f,g,DP order of charDefinition[].
+byte segmentEnablePins[] = {16,17,18,19,20,21,22,23}; //segment enable bits (a,b,c,d,e,f,g,DP) (You MUST define always 8 bits!!!)
+byte digitEnablePins[] = {0,1,2,3,4,5,6,7,8}; //digit enable bits (You may define any number of digits between 1 and 10 )
+
+//SN75512 control pins
+#define PIN_LE 13 // D7 Shift Register Latch Enable
+#define PIN_CLK 12 // D6 Shift Register Clock
+#define PIN_DATA 14 // D5 Shift Register Data
+#define PIN_STROBE 5 // D1 Shift Register Strobe (1=display off 0=display on)
+
+//Precomputed GPIO bitmasks (1< shift-register-bit-position map.
+//Indices 0-9 = digits, 10=BLANK, 11=minus, 12=decimal point, 13-20 = extra glyphs
+//(A,P,C,deg,%,lower-circle,I,F) - these indices must line up with the digit[]/newDigit[]
+//buffer value convention documented in the core .ino file.
+byte charDefinition[] = {
+ B11111100, //0: abcdef
+ B01100000, //1: bc
+ B11011010, //2: abdeg
+ B11110010, //3: abcdg
+ B01100110, //4: bcfg
+ B10110110, //5: acdfg
+ B10111110, //6: acdefg
+ B11100000, //7: abc
+ B11111110, //8: abcdefg
+ B11110110, //9: abcdfg
+ B00000000, // : BLANK (10)
+ B00000010, //-: minus (11)
+ B00000001, // decimal point (12)
+ B11101110, // A abcefg (13)
+ B11001110, // P abefg (14)
+ B10011100, // C adef (15)
+ B11000110, //grad (upper circle) abfg (16)
+ B10110100, //% acdf (17)
+ B00111010, //lower circle cdeg (18)
+ B01100000, //I bc (19)
+ B10001110 //F aefg (20)
+};
+
+#define MAXCHARS sizeof(charDefinition)
+#define MAXSEGMENTS sizeof(segmentEnablePins)
+int maxDigits = sizeof(digitEnablePins);
+
+uint32_t charTable[MAXCHARS]; //generated pin table from segmentDefinitions
+uint32_t segmentEnableBits[MAXSEGMENTS]; //bitmaps, generated from EnablePins tables
+uint32_t digitEnableBits[10];
+//-----------------------------------------------------------------------------------------
+
+void ICACHE_RAM_ATTR writeDisplay(){
+static volatile uint32_t val = 0;
+static volatile byte pos = 0;
+//Per-digit dithering/duty-cycle phase counters, one per tube position (up to 10 supported here).
+//Each entry free-runs 1..MAXBRIGHTNESS and is compared against the target brightness to decide
+//ON/OFF for that digit's current refresh slot (see below) - a cheap "N-cycle duty modulation"
+//dimmer rather than true PWM within one frame. The staggered starting phases {0,9,2,8,4,7,6,5,3,1}
+//(instead of all starting at 0) spread each digit's ON pulses across different points in the
+//brightness cycle, which avoids all digits blinking on/off in visible lock-step and reduces
+//perceived flicker/beating between adjacent tubes.
+static volatile int brightCounter[] = {0,9,2,8,4,7,6,5,3,1};
+
+//https://sub.nanona.fi/esp8266/timing-and-ticks.html
+//One tick is 1us / 80 = 0.0125us = 12.5ns on 80MHz
+//One tick is 1us / 80 = 0.0125us = 6.25ns on 160MHz
+// 1 NOP is 1 tick
+
+ //noInterrupts();
+
+
+ if (EEPROMsaving) { //stop refresh, while EEPROM write is in progress!
+ timer1_write(VFDrefresh); //TIM_SINGLE (one-shot) mode requires re-arming the timer every call,
+ return; //even when skipping the refresh, or the ISR would never fire again.
+ }
+
+ //Brightness gating for THIS digit position (pos), applied BEFORE this position's segment data
+ //is shifted out below. STROBE is the chip's global blank input (LOW=display on, HIGH=off, per
+ //the pin comment above), so this briefly leaves the PREVIOUS digit's already-latched pattern
+ //on screen at the new brightness-derived on/off state until the new data is latched via LE
+ //further down. With a ~1200-tick (see VFDrefresh) refresh period this overlap is a small
+ //fraction of the slot and isn't visually significant.
+ if (brightCounter[pos] % MAXBRIGHTNESS < (displayON ? prm.dayBright : prm.nightBright))
+ WRITE_PERI_REG( PIN_OUT_CLEAR, PIN_STROBE_BIT ); //ON
+ else
+ WRITE_PERI_REG( PIN_OUT_SET, PIN_STROBE_BIT ); //OFF
+
+ for (int t=0; t<4;t++) asm volatile ("nop");
+
+ brightCounter[pos]++;
+ if (brightCounter[pos]>MAXBRIGHTNESS) brightCounter[pos] = 1;
+
+ //NOTE: digit[pos] indexes charTable[] (built from charDefinition[], MAXCHARS=21 entries)
+ //directly with no bounds check. If digit[pos] were ever >=21 this would read out-of-bounds
+ //memory. In practice the rest of the firmware is expected to only ever write valid glyph
+ //codes (0-20) into digit[], so this is a latent risk rather than an observed failure.
+ val = (digitEnableBits[pos] | charTable[digit[pos]]); //the full bitmap to send to MAX chip
+ if (digitDP[pos]) val = val | charTable[12]; //Decimal Point
+ //Shift out all 24 bits, MSB first (bit DATABITS-1 down to bit 0), matching the bit-position
+ //convention used by segmentEnablePins[]/digitEnablePins[] (index = bit position, MSB shifted
+ //first so it ends up in the "first" chip in the daisy-chain after the final LE latch).
+ //Note the asymmetric NOP padding: a '1' bit only pads 4 NOPs before raising CLK, while a '0'
+ //bit pads 8 - this compensates for the extra WRITE_PERI_REG() call needed to explicitly clear
+ //the DATA line (vs. just letting it stay set) so that DATA has equal setup time before CLK
+ //rises in both cases; hand-tuned bit-bang timing, not an accidental asymmetry.
+ for (int i=0; i= maxDigits) pos = 0;
+
+ timer1_write(VFDrefresh); //re-arm the one-shot timer1 for the next digit's refresh
+ //interrupts();
+}
+
+//One-time (setup) precomputation: turns the human-editable pin-index tables
+//(segmentEnablePins[]/digitEnablePins[]) and the per-glyph segment masks (charDefinition[])
+//into ready-to-OR 24-bit shift-register bitmaps, so writeDisplay() never has to do this bit
+//translation work inside the timing-critical ISR - it just OR's digitEnableBits[pos] |
+//charTable[digit[pos]] together.
+void generateBitTable() {
+uint32_t out;
+
+ DPRINTLN("--- Generating segment pins bitmap ---");
+ for (int i=0;i> ");
+
+ charTable[i] = out;
+ //DPRINTLN(charTable[i],BIN);
+ } //end for
+}
+
+
+void setup_pins() {
+#if defined(ESP8266)
+#else
+ //Enforced at compile time because writeDisplay() bit-bangs via ESP8266-only
+ //WRITE_PERI_REG()/PIN_OUT_SET/PIN_OUT_CLEAR register macros and relies on the ESP8266
+ //timer1 API (timer1_write, TIM_SINGLE) armed in startTimer() - none of that exists on ESP32.
+ #error "Only 8266 Board is supported!"
+#endif
+
+ DPRINTLN("Clock with SN75512 - Setup pins...");
+ pinMode(PIN_LE, OUTPUT); regPin(PIN_LE,"PIN_LE");
+ pinMode(PIN_STROBE,OUTPUT); regPin(PIN_STROBE,"PIN_STROBE");
+ pinMode(PIN_DATA,OUTPUT); regPin(PIN_DATA,"PIN_DATA");
+ pinMode(PIN_CLK, OUTPUT); regPin(PIN_CLK,"PIN_CLK");
+ digitalWrite(PIN_STROBE,LOW); //brightness
+
+ generateBitTable(); //must run before the timer/ISR starts, since writeDisplay() reads charTable[]/digitEnableBits[]/segmentEnableBits[]
+ digitsOnly = false;
+
+ startTimer();
+}
+
+//POSSIBLE ISSUE: both clearTubes() and writeDisplaySingle() are empty stubs - they satisfy the
+//common driver interface (see other driver files / ESP32_UniClock2.ino) syntactically, but do
+//not actually blank the display or perform a blocking one-shot write. Note that the core .ino no
+//longer calls clearTubes() from anywhere at all (its former caller, disableDisplay(), has been
+//removed entirely as dead code), so in practice this is moot for now regardless. Given this driver
+//is not wired to any active CLOCK_NN (see file header), this looks like an incomplete/legacy
+//stub rather than something exercised by current configurations - flagged for confirmation.
+void clearTubes() {}
+void writeDisplaySingle() {}
+#endif
diff --git a/firmware/uniclock/ESP32_UniClock2/SN75518.ino b/firmware/uniclock/ESP32_UniClock2/SN75518.ino
new file mode 100644
index 0000000..ad20807
--- /dev/null
+++ b/firmware/uniclock/ESP32_UniClock2/SN75518.ino
@@ -0,0 +1,485 @@
+#ifdef SN75518_ESP32
+//=============================================================================================
+// Driver for VFD (Vacuum Fluorescent Display) tubes driven by the Texas Instruments SN75518
+// serial-in / high-voltage parallel-out VFD grid/segment driver IC (32 outputs).
+//
+// Wiring / protocol:
+// - DATA/CLK: plain serial shift-register bit-banging, MSB first (bit 31 down to bit 0),
+// see writeDisplay(). LE (Latch Enable) is pulsed high-then-low once per digit after all
+// 32 bits are shifted, to atomically transfer the shifted-in bits to the SN75518's
+// high-voltage outputs (glitch-free - outputs don't change mid-shift).
+// - STROBE is the chip's output-enable/blank pin (LOW=display on, HIGH=off, per the pin
+// comment below) and is toggled per-digit to blank the display outside its multiplex slot.
+// - SN75518 outputs are numbered 1..32 (not 0-based) in its datasheet, hence the "-1" applied
+// to segmentEnablePins[]/digitEnablePins[] everywhere they're turned into bitmasks (see
+// generateBitTable()) - unlike the SN75512 driver, which uses 0-based bit positions.
+// - Two glyph-table variants, selected in clocks.h per clock config:
+// * #define SEGMENT8: classic 7-segment+DP tubes (e.g. IV-11/IV-12), reuses the same
+// 8-bit a..g+DP charDefinition[] layout as the SN75512/MAX6921 drivers.
+// * default (no SEGMENT8): 16-segment+DP alphanumeric tubes (e.g. IV-17), with a full
+// ASCII (space..DEL) 17-bit charDefinition[] table and ASCIImode glyph lookup via
+// asciiConvert[] (maps the digit[]-buffer glyph codes 0-20 to a printable ASCII char).
+// - Brightness/dimming is done differently from SN75512: instead of a fixed multiplex period
+// with a duty-cycle counter, writeDisplay() runs the ESP32 hardware timer in a genuine
+// variable-period ON/OFF PWM scheme - see the state/timerON/timerOFF logic below.
+// - Platform: ESP32 only (uses the ESP32 hw_timer_t API - timerAlarmWrite/Enable - and
+// portENTER_CRITICAL_ISR/DRAM_ATTR/IRAM_ATTR).
+//
+// Used by CLOCK_48 (WROOM ESP32, UNFI, 6x IV-17, 16-segment mode) and CLOCK_49 (WROOM ESP32,
+// UNFI, 6x IV-11, SEGMENT8/7-segment mode) in clocks.h.
+//=============================================================================================
+char tubeDriver[] = "SN75518_ESP32";
+//int maxDigits = 6;
+int wt = 5; //Serial timing
+
+#define PIN_LE 13 // D7 Shift Register Latch Enable
+#define PIN_CLK 12 // D6 Shift Register Clock
+#define PIN_DATA 27 // D5 Shift Register Data
+#define PIN_STROBE 14 // D1 Shift Register Strobe (1=display off 0=display on)
+
+//#define SEGMENT8 //please set in clocks.h
+//#define TESTMODE //for testing the segments of a new type VFD module - cycles through output
+ //Q1..Q32 one at a time (see "count" below) so each physical segment/grid
+ //wire can be identified by eye when commissioning a new/unknown VFD module.
+int count = 15; //output# to test
+unsigned long lastTest = 0;
+
+#ifdef SEGMENT8
+ //7-segment+DP tubes (e.g. IV-11): reuse the digit[]-buffer glyph-code convention directly
+ //(no ASCII remapping), so ASCIImode stays false and dispChar==digit[pos] in writeDisplay().
+ boolean ASCIImode = false;
+ #define DP_CHAR 12
+ byte charDefinition[] = {
+//------------------abcdefgDP---------------- definition of different characters
+//Each byte is a 7-segment+DP mask, MSB=a ... LSB=DP. generateBitTable() expands these into
+//32-bit shift-register bitmaps (charTable[]) via segmentEnablePins[] (defined in clocks.h).
+ B11111100, //0: abcdef
+ B01100000, //1: bc
+ B11011010, //2: abdeg
+ B11110010, //3: abcdg
+ B01100110, //4: bcfg
+ B10110110, //5: acdfg
+ B10111110, //6: acdefg
+ B11100000, //7: abc
+ B11111110, //8: abcdefg
+ B11110110, //9: abcdfg
+ B00000000, // : BLANK (10)
+ B00000010, //-: minus (11)
+ B00000001, // decimal point (12)
+ B11101110, // A abcefg (13)
+ B11001110, // P abefg (14)
+ B10011100, // C adef (15)
+ B11000110, //grad (upper circle) abfg (16)
+ B10110100, //% acdf (17)
+ B00111010, //lower circle cdeg (18)
+ B01100000, //I bc (19)
+ B10001110 //F aefg (20)
+};
+
+//byte segmentEnablePins[] = {9,8,5,3,4,7,6,2}; //segment enable bits (a,b,c,d,e,f,g,DP) (You MUST define always 8 bits!!!)
+
+#else
+ //16 segment + DP ---------------------------------------------------------------------------------------------
+ //16-segment alphanumeric tubes (e.g. IV-17): full ASCII glyph set is needed (letters, symbols),
+ //so instead of the 21-entry digit[]-buffer glyph codes we index this table by ASCII character
+ //(space=0x20 .. DEL=0x7F, 96 entries), via the asciiConvert[]+dispChar-=32 lookup below.
+ boolean ASCIImode = true;
+ #define DP_CHAR 14
+ //Each entry is a 17-bit mask, one bit per segment, in the order given by the "Dutsrpnmkhgfedcba"
+ //comment below (bit0=a ... bit15=u, bit16=D/decimal point) - note this is the OPPOSITE bit
+ //order from the SEGMENT8/charDefinition[] table above (there MSB=a); generateBitTable() below
+ //reverses it (segmentEnableBits[MAXSEGMENTS-j-1]) to compensate when building charTable[].
+ uint32_t charDefinition[96] = {
+// Dutsrpnmkhgfedcba
+ 0b00000000000000000, /* (space) */
+ 0b10000000000001100, /* ! */
+ 0b00000001000000100, /* " */ // 100000001
+ 0b01010101000111100, /* # */
+ 0b01010101010111011, /* $ */
+ 0b01110111010011001, /* % */
+ 0b01001001101110001, /* & */
+ 0b00000001000000000, /* ' */
+ 0b00001010000000000, /* ( */
+ 0b00100000100000000, /* ) */
+ 0b01111111100000000, /* * */
+ 0b01010101000000000, /* + */
+ 0b00100000000000000, /* , */
+ 0b01000100000000000, /* - */
+ 0b10000000000000000, /* . */
+ 0b00100010000000000, /* / */
+ 0b00100010011111111, /* 0 */
+ 0b00000010000001100, /* 1 */
+ 0b01000100001110111, /* 2 */
+ 0b00000100000111111, /* 3 */
+ 0b01000100010001100, /* 4 */
+ 0b01001000010110011, /* 5 */
+ 0b01000100011111011, /* 6 */
+ 0b00000000000001111, /* 7 */
+ 0b01000100011111111, /* 8 */
+ 0b01000100010111111, /* 9 */
+ 0b00010001000000000, /* : */
+ 0b00100001000000000, /* ; */
+ 0b01001010000000000, /* < */
+ 0b01000100000110000, /* = */
+ 0b00100100100000000, /* > */
+ 0b10010100000000111, /* ? */
+ 0b00000101011110111, /* @ */
+ 0b01000100011001111, /* A */
+ 0b00010101000111111, /* B */
+ 0b00000000011110011, /* C */
+ 0b00010001000111111, /* D */
+ 0b01000000011110011, /* E */
+ 0b01000000011000011, /* F */
+ 0b00000100011111011, /* G */
+ 0b01000100011001100, /* H */
+ 0b00010001000110011, /* I */
+ 0b00000000001111100, /* J */
+ 0b01001010011000000, /* K */
+ 0b00000000011110000, /* L */
+ 0b00000010111001100, /* M */
+ 0b00001000111001100, /* N */
+ 0b00000000011111111, /* O */
+ 0b01000100011000111, /* P */
+ 0b00001000011111111, /* Q */
+ 0b01001100011000111, /* R */
+ 0b01000100010111011, /* S */
+ 0b00010001000000011, /* T */
+ 0b00000000011111100, /* U */
+ 0b00100010011000000, /* V */
+ 0b00101000011001100, /* W */
+ 0b00101010100000000, /* X */
+ 0b01000100010111100, /* Y */
+ 0b00100010000110011, /* Z */
+ 0b00010001000010010, /* [ */
+ 0b00001000100000000, /* \ */
+ 0b00010001000100001, /* ] */
+ 0b00101000000000000, /* ^ */
+ 0b00000000000110000, /* _ */
+ 0b00000000100000000, /* ` */
+ 0b01010000001110000, /* a */
+ 0b01010000011100000, /* b */
+ 0b01000000001100000, /* c */
+ 0b00010100000011100, /* d */
+ 0b01100000001100000, /* e */
+ 0b01010101000000010, /* f */
+ 0b01010001010100001, /* g */
+ 0b01010000011000000, /* h */
+ 0b00010000000000000, /* i */
+ 0b00010001001100000, /* j */
+ 0b00011011000000000, /* k */
+ 0b00000000011000000, /* l */
+ 0b01010100001001000, /* m */
+ 0b01010000001000000, /* n */
+ 0b01010000001100000, /* o */
+ 0b01000001011000001, /* p */
+ 0b01010001010000001, /* q */
+ 0b01000000001000000, /* r */
+ 0b01010000010100001, /* s */
+ 0b01000000011100000, /* t */
+ 0b00010000001100000, /* u */
+ 0b00100000001000000, /* v */
+ 0b00101000001001000, /* w */
+ 0b00101010100000000, /* x */
+ 0b00000101000011100, /* y */
+ 0b01100000000100000, /* z */
+ 0b01010001000010010, /* { */
+ 0b00010001000000000, /* | */
+ 0b00010101000100001, /* } */
+ 0b01000001010000001, /* ~ */
+ 0b00000000000000000, /* (del) */
+ //Dutsrpnmkhgfedcba
+};
+
+ //byte segmentEnablePins[] = {16,7,0,1,2,3,4,5,6,8,9,11,10,12,13,14,15}; //segment enable bits defined in clocks.h
+#endif
+
+#define MAXCHARS sizeof(charDefinition)/sizeof(charDefinition[0])
+#define MAXSEGMENTS sizeof(segmentEnablePins)
+//Maps the digit[]-buffer glyph codes (0-9=digits,10=blank,11=minus,12=dp,13-20=A,P,C,deg,%,
+//lower-circle,I,F - see index comments on the SEGMENT8 charDefinition[] table above) to a
+//printable ASCII character, so that in 16-segment/ASCIImode the same digit[]-buffer convention
+//used by the rest of the firmware still works: writeDisplay() does
+//asciiConvert[digit[pos]] then "-32" to turn that into a 0-based index into the 96-entry
+//charDefinition[]/charTable[] (which starts at ASCII space=0x20).
+char asciiConvert[] = "0123456789 -.APC~% IF";
+int maxDigits = sizeof(digitEnablePins);
+int dispChar;
+
+uint32_t DRAM_ATTR animationMaskBits[5];
+uint32_t DRAM_ATTR charTable[MAXCHARS]; //generated pin table from segmentDefinitions
+uint32_t DRAM_ATTR segmentEnableBits[MAXSEGMENTS]; //bitmaps, generated from EnablePins tables
+uint32_t DRAM_ATTR digitEnableBits[10];
+
+int DRAM_ATTR PWMrefresh=5500; //Multiplex time period, in ESP32 hardware-timer ticks (not milliseconds, despite the variable name). Greater value => slower multiplex frequency
+int DRAM_ATTR PWM_min = 500;
+int DRAM_ATTR PWM_max = 5000;
+//int DRAM_ATTR PWMtiming[11] = {0,500,800,1200,2000,2500,3000,3500,4000,4500,5000};
+
+//-----------------------------------------------------------------------------------------
+
+//https://sub.nanona.fi/esp8266/timing-and-ticks.html
+//One tick is 1us / 80 = 0.0125us = 12.5ns on 80MHz
+//One tick is 1us / 80 = 0.0125us = 6.25ns on 160MHz
+// 1 NOP is 1 tick
+
+//POSSIBLE BUG / dead code: this function is never actually called (only referenced from
+//commented-out lines in writeDisplay(), e.g. "//delayMS(1);"). Also, despite its name and the
+//"int d" parameter (presumably intended as a millisecond count), the parameter is unused and
+//the body always executes exactly 3 NOPs (a few nanoseconds), not d milliseconds - so even if
+//re-enabled it would not delay by the requested amount. Left as-is since it is not on any live
+//code path.
+void inline delayMS(int d) {
+ asm volatile ("nop");
+ asm volatile ("nop");
+ asm volatile ("nop");
+}
+
+void setup_pins() {
+ DPRINTLN("VFD Clock - setup SN75518 pins");
+ pinMode(PIN_LE, OUTPUT); regPin(PIN_LE,"PIN_LE");
+ pinMode(PIN_STROBE, OUTPUT); regPin(PIN_STROBE,"PIN_STROBE");
+ digitalWrite(PIN_STROBE,LOW); //brightness
+ pinMode(PIN_DATA,OUTPUT); regPin(PIN_DATA,"PIN_DATA");
+ pinMode(PIN_CLK, OUTPUT); regPin(PIN_CLK,"PIN_CLK");
+ #ifdef TESTMODE
+ DPRINT("Testing output Q#:"); DPRINTLN(count);
+ #endif
+ //Build a human-readable summary of the active pin mapping for display on the config web page
+ //(see driverSetupStr usage in ESP32_UniClock2.ino) - purely diagnostic, no functional effect.
+ driverSetupStr = "SN75518 segmentEnablePins:";
+ for (int j=0;jSN75518 digitEnablePins:";
+ for (int j=0;j");
+
+ generateBitTable(); //must run before the timer/ISR starts - writeDisplay() reads charTable[]/digitEnableBits[]/segmentEnableBits[]/animationMaskBits[]
+ digitsOnly = false;
+ startTimer();
+}
+
+//Timer-interrupt multiplex refresh handler. Unlike the fixed-period SN75512 driver (which uses
+//a constant multiplex tick and a duty-cycle counter for brightness), this driver re-arms the
+//ESP32 hardware timer with a DIFFERENT period every call, alternating between an "ON" phase
+//(digit lit, duration = PWMtimeBrightness, proportional to brightness) and an "OFF" phase
+//(digit blanked, duration = PWMrefresh - PWMtimeBrightness) via the `state` toggle - i.e. true
+//variable-mark-space PWM dimming rather than a fixed-frequency multiplex with sub-cycle gating.
+//The digit position (pos) only advances at the start of each "ON" phase.
+void IRAM_ATTR writeDisplay(){ //void IRAM_ATTR writeDisplay(){
+ static DRAM_ATTR int timer = PWMrefresh;
+ static DRAM_ATTR uint32_t val;
+ static DRAM_ATTR byte pos = 0;
+ static DRAM_ATTR boolean state=true;
+ static DRAM_ATTR int brightness;
+ static DRAM_ATTR int PWMtimeBrightness=PWM_min;
+
+ if (EEPROMsaving) { //stop refresh, while EEPROM write is in progress!
+ //digitalWrite(PIN_STROBE,HIGH); //OFF
+ return; //NOTE: unlike SN75512's ISR, no timerAlarmWrite()/timerAlarmEnable() re-arm is
+ //needed here - this driver's ESP32 hw timer is configured with autoreload=true
+ //(see the timerAlarmWrite(...,true) calls below), so it keeps firing at its last
+ //configured period on its own; simply returning is enough to skip a refresh cycle
+ //while an EEPROM write is in progress. (Whatever was last shifted out stays lit,
+ //since STROBE is left as-is - the blanking line above is commented out.)
+ }
+ //Example of how to force specific glyphs onto specific tube positions for bench testing:
+ //digit[5] = 15; digit[4] = 16; digit[3] = 17; digit[2] = 18; digit[1] = 19; digit[0] = 20;
+ portENTER_CRITICAL_ISR(&timerMux);
+ noInterrupts(); //belt-and-braces alongside portENTER_CRITICAL_ISR - protects the whole
+ //shift+latch sequence below from being interleaved with other interrupts/
+ //the other core while the SN75518's serial frame is being clocked out.
+ intCounter++;
+
+ brightness = displayON ? prm.dayBright : prm.nightBright;
+ if (brightness>MAXBRIGHTNESS) brightness = MAXBRIGHTNESS; //only for safety
+
+ //At full manual brightness (auto-brightness off), force state permanently ON so the OFF/blank
+ //phase below is skipped entirely - i.e. 100% duty cycle / no blanking gap, for maximum
+ //perceived brightness and refresh rate when the user has explicitly asked for max brightness.
+ if ((!autoBrightness) && (brightness==MAXBRIGHTNESS)) state = true;
+
+ if (state) { //ON state: light up tube #pos for PWMtimeBrightness ticks, then go to OFF state
+ pos++; if (pos>maxDigits-1) { //go to the tube#0
+ pos = 0;
+ //Brightness (the ON-phase duration) is only recalculated once per full multiplex cycle,
+ //at tube#0, so all tubes share the same brightness within one cycle (avoids visible
+ //brightness differences between tubes if the ambient light level changes mid-cycle).
+ if (autoBrightness && displayON) { //change brightness only on the tube#0
+ PWMtimeBrightness = max(PWM_min,PWM_max*lx/MAXIMUM_LUX);
+ }
+ else
+ PWMtimeBrightness = max(PWM_min,PWM_max*brightness/MAXBRIGHTNESS);
+ }
+ dispChar = digit[pos];
+ if (ASCIImode) {
+ //16-segment tables are indexed by ASCII code (space=0x20 based), not by the digit[]-buffer
+ //glyph code directly, so translate glyph-code -> ASCII char via asciiConvert[] first.
+ //FIXED: previously "dispChar -= 32" ran unconditionally, even when the bounds check just
+ //below skipped the asciiConvert[] lookup (dispChar >= sizeof(asciiConvert), or dispChar
+ //landing on the string's NUL terminator) - that produced a negative index into charTable[]
+ //below (out-of-bounds read). Now the subtraction only happens together with a successful
+ //lookup; anything unmapped falls back to charTable[0], the blank/space glyph.
+ if (dispChar blank glyph (charTable[0])
+ }
+ }
+ //NOTE: as with the SN75512 driver, dispChar indexes charTable[] (MAXCHARS entries) with no
+ //explicit bounds check against MAXCHARS itself - relies on upstream code only ever writing
+ //valid glyph codes into digit[].
+ val = (digitEnableBits[pos] | charTable[dispChar]); //the full bitmap to send to MAX chip
+ if (digitDP[pos]) val = val | charTable[DP_CHAR]; //Decimal Point
+ #ifdef TESTMODE
+ //Override the normal glyph lookup: light exactly one output (Q) at a time so each
+ //physical pin can be identified on the tube; "count-1" because SN75518 outputs are
+ //numbered 1-based (Q1..Q32), same convention as segmentEnablePins/digitEnablePins.
+ val = digitEnableBits[pos] | ((uint32_t)1<<(count-1));
+ animMask[pos] = 0;
+ if ((millis()-lastTest)>5000) {
+ lastTest=millis();
+ count++; if (count>32) count = 0;
+ }
+ #endif
+ timer = PWMtimeBrightness;
+ #ifdef CLOCK_xx //example only
+ //Template/example only - "CLOCK_xx" is never actually #defined by any real CLOCK_NN in
+ //clocks.h, so this block is permanently dead code as shipped. It documents the intended
+ //pattern for compensating individual tubes that appear dimmer than others (e.g. due to
+ //VFD wear or grid-wiring differences): give a specific tube position a longer ON time
+ //(here 2x/3x) than PWMtimeBrightness would otherwise give it. To use it for a real clock,
+ //replace CLOCK_xx with that clock's actual CLOCK_NN macro.
+ if (pos==2) timer = 2*timer; //Weak tube#2 brightness compensation, some hacking
+ if (pos==4) timer = 3*timer; //Weak tube#4 brightness compensation, some hacking
+ #endif
+ timerON = timer; //debug/telemetry only (see intCounter/timerON/timerOFF in ESP32_UniClock2.ino)
+ timerOFF = PWMrefresh-PWMtimeBrightness;
+ }
+ else { //OFF state: blank for the remainder of the PWMrefresh period
+ timer = PWMrefresh-PWMtimeBrightness;
+ }
+
+ if (timer<500) timer = 500; //safety only... avoids an excessively short/zero hw-timer alarm period (very high ISR rate) from a pathological brightness/PWM combination
+
+ if ( (brightness == 0) || (!state) || (!radarON)) { //OFF state, blank digit
+ digitalWrite(PIN_STROBE,HIGH); //OFF
+ }
+ else { //ON state
+ if (animMask[pos]>0) val &= animationMaskBits[animMask[pos]-1]; //animationMode 6, mask characters from up to down and back
+ //Shift out all 32 bits, MSB first (Q32 down to Q1). Timing note: unlike the SN75512 driver
+ //(which uses raw WRITE_PERI_REG()+NOP padding for a tightly-controlled bit-bang), this uses
+ //plain digitalWrite() with no explicit delay between DATA and CLK edges - the delayMS(1)
+ //calls that would add a small pad are commented out. The function-call overhead of
+ //digitalWrite() itself appears to provide enough setup/hold margin for the SN75518 at the
+ //ESP32's clock speed, but that margin is implicit rather than guaranteed by an explicit delay.
+ for (int i=0; i<32; i++) {
+ if (val & uint32_t(1 << (31 - i)))
+ {digitalWrite(PIN_DATA, HIGH); //asm volatile ("nop");
+ }
+ else
+ {digitalWrite(PIN_DATA, LOW); //asm volatile ("nop");
+ }
+
+ digitalWrite(PIN_CLK,HIGH); //asm volatile ("nop"); //delayMS(1);
+ digitalWrite(PIN_CLK,LOW); //asm volatile ("nop"); //delayMS(1);
+ } //end for
+
+ //Latch: pulse LE high then low to transfer the just-shifted 32 bits into the SN75518's
+ //output latch (glitch-free update), then re-enable STROBE (LOW=on) to actually light the tube.
+ digitalWrite(PIN_LE,HIGH ); asm volatile ("nop");
+ digitalWrite(PIN_LE,LOW);
+ digitalWrite(PIN_STROBE,LOW ); //ON
+ } //end else
+
+ portEXIT_CRITICAL_ISR(&timerMux);
+ state = !state; //flip ON/OFF phase for the next timer interrupt
+ //ESP32timer = timerBegin(0, PRESCALER, true); //set prescaler, true = edge generated signal
+ //timerAttachInterrupt(ESP32timer, &writeDisplay, true);
+ //Re-arm the hw timer with the period (ON or OFF duration) just computed above - this is what
+ //gives this driver its variable-frequency PWM dimming (as opposed to SN75512's fixed period).
+ timerAlarmWrite(ESP32timer, timer, true);
+ timerAlarmEnable(ESP32timer);
+ interrupts();
+}
+
+//One-time (setup) precomputation, mirroring SN75512's generateBitTable() but for the SN75518's
+//1-based (Q1..Q32) output numbering (note the "-1" on every segmentEnablePins[i]/
+//digitEnablePins[i] use below) and its extra animationMaskBits[] table used by the digit-change
+//"wipe" animation (mode 5/6 in changeDigit(), core .ino file).
+void generateBitTable() {
+uint32_t out;
+
+ DPRINT("--- Generating segment pins bitmap:"); DPRINTLN(MAXSEGMENTS);
+ for (int i=0;i +b/f -> +g -> +c/e -> +d), later inverted below so that
+ //ANDing a character's bitmap with animationMaskBits[N] blanks out the segments not yet
+ //reached by the wipe.
+ animationMaskBits[0] = uint32_t(1<> ");
+
+ charTable[i] = out;
+ //DPRINTLN(charTable[i],BIN);
+ } //end for
+}
+
+
+void clearTubes() {
+ digitalWrite(PIN_STROBE,HIGH);
+}
+
+void writeDisplaySingle() {}
+#endif
diff --git a/firmware/uniclock/ESP32_UniClock2/VQC10.ino b/firmware/uniclock/ESP32_UniClock2/VQC10.ino
new file mode 100644
index 0000000..61f2b7b
--- /dev/null
+++ b/firmware/uniclock/ESP32_UniClock2/VQC10.ino
@@ -0,0 +1,625 @@
+// ============================================================================
+// VQC10 driver
+// ----------------------------------------------------------------------------
+// Drives 8x VQC10 tubes/modules, each a 5x7 dot-matrix (LED or VFD-style)
+// character display (see Datasheets/VQC10_Funkamateur.pdf and
+// "VQC10 timing.jpeg" for the original hardware reference - VQC10 is an
+// Eastern-European 5x7 dot-matrix indicator tube, not a classic 7-segment
+// Nixie/VFD tube). Because each character cell is a dot matrix rather than
+// a fixed set of segments, this driver renders full ASCII text (via the
+// bitmap _font[] table below) instead of just digits.
+//
+// Wiring / protocol: the 8 character cells are driven through a chain of
+// 74HC595 serial-in/parallel-out shift registers (latchPin=RCK/STCP,
+// clockPin=SCK/SHCP, dataPin=DIN/SER - classic 3-wire SPI-like shift-out,
+// no MISO). Unlike most other drivers in this project (which multiplex
+// digit-by-digit), the VQC10 hardware is scanned ROW-by-ROW: for every one
+// of the NUMROWS=7 dot-rows, the column data for all NUMDIGITS=8 characters
+// is shifted out and latched digit-by-digit, then a single row-select line
+// is pulsed on for a PWM-timed on-period before moving to the next row.
+// Refresh timing is fully cooperative/blocking (see writeDisplay2() below)
+// rather than driven from the hardware timer ISR - see the note above
+// writeDisplay() for why.
+//
+// Used by clocks.h configuration(s): CLOCK_71 ("2x VQC10 clock by UNFI").
+// ============================================================================
+#ifdef VQC10
+char tubeDriver[] = "VQC10";
+#define NUMDIGITS 8
+int maxDigits = NUMDIGITS;
+
+//Please, set the pin numbers in clock.h!
+//#define latchPin 5 //D2 RCK
+//#define clockPin 4 //D1 SCK
+//#define dataPin 2 //D4 DIN
+//#define ledOffpin 0 //D5
+//#define D5pin 14
+
+
+#define NUMCOLS 5 // font glyph width in dots (columns per character cell)
+#define NUMROWS 7 // font glyph height in dots (rows per character cell) - also the number of physical row-select lines scanned in writeDisplay2()
+
+boolean _upsidedown = false; // flips the dot-matrix rendering 180 degrees (for tubes mounted upside down); never set to true anywhere in this codebase - effectively dead/unused, kept for parity with wordclock.ino which shares this same font/transpose code
+byte row[NUMROWS]; // scratch buffer filled by show()/transposePattern(): one row-bitmap per dot-row for the character currently being rendered
+
+// Maps the shared digit[]/newDigit[] buffer codes (see core file: 0-9=digit, 10=BLANK,
+// 11=minus, 12=decimal point, 13+=driver-specific extra glyphs) to an index into the
+// ASCII-ordered _font[] table below, i.e. this converts the generic "digit code" alphabet
+// used by the core firmware into actual character codes this driver can render.
+char asciiConvert[] = "0123456789 -.APC~%oIF";
+char DPchar = 0xFF; //decimal point - index of the dedicated 1-dot "decimal point" glyph appended at the very end of _font[] (see 0xFF entry below)
+
+char cDigit[NUMDIGITS+1] = "76543210"; // NOTE: declared/initialized but never referenced anywhere else in this file or the project - dead code (candidate for removal)
+byte dat[NUMDIGITS][NUMROWS]; // final row-bitmaps actually shifted out to hardware (post-animation-blend result)
+byte oldDat[NUMDIGITS][NUMROWS]; // row-bitmaps for the outgoing (old) character, built fresh each writeDisplaySingle() call
+byte newDat[NUMDIGITS][NUMROWS]; // row-bitmaps for the incoming (new) character, built fresh each writeDisplaySingle() call
+
+// standard ascii 5x7 font incl. the IBM-PC extension:
+// already defined in DotMatrx5x7.h
+// Format: NUMCOLS(5) bytes per glyph, indexed as glyph_code*NUMCOLS in show() above; each
+// byte is one column of the cell, with dot rows packed in its low NUMROWS(7) bits (bit0=row0
+// top .. bit6=row6 bottom in normal orientation). Glyph 0xFF at the very end of the table is
+// repurposed as a dedicated "decimal point" glyph (see DPchar above) rather than a real
+// character, so it only lights a single dot instead of a printable symbol.
+//#define aUML "\x84" // ä = 0x84
+//#define oUML "\x94" // ö = 0x94
+//#define uUML "\x81" // ü = 0x81
+//#define sZET "\xE1" // ß = 0xE1
+//#define AUML "\x8E" // Ä = 0x8E
+//#define OUML "\x99" // Ö = 0x99
+//#define UUML "\x9A" // Ü = 0x9A
+
+byte _font[] = {
+ 0x00, 0x00, 0x00, 0x00, 0x00, // 0x00 empty
+ 0x3E, 0x5B, 0x4F, 0x5B, 0x3E, // 0x01 sad
+ 0x3E, 0x6B, 0x4F, 0x6B, 0x3E, // 0x02 happy
+ 0x1C, 0x3E, 0x7C, 0x3E, 0x1C, // 0x03 heart
+ 0x18, 0x3C, 0x7E, 0x3C, 0x18, // 0x04 diamond
+ 0x1C, 0x57, 0x7D, 0x57, 0x1C, // 0x05 clover
+ 0x1C, 0x5E, 0x7F, 0x5E, 0x1C, // 0x06 spade
+ 0x00, 0x18, 0x3C, 0x18, 0x00, // 0x07 small full circle
+ 0xFF, 0xE7, 0xC3, 0xE7, 0xFF, // 0x08 inverted small circle
+ 0x06, 0x09, 0x06, 0x00, 0x00, // 0x09 small empty circle //old: 0x00, 0x18, 0x24, 0x18, 0x00, // 0x09 small empty circle
+ 0xFF, 0xE7, 0xDB, 0xE7, 0xFF, // 0x0A block (linefeed)
+ 0x30, 0x48, 0x3A, 0x06, 0x0E, // 0x0B male
+ 0x26, 0x29, 0x79, 0x29, 0x26, // 0x0C female
+ 0x40, 0x7F, 0x05, 0x05, 0x07, // 0x0D note (carriage return)
+ 0x40, 0x7F, 0x05, 0x25, 0x3F, // 0x0E two notes
+ 0x5A, 0x3C, 0xE7, 0x3C, 0x5A, // 0x0F sun
+ 0x7F, 0x3E, 0x1C, 0x1C, 0x08, // 0x10 right triangle
+ 0x08, 0x1C, 0x1C, 0x3E, 0x7F, // 0x11 left triangle
+ 0x14, 0x22, 0x7F, 0x22, 0x14, // 0x12 up/down arrow
+ 0x5F, 0x5F, 0x00, 0x5F, 0x5F, // 0x13 double exclamation mark
+ 0x06, 0x09, 0x7F, 0x01, 0x7F, // 0x14 paragraph end sign (pilcrow)
+ // 0x00, 0x66, 0x89, 0x95, 0x6A, // 0x15 paragraph sign original
+ 0x00, 0x26, 0x49, 0x55, 0x28, // 0x15 paragraph sign
+ 0x60, 0x60, 0x60, 0x60, 0x60, // 0x16 double underline
+ 0x54, 0x62, 0x7F, 0x62, 0x54, // 0x17 up/down arrow with underline
+ 0x08, 0x04, 0x7E, 0x04, 0x08, // 0x18 up arrow
+ 0x10, 0x20, 0x7E, 0x20, 0x10, // 0x19 down arrow
+ 0x08, 0x08, 0x2A, 0x1C, 0x08, // 0x1A right arrow
+ 0x08, 0x1C, 0x2A, 0x08, 0x08, // 0x1B left arrow
+ 0x14, 0x3E, 0x55, 0x41, 0x22, // 0x1C euro
+ 0x63, 0x75, 0x69, 0x75, 0x63, // 0x1D timer
+ 0x30, 0x38, 0x3E, 0x38, 0x30, // 0x1E up triangle
+ 0x06, 0x0E, 0x3E, 0x0E, 0x06, // 0x1F down triangle
+ 0x00, 0x00, 0x00, 0x00, 0x00, // 0x20 space
+ 0x00, 0x00, 0x5F, 0x00, 0x00, // 0x21 !
+ 0x00, 0x07, 0x00, 0x07, 0x00, // 0x22 "
+ 0x14, 0x7F, 0x14, 0x7F, 0x14, // 0x23 #
+ 0x24, 0x2A, 0x7F, 0x2A, 0x12, // 0x24 $
+ 0x23, 0x13, 0x08, 0x64, 0x62, // 0x25 %
+ 0x36, 0x49, 0x56, 0x20, 0x50, // 0x26 &
+ 0x00, 0x08, 0x07, 0x03, 0x00, // 0x27 ´
+ 0x00, 0x1C, 0x22, 0x41, 0x00, // 0x28 (
+ 0x00, 0x41, 0x22, 0x1C, 0x00, // 0x29 )
+ 0x2A, 0x1C, 0x7F, 0x1C, 0x2A, // 0x2A *
+ 0x08, 0x08, 0x3E, 0x08, 0x08, // 0x2B +
+ 0x00, 0x80, 0x70, 0x30, 0x00, // 0x2C ,
+ 0x08, 0x08, 0x08, 0x08, 0x08, // 0x2D -
+ 0x00, 0x00, 0x60, 0x60, 0x00, // 0x2E .
+ 0x20, 0x10, 0x08, 0x04, 0x02, // 0x2F /
+ 0x3E, 0x51, 0x49, 0x45, 0x3E, // 0x30 0
+ 0x00, 0x42, 0x7F, 0x40, 0x00, // 0x31 1
+ 0x72, 0x49, 0x49, 0x49, 0x46, // 0x32 2
+ 0x21, 0x41, 0x49, 0x4D, 0x33, // 0x33 3
+ 0x18, 0x14, 0x12, 0x7F, 0x10, // 0x34 4
+ 0x27, 0x45, 0x45, 0x45, 0x39, // 0x35 5
+ 0x3C, 0x4A, 0x49, 0x49, 0x31, // 0x36 6
+ 0x41, 0x21, 0x11, 0x09, 0x07, // 0x37 7
+ 0x36, 0x49, 0x49, 0x49, 0x36, // 0x38 8
+ 0x46, 0x49, 0x49, 0x29, 0x1E, // 0x39 9
+ 0x00, 0x00, 0x14, 0x00, 0x00, // 0x3A :
+ 0x00, 0x40, 0x34, 0x00, 0x00, // 0x3B ;
+ 0x00, 0x08, 0x14, 0x22, 0x41, // 0x3C <
+ 0x14, 0x14, 0x14, 0x14, 0x14, // 0x3D =
+ 0x00, 0x41, 0x22, 0x14, 0x08, // 0x3E >
+ 0x02, 0x01, 0x59, 0x09, 0x06, // 0x3F ?
+ 0x3E, 0x41, 0x5D, 0x59, 0x4E, // 0x40 @
+ 0x7C, 0x12, 0x11, 0x12, 0x7C, // 0x41 A
+ 0x7F, 0x49, 0x49, 0x49, 0x36, // 0x42 B
+ 0x3E, 0x41, 0x41, 0x41, 0x22, // 0x43 C
+ 0x7F, 0x41, 0x41, 0x41, 0x3E, // 0x44 D
+ 0x7F, 0x49, 0x49, 0x49, 0x41, // 0x45 E
+ 0x7F, 0x09, 0x09, 0x09, 0x01, // 0x46 F
+ 0x3E, 0x41, 0x41, 0x51, 0x73, // 0x47 G
+ 0x7F, 0x08, 0x08, 0x08, 0x7F, // 0x48 H
+ 0x00, 0x41, 0x7F, 0x41, 0x00, // 0x49 I
+ 0x20, 0x40, 0x41, 0x3F, 0x01, // 0x4A J
+ 0x7F, 0x08, 0x14, 0x22, 0x41, // 0x4B K
+ 0x7F, 0x40, 0x40, 0x40, 0x40, // 0x4C L
+ 0x7F, 0x02, 0x1C, 0x02, 0x7F, // 0x4D M
+ 0x7F, 0x04, 0x08, 0x10, 0x7F, // 0x4E N
+ 0x3E, 0x41, 0x41, 0x41, 0x3E, // 0x4F O
+ 0x7F, 0x09, 0x09, 0x09, 0x06, // 0x50 P
+ 0x3E, 0x41, 0x51, 0x21, 0x5E, // 0x51 Q
+ 0x7F, 0x09, 0x19, 0x29, 0x46, // 0x52 R
+ 0x26, 0x49, 0x49, 0x49, 0x32, // 0x53 S
+ 0x03, 0x01, 0x7F, 0x01, 0x03, // 0x54 T
+ 0x3F, 0x40, 0x40, 0x40, 0x3F, // 0x55 U
+ 0x1F, 0x20, 0x40, 0x20, 0x1F, // 0x56 V
+ 0x3F, 0x40, 0x38, 0x40, 0x3F, // 0x57 W
+ 0x63, 0x14, 0x08, 0x14, 0x63, // 0x58 X
+ 0x03, 0x04, 0x78, 0x04, 0x03, // 0x59 Y
+ 0x61, 0x59, 0x49, 0x4D, 0x43, // 0x5A Z
+ 0x00, 0x7F, 0x41, 0x41, 0x41, // 0x5B [
+ 0x02, 0x04, 0x08, 0x10, 0x20, // 0x5C '\'
+ 0x00, 0x41, 0x41, 0x41, 0x7F, // 0x5D ]
+ 0x04, 0x02, 0x01, 0x02, 0x04, // 0x5E ^
+ 0x40, 0x40, 0x40, 0x40, 0x40, // 0x5F _
+ 0x00, 0x03, 0x07, 0x08, 0x00, // 0x60 `
+ 0x20, 0x54, 0x54, 0x78, 0x40, // 0x61 a
+ 0x7F, 0x28, 0x44, 0x44, 0x38, // 0x62 b
+ 0x38, 0x44, 0x44, 0x44, 0x00, // 0x63 c
+ 0x38, 0x44, 0x44, 0x28, 0x7F, // 0x64 d
+ 0x38, 0x54, 0x54, 0x54, 0x18, // 0x65 e
+ 0x00, 0x08, 0x7E, 0x09, 0x02, // 0x66 f
+ // 0x18, 0xA4, 0xA4, 0x9C, 0x78, // 0x67 g (old)
+ 0x0C, 0x52, 0x52, 0x4E, 0x3C, // 0x67 g
+ 0x7F, 0x08, 0x04, 0x04, 0x78, // 0x68 h
+ 0x00, 0x44, 0x7D, 0x40, 0x00, // 0x69 i
+ 0x20, 0x40, 0x40, 0x3D, 0x00, // 0x6A j
+ 0x7F, 0x10, 0x28, 0x44, 0x00, // 0x6B k
+ 0x00, 0x41, 0x7F, 0x40, 0x00, // 0x6C l
+ 0x7C, 0x04, 0x78, 0x04, 0x78, // 0x6D m
+ 0x7C, 0x08, 0x04, 0x04, 0x78, // 0x6E n
+ 0x38, 0x44, 0x44, 0x44, 0x38, // 0x6F o
+ 0xFC, 0x18, 0x24, 0x24, 0x18, // 0x70 p
+ // 0x18, 0x24, 0x24, 0x18, 0xFC, // 0x71 q (old)
+ 0x0C, 0x12, 0x12, 0x0C, 0x7E, // 0x71 q
+ 0x7C, 0x08, 0x04, 0x04, 0x08, // 0x72 r
+ 0x48, 0x54, 0x54, 0x54, 0x24, // 0x73 s
+ 0x04, 0x04, 0x3F, 0x44, 0x24, // 0x74 t
+ 0x3C, 0x40, 0x40, 0x20, 0x7C, // 0x75 u
+ 0x1C, 0x20, 0x40, 0x20, 0x1C, // 0x76 v
+ 0x3C, 0x40, 0x30, 0x40, 0x3C, // 0x77 w
+ 0x44, 0x28, 0x10, 0x28, 0x44, // 0x78 x
+ 0x0C, 0x10, 0x10, 0x50, 0x3C, // 0x79 y
+ 0x44, 0x64, 0x54, 0x4C, 0x44, // 0x7A z
+ 0x00, 0x08, 0x36, 0x41, 0x00, // 0x7B {
+ 0x00, 0x00, 0x7F, 0x00, 0x00, // 0x7C |
+ 0x00, 0x41, 0x36, 0x08, 0x00, // 0x7D }
+ 0x02, 0x01, 0x02, 0x04, 0x02, // 0x7E ~
+ 0x3C, 0x26, 0x23, 0x26, 0x3C, // 0x7F house
+ 0x1E, 0xA1, 0xA1, 0x61, 0x12, // 0x81
+ 0x3A, 0x40, 0x40, 0x20, 0x7A, // 0x81 ü
+ 0x38, 0x54, 0x54, 0x55, 0x59, // 0x82
+ 0x21, 0x55, 0x55, 0x79, 0x41, // 0x83
+ 0x21, 0x54, 0x54, 0x78, 0x41, // 0x84 ä
+ 0x21, 0x55, 0x54, 0x78, 0x40, // 0x85
+ 0x20, 0x54, 0x55, 0x79, 0x40, // 0x86
+ 0x0C, 0x1E, 0x52, 0x72, 0x12, // 0x87
+ 0x39, 0x55, 0x55, 0x55, 0x59, // 0x88
+ 0x39, 0x54, 0x54, 0x54, 0x59, // 0x89
+ 0x39, 0x55, 0x54, 0x54, 0x58, // 0x8A
+ 0x00, 0x00, 0x45, 0x7C, 0x41, // 0x8B
+ 0x00, 0x02, 0x45, 0x7D, 0x42, // 0x8C
+ 0x00, 0x01, 0x45, 0x7C, 0x40, // 0x8D
+ 0xF0, 0x29, 0x24, 0x29, 0xF0, // 0x8E Ä
+ 0xF0, 0x28, 0x25, 0x28, 0xF0, // 0x8F
+ 0x7C, 0x54, 0x55, 0x45, 0x00, // 0x90
+ 0x20, 0x54, 0x54, 0x7C, 0x54, // 0x91
+ 0x7C, 0x0A, 0x09, 0x7F, 0x49, // 0x92
+ 0x32, 0x49, 0x49, 0x49, 0x32, // 0x93
+ 0x32, 0x48, 0x48, 0x48, 0x32, // 0x94 ö
+ 0x32, 0x4A, 0x48, 0x48, 0x30, // 0x95
+ 0x3A, 0x41, 0x41, 0x21, 0x7A, // 0x96
+ 0x3A, 0x42, 0x40, 0x20, 0x78, // 0x97
+ 0x00, 0x9D, 0xA0, 0xA0, 0x7D, // 0x98
+ //0x39, 0x44, 0x44, 0x44, 0x39, // 0x99 Ö (old)
+ 0x3D, 0x42, 0x42, 0x42, 0x3D, // 0x99 Ö
+ 0x3D, 0x40, 0x40, 0x40, 0x3D, // 0x9A Ü
+ 0x3C, 0x24, 0xFF, 0x24, 0x24, // 0x9B cent
+ 0x48, 0x7E, 0x49, 0x43, 0x66, // 0x9C pound
+ 0x2B, 0x2F, 0xFC, 0x2F, 0x2B, // 0x9D
+ 0xFF, 0x09, 0x29, 0xF6, 0x20, // 0x9E
+ 0xC0, 0x88, 0x7E, 0x09, 0x03, // 0x9F
+ 0x20, 0x54, 0x54, 0x79, 0x41, // 0xA0
+ 0x00, 0x00, 0x44, 0x7D, 0x41, // 0xA1
+ 0x30, 0x48, 0x48, 0x4A, 0x32, // 0xA2
+ 0x38, 0x40, 0x40, 0x22, 0x7A, // 0xA3
+ 0x00, 0x7A, 0x0A, 0x0A, 0x72, // 0xA4
+ 0x7D, 0x0D, 0x19, 0x31, 0x7D, // 0xA5
+ 0x26, 0x29, 0x29, 0x2F, 0x28, // 0xA6
+ 0x26, 0x29, 0x29, 0x29, 0x26, // 0xA7
+ 0x30, 0x48, 0x4D, 0x40, 0x20, // 0xA8
+ 0x38, 0x08, 0x08, 0x08, 0x08, // 0xA9
+ 0x08, 0x08, 0x08, 0x08, 0x38, // 0xAA
+ 0x2F, 0x10, 0xC8, 0xAC, 0xBA, // 0xAB
+ 0x2F, 0x10, 0x28, 0x34, 0xFA, // 0xAC
+ 0x00, 0x00, 0x7B, 0x00, 0x00, // 0xAD exclamation mark upside down
+ 0x08, 0x14, 0x2A, 0x14, 0x22, // 0xAE less less
+ 0x22, 0x14, 0x2A, 0x14, 0x08, // 0xAF greater greater
+ 0xAA, 0x00, 0x55, 0x00, 0xAA, // 0xB0
+ 0xAA, 0x55, 0xAA, 0x55, 0xAA, // 0xB1
+ 0xAA, 0x55, 0xAA, 0x55, 0xAA, // 0xB2
+ 0x00, 0x00, 0x00, 0xFF, 0x00, // 0xB3
+ 0x10, 0x10, 0x10, 0xFF, 0x00, // 0xB4
+ 0x14, 0x14, 0x14, 0xFF, 0x00, // 0xB5
+ 0x10, 0x10, 0xFF, 0x00, 0xFF, // 0xB6
+ 0x10, 0x10, 0xF0, 0x10, 0xF0, // 0xB7
+ 0x14, 0x14, 0x14, 0xFC, 0x00, // 0xB8
+ 0x14, 0x14, 0xF7, 0x00, 0xFF, // 0xB9
+ 0x00, 0x00, 0xFF, 0x00, 0xFF, // 0xBA
+ 0x14, 0x14, 0xF4, 0x04, 0xFC, // 0xBB
+ 0x14, 0x14, 0x17, 0x10, 0x1F, // 0xBC
+ 0x10, 0x10, 0x1F, 0x10, 0x1F, // 0xBD
+ 0x14, 0x14, 0x14, 0x1F, 0x00, // 0xBE
+ 0x10, 0x10, 0x10, 0xF0, 0x00, // 0xBF
+ 0x00, 0x00, 0x00, 0x1F, 0x10, // 0xC0
+ 0x10, 0x10, 0x10, 0x1F, 0x10, // 0xC1
+ 0x10, 0x10, 0x10, 0xF0, 0x10, // 0xC2
+ 0x00, 0x00, 0x00, 0xFF, 0x10, // 0xC3
+ 0x10, 0x10, 0x10, 0x10, 0x10, // 0xC4
+ 0x10, 0x10, 0x10, 0xFF, 0x10, // 0xC5
+ 0x00, 0x00, 0x00, 0xFF, 0x14, // 0xC6
+ 0x00, 0x00, 0xFF, 0x00, 0xFF, // 0xC7
+ 0x00, 0x00, 0x1F, 0x10, 0x17, // 0xC8
+ 0x00, 0x00, 0xFC, 0x04, 0xF4, // 0xC9
+ 0x14, 0x14, 0x17, 0x10, 0x17, // 0xCA
+ 0x14, 0x14, 0xF4, 0x04, 0xF4, // 0xCB
+ 0x00, 0x00, 0xFF, 0x00, 0xF7, // 0xCC
+ 0x14, 0x14, 0x14, 0x14, 0x14, // 0xCD
+ 0x14, 0x14, 0xF7, 0x00, 0xF7, // 0xCE
+ 0x14, 0x14, 0x14, 0x17, 0x14, // 0xCF
+ 0x10, 0x10, 0x1F, 0x10, 0x1F, // 0xD0
+ 0x14, 0x14, 0x14, 0xF4, 0x14, // 0xD1
+ 0x10, 0x10, 0xF0, 0x10, 0xF0, // 0xD2
+ 0x00, 0x00, 0x1F, 0x10, 0x1F, // 0xD3
+ 0x00, 0x00, 0x00, 0x1F, 0x14, // 0xD4
+ 0x00, 0x00, 0x00, 0xFC, 0x14, // 0xD5
+ 0x00, 0x00, 0xF0, 0x10, 0xF0, // 0xD6
+ 0x10, 0x10, 0xFF, 0x10, 0xFF, // 0xD7
+ 0x14, 0x14, 0x14, 0xFF, 0x14, // 0xD8
+ 0x10, 0x10, 0x10, 0x1F, 0x00, // 0xD9
+ 0x00, 0x00, 0x00, 0xF0, 0x10, // 0xDA
+ 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 0xDB
+ 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, // 0xDC
+ 0xFF, 0xFF, 0xFF, 0x00, 0x00, // 0xDD
+ 0x00, 0x00, 0x00, 0xFF, 0xFF, // 0xDE
+ 0x0F, 0x0F, 0x0F, 0x0F, 0x0F, // 0xDF
+ 0x38, 0x44, 0x44, 0x38, 0x44, // 0xE0 alpha
+ 0x7C, 0x2A, 0x2A, 0x3E, 0x14, // 0xE1 beta / ß
+ 0x7E, 0x02, 0x02, 0x06, 0x06, // 0xE2 Gamma
+ 0x02, 0x7E, 0x02, 0x7E, 0x02, // 0xE3 Pi
+ 0x63, 0x55, 0x49, 0x41, 0x63, // 0xE4 Sigma
+ 0x38, 0x44, 0x44, 0x3C, 0x04, // 0xE5 sigma
+ 0x40, 0x7E, 0x20, 0x1E, 0x20, // 0xE6 mu
+ 0x06, 0x02, 0x7E, 0x02, 0x02, // 0xE7 tau
+ 0x99, 0xA5, 0xE7, 0xA5, 0x99, // 0xE8 Phi
+ 0x1C, 0x2A, 0x49, 0x2A, 0x1C, // 0xE9 theta
+ 0x4C, 0x72, 0x01, 0x72, 0x4C, // 0xEA Omega
+ 0x30, 0x4A, 0x4D, 0x4D, 0x30, // 0xEB delta
+ 0x30, 0x48, 0x78, 0x48, 0x30, // 0xEC phi?
+ 0xBC, 0x62, 0x5A, 0x46, 0x3D, // 0xED empty set
+ 0x3E, 0x49, 0x49, 0x49, 0x00, // 0xEE epsilon
+ 0x7E, 0x01, 0x01, 0x01, 0x7E, // 0xEF
+ 0x2A, 0x2A, 0x2A, 0x2A, 0x2A, // 0xF0 equivalence
+ 0x44, 0x44, 0x5F, 0x44, 0x44, // 0xF1 plus minus
+ 0x40, 0x51, 0x4A, 0x44, 0x40, // 0xF2 greater or equal
+ 0x40, 0x44, 0x4A, 0x51, 0x40, // 0xF3 less or equal
+ 0x00, 0x00, 0xFF, 0x01, 0x03, // 0xF4
+ 0xE0, 0x80, 0xFF, 0x00, 0x00, // 0xF5
+ 0x08, 0x08, 0x6B, 0x6B, 0x08, // 0xF6 divided by
+ 0x36, 0x12, 0x36, 0x24, 0x36, // 0xF7 approx
+ 0x06, 0x0F, 0x09, 0x0F, 0x06, // 0xF8 degree
+ 0x00, 0x00, 0x18, 0x18, 0x00, // 0xF9 big dot
+ 0x00, 0x00, 0x10, 0x10, 0x00, // 0xFA small dot
+ 0x30, 0x40, 0xFF, 0x01, 0x01, // 0xFB square root
+ 0x00, 0x1F, 0x01, 0x01, 0x1E, // 0xFC
+ 0x00, 0x19, 0x1D, 0x17, 0x12, // 0xFD power of two
+ 0x00, 0x3C, 0x3C, 0x3C, 0x3C, // 0xFE blob
+ //0x00, 0x00, 0x00, 0x60, 0x60, // 0xFF: Decimal point: 4 points
+ 0x00, 0x00, 0x00, 0x00, 0x40, // 0xFF: Decimal point, only 1 point
+};
+
+void setup_pins() {
+ DPRINTLN("VQC10 Clock - setup 74HC595 pins");
+ pinMode(latchPin, OUTPUT); regPin(latchPin,"latchPin");
+ pinMode(dataPin,OUTPUT); regPin(dataPin,"dataPin");
+ pinMode(clockPin,OUTPUT); regPin(clockPin,"clockPin");
+ pinMode(D5pin,INPUT); regPin(D5pin,"D5pin"); // NOTE: configured and registered but never read anywhere in this driver or the core file - purpose (spare sensor/button input?) is undocumented, see open questions
+ digitsOnly = false; // this driver can render full ASCII text (dot-matrix font), not just 0-9, so let the core firmware know
+ asciiConvert[16] = 9; // runtime override: digit-buffer code 16 (originally mapped to '~' above) is remapped to raw font index 9 ("small empty circle" glyph, see _font table) for a driver-specific extra symbol
+ // Deliberately does NOT call startTimer() here (unlike most other drivers): the hardware
+ // timer ISR (writeDisplay(), see below) is intentionally a no-op for VQC10, and indeed
+ // startTimer() is never invoked anywhere for this driver in the whole project. All actual
+ // refresh work happens cooperatively via writeDisplay2(), called repeatedly from loop()/
+ // Fdelay() in the core file - see the comment above writeDisplay() for why.
+}
+
+void inline shiftOutForward(byte myDataOut) {
+ // This shifts 8 bits out MSB first, on the rising edge of the clock, clock idles low
+ // Used for the "digit select" (Z) and "row select" shift-register bytes, whose bit
+ // order must match how those select lines are physically wired to the 74HC595 outputs.
+ for (int i=7; i>=0; i--) {
+ digitalWrite(clockPin, 0);
+ digitalWrite(dataPin, myDataOut & (1<MAXBRIGHTNESS) brightness = MAXBRIGHTNESS; //only for safety
+
+ if (autoBrightness && displayON) {
+ PWMtimeBrightness = max(PWM_min,PWM_max*lx/MAXIMUM_LUX); // ambient-light-driven dimming: scale row on-time by measured lux vs. MAXIMUM_LUX
+ }
+ else
+ PWMtimeBrightness = max(PWM_min,PWM_max*brightness/MAXBRIGHTNESS); // manual brightness: scale row on-time by configured day/night brightness level
+
+ iRow = 0;
+ // FIXED (was "loopMax = maxDigits;", i.e. NUMDIGITS=8): this loop indexes dot-matrix ROWS, which
+ // only range 0..NUMROWS-1 (NUMROWS=7, i.e. dat[i] is sized [NUMROWS]=[7]), not digit columns. The
+ // old bound made the loop run iRow = 0..7 (8 iterations) instead of 0..6 (7 iterations), so on the
+ // last iteration (iRow==7) "dat[i][iRow]" below read one element past the end of each digit's
+ // 7-byte row array - for i> 1;
+ else cmask = cmask << 1;
+ rows[r] = rows[r] << (7-NUMCOLS);
+ }
+}
+
+// Debug helper: prints one byte as 8 ASCII '0'/'1' characters (MSB first).
+void bPrint(byte in) {
+ for (int i=7;i>=0;i--) {
+ boolean b = in & (1<7) a=7;
+ for (int k=0;k slower multiplex frequency
+int DRAM_ATTR PWM_min = 2000;
+int DRAM_ATTR PWM_max = 10000;
+
+//#define dataPin 14 //D5
+//#define latchPin 27 //12 //D6
+//#define clkPin 26 //13 //D7
+
+// Segment bit patterns per displayable glyph code (index = the value stored in digit[]/newDigit[]).
+// Each byte is a bitmask over 8 abstract "segment slots" (bit7=a .. bit0=DP by convention elsewhere in
+// this codebase); generateBitTable() below translates these abstract bits into the actual physical
+// shift-register output bits via segmentEnablePins[] (defined per-CLOCK_NN in clocks.h). Codes 0-9 are
+// digits, 10=blank, 11=minus, 12=a standalone decimal-point glyph, 13+ are extra characters (A, P, C,
+// degree symbol, %, etc.) used for date/temperature/humidity/IP-address display - see clocks.h's
+// TEMP_CHARCODE/GRAD_CHARCODE/PERCENT_CHARCODE and the core file's displayTemp()/displayHumid()/writeIpTag().
+byte DRAM_ATTR charDefinition[] = {
+ B11111100, //0: abcdef
+ B01100000, //1: bc
+ B11011010, //2: abdeg
+ B11110010, //3: abcdg
+ B01100110, //4: bcfg
+ B10110110, //5: acdfg
+ B10111110, //6: acdefg
+ B11100000, //7: abc
+ B11111110, //8: abcdefg
+ B11110110, //9: abcdfg
+ B00000000, // : BLANK (10)
+ B00000010, //-: minus (11)
+ B00000001, // decimal point (12)
+ B11101110, // A abcefg (13)
+ B11001110, // P abefg (14)
+ B10011100, // C adef (15)
+ B11000110, //grad (upper circle) abfg (16)
+ B10110100, //% acdf (17)
+ B00111010, //lower circle cdeg (18)
+ B01100000, //I bc (19)
+ B10001110 //F aefg (20)
+};
+
+#define MAXCHARS sizeof(charDefinition)
+#define MAXSEGMENTS sizeof(segmentEnablePins)
+int maxDigits = sizeof(digitEnablePins);
+
+uint32_t DRAM_ATTR charTable[MAXCHARS]; //generated pin table from segmentDefinitions
+uint32_t DRAM_ATTR segmentEnableBits[MAXSEGMENTS]; //bitmaps, generated from EnablePins tables
+uint32_t DRAM_ATTR animationMaskBits[5];
+uint32_t DRAM_ATTR digitEnableBits[12];
+
+// This driver refreshes the whole display purely from the writeDisplay() ISR below (hardware-timer
+// driven multiplex), so there is nothing left to do for a "blocking, one-shot" write - required by
+// the common driver interface, but intentionally a no-op here.
+void IRAM_ATTR writeDisplaySingle() { }
+
+void setup_pins(){
+ DPRINT("ZM1500 clock "); DPRINT(maxDigits); DPRINTLN(" digits - setup pins");
+ pinMode(dataPin, OUTPUT); regPin(dataPin,"dataPin");
+ pinMode(latchPin,OUTPUT); regPin(latchPin,"latchPin");
+ pinMode(clkPin,OUTPUT); regPin(clkPin,"clkPin");
+ digitalWrite(latchPin,LOW);
+ digitalWrite(clkPin,LOW);
+ generateBitTable(); //pre-compute charTable[]/digitEnableBits[]/animationMaskBits[] from clocks.h's pin arrays
+ startTimer(); //arms the ESP32 hardware timer that drives writeDisplay() below
+}
+
+// Hardware-timer ISR: multiplexes one digit position per "ON" call, using a two-phase (ON/OFF)
+// duty-cycle so the *ratio* of ON time to OFF time controls perceived brightness (a software PWM),
+// then re-arms itself (timerAlarmWrite/timerAlarmEnable) with however long the next phase should
+// last. Runs continuously in the background once startTimer() has been called from setup_pins().
+void IRAM_ATTR writeDisplay(){ //void IRAM_ATTR writeDisplay(){
+
+ static DRAM_ATTR int pos = maxDigits;
+ static DRAM_ATTR boolean state=0;
+ static DRAM_ATTR int brightness;
+ static DRAM_ATTR uint32_t PWMtimeBrightness=PWM_min;
+ uint32_t timer = PWMrefresh;
+ uint32_t val = 0;
+
+ portENTER_CRITICAL_ISR(&timerMux);
+ noInterrupts();
+ intCounter++;
+
+ if (state) { //ON state: advance to the next digit position and compute its bit pattern
+ pos++;
+ if (pos>maxDigits-1) { //go to the tube#0
+ pos = 0;
+ // Brightness (and the resulting PWMtimeBrightness ON-duration) is only recomputed once per
+ // full multiplex sweep (i.e. when we wrap back to position 0), not on every single digit -
+ // this keeps all digits at the same, consistently-updated brightness within one sweep.
+ brightness = displayON ? prm.dayBright : prm.nightBright;
+ if (brightness>MAXBRIGHTNESS) brightness = MAXBRIGHTNESS; //only for safety
+ if (autoBrightness && displayON) { //change brightness only on the tube#0
+ PWMtimeBrightness = max(PWM_min,PWM_max*lx/MAXIMUM_LUX);
+ }
+ else
+ PWMtimeBrightness = max(PWM_min,PWM_max*brightness/MAXBRIGHTNESS);
+ }
+ // Look up this position's live glyph, apply the wipe-animation mask (changeDigit()'s animMode 5,
+ // in the core .ino) and OR in a standalone decimal-point glyph (index 12) if this tube's dot
+ // should be lit - digit[]/animMask[]/digitDP[] are all written by the core file's display
+ // pipeline (displayTimeX()/changeDigit()) before this ISR ever reads them.
+ val = uint32_t(charTable[digit[pos]]); //the full bitmap to send to the shift-register chain
+ if (animMask[pos]>0) val &= uint32_t(animationMaskBits[animMask[pos]-1]); //animationMode 6, mask characters from up to down and back
+ if (digitDP[pos]) val = val | uint32_t(charTable[12]); //Decimal Point
+
+ timer = PWMtimeBrightness;
+ timerON = timer; //debug only
+ timerOFF = PWMrefresh-PWMtimeBrightness; //debug only
+ }
+ else { //OFF state: blank the shift-register output for the remainder of this digit's time-slice
+ timer = PWMrefresh-PWMtimeBrightness;
+ val = 0;
+ }
+
+ if (timer<500) timer = 500; //safety only...
+ if ( (brightness == 0) || (!radarON)) val = 0; //blank digit
+ val = val | digitEnableBits[pos]; //combine segment bits (val) with this tube's select bit
+
+ // Bit-bang val out LSB-first over 20 clock pulses - only the low 20 bits of the 32-bit val
+ // actually reach the hardware (8 segment bits + up to 12 digit-select bits, see the file header).
+ for (int i=0; i<20; i++) {
+ if (val & uint32_t(1 << i)) //reverse: (1 << (19 - i)))
+ digitalWrite(dataPin, HIGH);
+ else
+ digitalWrite(dataPin, LOW);
+ digitalWrite(clkPin,HIGH); //asm volatile ("nop"); //Clock impulse
+ digitalWrite(clkPin,LOW); //asm volatile ("nop");
+ } //end for
+
+ // Pulse latchPin to atomically move the just-shifted-in 20 bits from the 74HC595's internal shift
+ // register to its output latches - this is the instant the physical display actually changes.
+ digitalWrite(latchPin,HIGH); //asm volatile ("nop");
+ digitalWrite(latchPin,LOW);
+ state = !state;
+
+ portEXIT_CRITICAL_ISR(&timerMux);
+ timerAlarmWrite(ESP32timer, timer, true); //re-arm the hardware timer for the just-computed phase duration
+ timerAlarmEnable(ESP32timer);
+ interrupts();
+}
+
+
+// One-time setup helper (called from setup_pins()): pre-computes the lookup tables writeDisplay()'s
+// ISR needs every cycle, so the ISR itself never has to do bit-shifting arithmetic based on the raw
+// clocks.h pin arrays - just plain table lookups, keeping the interrupt handler fast.
+void generateBitTable() {
+uint32_t out;
+
+ DPRINTLN("--- Generating segment pins bitmap ---");
+ for (int i=0;i> ");
+
+ charTable[i] = out;
+ DPRINTLN(charTable[i],BIN);
+ } //end for
+
+ // "+8" shifts the digit-select bit up past the 8 segment-enable bit positions (0-7, from
+ // segmentEnablePins[] above), so a combined value (segment bits | digitEnableBits[pos]) never
+ // collides: segment data occupies bits 0-7, tube selection occupies bits 8-19 (up to 12 tubes).
+ DPRINTLN("--- Generating digit pins bitmap ---");
+ for (int i=0;i ... #endif". This file is
+ the switchboard that decides which one of those driver bodies actually survives the preprocessor:
+
+ 1. Exactly ONE "#define CLOCK_NN" below (out of the ~65 listed) should be left un-commented -
+ that selects which physical clock build this firmware image is for.
+ 2. That CLOCK_NN's "#ifdef CLOCK_NN ... #endif" block (further down in this file) then defines:
+ - a DRIVER-SELECTION macro (exactly one), e.g. MAX6921, MAX6921_ESP32, HV5122, NEWHV5122,
+ PT6311, PT6355, ZM1500_ESP32, MULTIPLEX74141_ESP32, VQC10, WORDCLOCK, MULE_V2, TUBE1CLOCK,
+ Numitron_4511N, SN75512, SN75518_ESP32, MM5450, MAX7219CNG, NO_MULTIPLEX74141,
+ NO_MULTIPLEX_ESP32, PCF_74141, DUMMY - this is what makes ONE specific driver .ino's
+ "#ifdef " body compile, and leaves every other driver file's body empty.
+ - the physical GPIO pin assignments for that driver (names vary per driver family - e.g.
+ digitEnablePins[]/segmentEnablePins[]/ABCDPins[] for BCD/segment-style drivers, or
+ PIN_LE/PIN_CLK/PIN_DATA/PIN_BL for MAX6921-style shift-register drivers).
+ - optional feature toggles for that specific build (USE_NEOPIXEL, USE_RTC, USE_GPS,
+ USE_DALLAS_TEMP, USE_MQTT, ALARMSPEAKER_PIN, etc.) - only define the ones this build needs.
+ 3. Right at the end of this file (search for "if not defined, here is defined as NOT USED") a big
+ block of "#ifndef X / #define X / #endif" pairs fills in a sane default for every
+ OPTIONAL parameter a CLOCK_NN block didn't bother to set explicitly (e.g. pins default to -1
+ meaning "not connected", brightness/timing constants get conservative defaults). This is what
+ lets each CLOCK_NN block above stay short - it only needs to override what's actually different
+ for that particular physical build.
+
+ To add a brand-new clock: copy the block of an existing, similar CLOCK_NN, give it the next free
+ number, add its "//#define CLOCK_NN ..." line to the list below (commented out until you're ready to
+ build it), and write a new driver .ino file if the hardware needs a genuinely new driver (or reuse an
+ existing driver macro if the same driver chip is used with different pins/tube count).
+
+ Driver-macro -> implementing .ino file quick reference (see docs/DEVELOPER_GUIDE.md section 7 for the
+ full table with which CLOCK_NN uses which):
+ MULTIPLEX74141 / MULTIPLEX74141_ESP32 -> multiplex74141.ino / multiplex74141_ESP32.ino
+ NO_MULTIPLEX74141 / NO_MULTIPLEX_ESP32 -> no_multiplex74141.ino / no_multiplex_ESP32.ino
+ PCF_74141 -> pcf_multiplex74141.ino
+ MAX6921 / MAX6921_ESP32 -> MAX6921.ino / MAX6921_ESP32.ino
+ HV5122 (orphaned, unused) / NEWHV5122 -> HV5122.ino / HV5122new.ino
+ PT6311, PT6355 -> PT6311.ino, PT6355.ino
+ SN75512 (orphaned, unused) -> SN75512.ino
+ SN75518_ESP32 -> SN75518.ino
+ MM5450, MAX7219CNG -> MM5450.ino, MAX7219CNG.ino
+ Numitron_4511N -> Numitron_4511N.ino
+ VQC10 -> VQC10.ino
+ TUBE1CLOCK -> 1tube.ino
+ MULE_V2 -> mulev2.ino
+ 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)
+*/
+
+//#define CLOCK_1 //8266, UNFI PCB clock, 4x IN-16 tubes
+//#define CLOCK_2 //8266, UNFI PCB clock, 4x Z-570M tubes
+//#define CLOCK_3 //8266, PCB less clock, IV-18 VFD tube https://www.thingiverse.com/thing:3417955
+//#define CLOCK_4 //8266, custom clock, 4x IV-11 VFD tubes
+//#define CLOCK_5 //8266, UNFI PCB clock IVL-2-5/7 tube with RTC
+//#define CLOCK_6 //8266, GP PCB clock, 4x IN-1 tubes and 4x74141 driver (NON-MULTIPLEX)
+//#define CLOCK_6A //8266, UNFI PCB clock, 4x Z568 tubes and 4x74141 driver (NON-MULTIPLEX)
+//#define CLOCK_7 //8266 UNFI PCB clock 6x Z574M/Z573M tubes, tube selection by PCF8574 chip
+//#define CLOCK_8 //8266 GP PCB v3 clock with 4x IV-16 Numitron tubes (plexi box)
+//#define CLOCK_9 //8266 GP PCB v1 clock with 4x IV-16 Numitron tubes (brown box)
+//#define CLOCK_10 //8266 LED Clock with MAX7219 chip
+//#define CLOCK_11 //8266 LED Clock with MM5450 chip
+//#define CLOCK_12 //8266 VFD Clock with PT6355 chip
+//#define CLOCK_13 //8266 GP PCB v1 clock with 4x IV-16 Numitron tubes + GPS timesync (white box)
+//#define CLOCK_14 //8266 6X LD8035 VFD-clock
+//#define CLOCK_15 //8266 6X IV-6 VFD-clock RGB
+//#define CLOCK_15a //8266 6X IV-6 VFD-clock RGB
+//#define CLOCK_16 //8266 4X IV-6, 1X LD8035
+//#define CLOCK_17 //8266 + PT6311 4X IV-22, russian clock
+//#define CLOCK_18 //8266 IV-18 VFD-clock RGB
+//#define CLOCK_20 //8266 D1-mini, P.S. PCB 4xIN14 clock-thermometer
+//#define CLOCK_21 //8266 D1-mini, P.S. PCB 4xIN14 thermometer / humidity
+//#define CLOCK_22 //8266 NODEMCU, P.S. PCB 4xIN14 thermometer / humidity
+//#define CLOCK_23 ////ESP32 D1 mini, P.S. PCB 3xIN14 1xIN-19A thermometer / humidity
+//#define CLOCK_23A //ESP32 WROOM, U.Z. PCB 4xZ566M 1xIN-19A thermometer / humidity
+//#define CLOCK_23B //ESP32 WROOM U.Z. PCB Z571M+4xZ566M+IN-19A clock/thermometer/humidity
+//#define CLOCK_26 //ESP32 D1 mini, UNFI 6 x IV-11 VFD tubes clock, SHT21
+//#define CLOCK_30 //ESP32 prototype, UNFI PCB clock, 6 x IV-11 VFD tubes
+//#define CLOCK_31 //ESP32 prototype, UNFI PCB board, 6 x Z573M Nixie tubes
+//#define CLOCK_32 //ESP32 prototype, UNFI 6 x IV-11 VFD tubes clock, DHT22 sensor
+//#define CLOCK_33 //TOM025 ESP32, Pálfi S. board, 6 x Z573M Nixie tubes
+//#define CLOCK_34 //Mule V2 ESP32, board, 6 x SA40 LED Display
+//#define CLOCK_35 //ESP32, 6x Z566M and 3x74595 6x74141 (NON-MULTIPLEX)
+//#define CLOCK_36 //ESP32, 4x IN-1 tubes and 4x74141 driver (NON-MULTIPLEX)
+//#define CLOCK_40 //V1 ESP32, UNFI 6 x IV-11 VFD tubes clock
+//#define CLOCK_41 //V2 ESP32, UNFI 6 x IV-11 VFD tubes clock (rewired revision, átkötés)
+//#define CLOCK_42 //V3 ESP32, UNFI 6 x IV-11 VFD tubes clock
+//#define CLOCK_43 //V1 ESP32, UNFI 6 x Z573M Nixie tubes
+//#define CLOCK_44 //V2 ESP32, UNFI 6 x Z573M Nixie tubes (rewired revision, átkötés)
+//#define CLOCK_45 //V3 ESP32, UNFI 6 x Z573M Nixie tubes
+//#define CLOCK_46 //V1 ESP32, UNFI 6 x IV-9 Numitron clock
+//#define CLOCK_47 //V3 ESP32, UNFI 10-LT-50G VFD tubes clock
+//#define CLOCK_48 //WROOM ESP32, UNFI SN75518 6xIV-17 clock
+//#define CLOCK_49 //WROOM ESP32, UNFI SN75518 6xIV-11 clock
+//--------------------------------------------------------------------------------------------------------------
+//#define CLOCK_50 //V1 ESP32, UNFI 2xHV5122 PCB version, 6xZ573 clock
+//#define CLOCK_51 //V2 ESP32, UNFI 2xHV5122 PCB version, 6xZ573 clock
+//#define CLOCK_52 //ESP32, P.S. 2xHV5122 PCB version, 6xIN18 clock PROTOTYPE TESTING!!!
+//#define CLOCK_53 //ESP8266, UNFI 2xHV5122 PCB version, 6xZ573 clock PROTOTYPE TESTING!!!
+//#define CLOCK_54 // D1 R32 ESP32 (Uno compatible), + NCS312 Nixie clock KIT!!!
+//#define CLOCK_55 //ESP8266, UNFI 2xHV5122 PCB version, 6xZ573 clock
+//#define CLOCK_56 //WROOM ESP32, UNFI 2xHV5122 6xZ573 clock /flat panel/
+//#define CLOCK_57 //V2-WROOM ESP32, UNFI 2xHV5122 6xZM1000 clock /flat panel
+//#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_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
+
+//______________________ESP8266 CLOCKS by UNFI and GP ______________________________________________________
+#ifdef CLOCK_1 //8266, UNFI PCB clock, 4x IN-16 tubes
+#define DEBUG
+#define FW "fw1" //firmware name
+#define MAXBRIGHTNESS 255
+//#define USE_NEOPIXEL
+//byte tubePixels[] = {3,2,1,0}; //4 tubes, single leds, reverse direction
+//#define USE_DALLAS_TEMP
+//#define TEMP_DALLAS_PIN -1 //Dallas temp sensor pin. If not used, SET TO -1
+#define MULTIPLEX74141
+byte digitEnablePins[] = {14, 12, 13, 15}; //IN16 4x tube clock
+byte ABCDPins[4] = {2, 4, 5, 0};
+#define DP_PIN -1 // decimalPoint inside Nixie tube, set -1, if not used!
+#define COLON_PIN 16 //Blinking Colon pin. If not used, SET TO -1
+#define AP_NAME "UNICLOCK"
+#define AP_PASSWORD ""
+#define WEBNAME "IN-16 Nixie Clock"
+//#define DEFAULT_SSID ""
+//#define DEFAULT_PSW ""
+//#define USE_WIFIMANAGER
+
+#define USE_GPS
+#define SPEEDOMETER //use clock as speedometer
+#define SPEEDOMETER_SWITCH -1 //optional pin for mode switch: clock / speedometer
+#endif
+
+#ifdef CLOCK_2 //8266, UNFI PCB clock, 4x Z-570M tubes
+#define DEBUG
+#define FW "fw2" //firmware name
+#define MAXBRIGHTNESS 100
+//#define USE_NEOPIXEL
+//#define USE_DALLAS_TEMP
+#define TEMP_DALLAS_PIN -1 //Dallas temp sensor pin.
+#define MULTIPLEX74141
+byte digitEnablePins[] = {13, 12, 14, 15};
+byte ABCDPins[4] = {16, 5, 4, 0};
+#define DP_PIN -1 // decimalPoint inside Nixie tube, set -1, if not used!
+#define COLON_PIN 2 //Blinking Colon pin. If not used, SET TO -1
+#define ALARMSPEAKER_PIN -1 //Alarm buzzer pin
+#define ALARMBUTTON_PIN -1 //Alarm switch off button pin
+#define ALARM_ON HIGH //How to switch ON alarm buzzer
+#define AP_NAME "UNICLOCK"
+#define AP_PASSWORD ""
+#define WEBNAME "Z-570M Nixie Clock"
+//#define DEFAULT_SSID ""
+//#define DEFAULT_PSW ""
+//#define USE_WIFIMANAGER
+//#define DISABLE_NIGHT_ANIMATION
+#endif
+
+#ifdef CLOCK_3 //8266, PCB less clock, IV-18 VFD tube https://www.thingiverse.com/thing:3417955
+#define DEBUG
+#define FW "fw3" //firmware name
+#define MAXBRIGHTNESS 100
+#define USE_DALLAS_TEMP
+#define TEMP_DALLAS_PIN 4 //Dallas temp sensor pin. If not used, SET TO -1
+#define MAX6921
+byte segmentEnablePins[] = {0, 2, 5, 6, 4, 1, 3, 7}; //segment enable OUTbits of MAX6921 (a,b,c,d,e,f,g,DP) (You MUST define always 8 Pins!!!)
+byte digitEnablePins[] = {18, 11, 17, 12, 16, 13, 14, 15}; //19}; //digit enable OUTbits of MAX6921 (1,2,3,4,5,6,7,8) (You may define any number)
+//MAX6921 pins
+#define PIN_LE 12 // D6 Shift Register Latch Enable
+#define PIN_CLK 13 // D7 Shift Register Clock
+#define PIN_DATA 14 // D5 Shift Register Data
+#define PIN_BL 15 // D8 Shift Register Blank (1=display off 0=display on)
+#define AP_NAME "UNICLOCK"
+#define AP_PASSWORD ""
+#define WEBNAME "IV-18 VFD Clock"
+//#define DEFAULT_SSID ""
+//#define DEFAULT_PSW ""
+//#define USE_WIFIMANAGER
+//#define DISABLE_NIGHT_ANIMATION
+#endif
+
+
+#ifdef CLOCK_4 //8266, custom clock, 4x IV-11 VFD tubes
+#define DEBUG
+#define FW "fw4" //firmware name
+#define MAXBRIGHTNESS 100
+#define MAX6921
+byte segmentEnablePins[] = {7, 5, 1, 0, 2, 4, 6, 3}; //segment enable OUTbits of MAX6921 (a,b,c,d,e,f,g,DP) (You MUST define always 8 Pins!!!)
+byte digitEnablePins[] = {19, 18, 17, 16}; //digit enable OUTbits of MAX6921 (1,2,3,4,5,6,7,8) (You may define any number)
+//MAX6921 pins
+#define PIN_LE 12 // D6 Shift Register Latch Enable
+#define PIN_CLK 13 // D7 Shift Register Clock
+#define PIN_DATA 14 // D5 Shift Register Data
+#define PIN_BL 15 // D8 Shift Register Blank (1=display off 0=display on)
+#define USE_NEOPIXEL
+byte tubePixels[] = {0, 9, 1, 9, 2, 9, 3}; //4 tubes, single leds, 3 leds not used
+//#define RADAR_PIN 21
+#define RADAR_TIMEOUT 5 //(min)
+//#define TUBE_POWER_PIN 2
+//#define TUBE_POWER_ON LOW
+#define LIGHT_SENSOR_PIN A0 //Only ADC pins are usable! for example: 34,35,36,39...
+#define MAXIMUM_LUX 100 //Lux level for maximum tube brightness
+#define LUX_CALC_SCALAR 12518931 * 1.2
+#define AP_NAME "UNICLOCK"
+#define AP_PASSWORD ""
+#define WEBNAME "IV-11 VFD Clock"
+//#define DEFAULT_SSID ""
+//#define DEFAULT_PSW ""
+//#define USE_WIFIMANAGER
+//#define DISABLE_NIGHT_ANIMATION
+#endif
+
+#ifdef CLOCK_5 //8266, UNFI PCB clock IVL-2-5/7 tube with RTC
+//#define DEBUG
+#define FW "fw5" //firmware name
+#define USE_DALLAS_TEMP
+#define MAXBRIGHTNESS 100
+#define TEMP_DALLAS_PIN 3
+#define USE_RTC
+#define PIN_MODE_SWITCH 17 //Analog A0 port!!!
+#define PIN_FLD_BUTTON 16
+#define PIN_SET_BUTTON 0
+#define PIN_SDA 5 //D1 SDA/SCL swapped
+#define PIN_SCL 4 //D2
+#define MAX6921
+//Fill this table with the OUT positions of the MAX6921 chip!
+byte segmentEnablePins[] = {11, 13, 5, 8, 3, 12, 2, 1}; //segment enable OUTbits of MAX6921 (a,b,c,d,e,f,g,DP) (You MUST define always 8 Pins!!!)
+byte digitEnablePins[] = {10, 9, 4, 0}; //segment enable OUTbits of MAX6921 (1,2,3,4,5,6,7,8)
+//MAX6921 pins
+#define PIN_LE 13 // D6 Shift Register Latch Enable
+#define PIN_CLK 12 // D7 Shift Register Clock
+#define PIN_DATA 14 // D5 Shift Register Data
+#define PIN_BL 15 // D8 Shift Register Blank (1=display off 0=display on)
+#define AP_NAME "UNICLOCK"
+#define AP_PASSWORD ""
+#define WEBNAME "IVL-2 VFD Clock"
+//#define DEFAULT_SSID ""
+//#define DEFAULT_PSW ""
+//#define USE_WIFIMANAGER
+//#define DISABLE_NIGHT_ANIMATION
+#endif
+
+#ifdef CLOCK_6 //8266, GP PCB clock, 4x IN-1 tubes and 4x74141 driver (NON-MULTIPLEX)
+#define DEBUG
+#define FW "fw6" //firmware name
+#define MAXBRIGHTNESS 100
+//#define USE_DALLAS_TEMP
+//#define TEMP_DALLAS_PIN -1
+#define USE_NEOPIXEL
+byte tubePixels[] = {0, 0, 1, 1, 2, 2, 3, 3, 3, 3, 2, 2, 1, 1, 0, 0, 0}; //4 tubes, double row, 17 leds (GP)
+#define NO_MULTIPLEX74141
+byte tubes[] = {3, 2, 1, 0}; //4 tubes, old clock... in clocks.h
+#define dataPin 14 //D5
+#define latchPin 12 //D6
+#define clkPin 13 //D7
+#define TEMP_DALLAS_PIN -1
+#define COLON_PIN 2 //Blinking Colon pin. If not used, SET TO -1 ( old IN-1-Clock: white:2 brown: SDA)
+#define AP_NAME "UNICLOCK"
+#define AP_PASSWORD ""
+#define WEBNAME "IN-1 Nixie Clock"
+//#define DEFAULT_SSID ""
+//#define DEFAULT_PSW ""
+//#define USE_WIFIMANAGER
+//#define DISABLE_NIGHT_ANIMATION
+#endif
+
+#ifdef CLOCK_6A //8266, UNFI PCB clock, 4x Z568 tubes and 4x74141 driver (NON-MULTIPLEX)
+#define DEBUG
+#define FW "fw6a" //firmware name
+#define MAXBRIGHTNESS 100
+#define USE_DALLAS_TEMP
+#define TEMP_DALLAS_PIN 4
+#define GRAD_CHARCODE -1 //no display grad char
+#define TEMP_CHARCODE -1 //no display temp char
+//#define USE_NEOPIXEL
+//byte tubes[] = {2,3,0,1}; //4 tubes, old clock...
+byte tubes[] = {5, 4, 2, 3, 0, 1}; //6 tubes, Kovecses clock...
+#define NO_MULTIPLEX74141
+#define dataPin 14 //D5
+#define latchPin 12 //D6
+#define clkPin 13 //D7
+#define COLON_PIN 15 //Blinking Colon pin. If not used, SET TO -1 ( old IN-1-Clock: white:2 brown: SDA)
+#define AP_NAME "UNICLOCK"
+#define AP_PASSWORD ""
+#define WEBNAME "Z568M Nixie Clock"
+//#define DEFAULT_SSID ""
+//#define DEFAULT_PSW ""
+//#define USE_WIFIMANAGER
+#endif
+
+
+#ifdef CLOCK_7 //8266 UNFI PCB clock 6x Z574M/Z573M tubes, tube selection by PCF8574 chip
+#define DEBUG
+#define FW "fw7" //firmware name
+#define MAXBRIGHTNESS 100
+#define USE_DALLAS_TEMP
+#define TEMP_DALLAS_PIN 0
+//#define USE_NEOPIXEL
+//byte tubePixels[] = {0,1,2,3,4,5}; //6 tubes, single leds
+//byte tubePixels[] = {5,4,3,2,1,0}; //6 tubes, single leds, reverse direction
+#define LEFTDECIMAL true //set true (Z574M), false to Z573M
+#define TEMP_CHARCODE -1 //disable char => shift display right with 1 digit
+//#define GRAD_CHARCODE -1 //disable char => shift display right with 1 digit
+//#define LIGHT_SENSOR_PIN A0 //Only ADC pins are usable! for example: 34,35,36,39...
+#define MAXIMUM_LUX 100 //Lux level for maximum tube brightness
+#define LUX_CALC_SCALAR 12518931 * 1.2
+#define PCF_74141
+#define AP_NAME "UNICLOCK"
+#define AP_PASSWORD ""
+#define WEBNAME "Z574M Nixie Clock"
+//#define DEFAULT_SSID ""
+//#define DEFAULT_PSW ""
+//#define USE_WIFIMANAGER
+//#define DISABLE_NIGHT_ANIMATION
+#endif
+
+#ifdef CLOCK_8 //8266 GP PCB v3 clock with 4x IV-16 Numitron tubes (plexi box) //Flash size: 1MB (FS:160k, OTA:422k)
+//#define DEBUG
+#define FW "fw8" //firmware name
+#define MAXBRIGHTNESS 100
+#define PCB_VERSION 3 //1,2 or 3
+#define LTBIpin 5
+byte digitEnablePins[] = {13, 12, 14, 16}; //define here the digit enable pins from 4 to 8
+byte ABCDPins[4] = {4, 0, 2, 15};
+#define USE_NEOPIXEL
+byte tubePixels[] = {3, 2, 6, 1, 0}; //Numitron 4 tubes, 4 x single leds + 1. The extra led in the middle is not used, is always dark!
+#define Numitron_4511N
+#define ALARMSPEAKER_PIN -1 //Alarm buzzer pin
+#define ALARMBUTTON_PIN -1 //Alarm switch off button pin
+#define ALARM_ON HIGH //How to switch ON alarm buzzer
+#define AP_NAME "UNICLOCK"
+#define AP_PASSWORD ""
+#define WEBNAME "Numitron Clock"
+//#define DEFAULT_SSID ""
+//#define DEFAULT_PSW ""
+//#define USE_WIFIMANAGER
+//#define DISABLE_NIGHT_ANIMATION
+#endif
+
+#ifdef CLOCK_9 //8266 GP PCB v1 clock with 4x IV-16 Numitron tubes (brown box) //Flash size: 1MB (FS:160k, OTA:422k)
+#define DEBUG
+#define FW "fw9" //firmware name
+#define MAXBRIGHTNESS 100
+#define PCB_VERSION 1 //1,2 or 3
+#define Numitron_4511N
+#define LTBIpin 5
+byte digitEnablePins[] = {13, 12, 14, 16}; //define here the digit enable pins from 4 to 8
+byte ABCDPins[4] = {4, 0, 2, 15};
+#define AP_NAME "UNICLOCK"
+#define AP_PASSWORD ""
+#define WEBNAME "Numitron Clock"
+//#define DEFAULT_SSID ""
+//#define DEFAULT_PSW ""
+//#define USE_WIFIMANAGER
+//#define DISABLE_NIGHT_ANIMATION
+#endif
+
+#ifdef CLOCK_10 //8266 LED Clock with MAX7219 chip
+#define DEBUG
+#define MAXBRIGHTNESS 10
+#define FW "fw10" //firmware name
+#define MAX7219CNG
+#define AP_NAME "UNICLOCK"
+#define AP_PASSWORD ""
+#define WEBNAME "LED UniClock"
+//#define DEFAULT_SSID ""
+//#define DEFAULT_PSW ""
+//#define USE_WIFIMANAGER
+//#define DISABLE_NIGHT_ANIMATION
+#endif
+
+#ifdef CLOCK_11 //8266 LED Clock with MM5450 chip
+#define DEBUG
+#define FW "fw11" //firmware name
+#define MAXBRIGHTNESS 100
+//#define USE_DALLAS_TEMP
+//#define TEMP_DALLAS_PIN 2
+//#define RADAR_PIN 4
+#define RADAR_TIMEOUT 5 //min
+#define LIGHT_SENSOR_PIN A0 //Only ADC pins are usable! for example: 34,35,36,39...
+#define MAXIMUM_LUX 100 //Lux level for maximum tube brightness
+#define MM5450
+#define AP_NAME "UNICLOCK"
+#define AP_PASSWORD ""
+#define WEBNAME "LED UniClock"
+//#define DEFAULT_SSID ""
+//#define DEFAULT_PSW ""
+//#define USE_WIFIMANAGER
+//#define DISABLE_NIGHT_ANIMATION
+#endif
+
+#ifdef CLOCK_12 //8266 VFD Clock with PT6355 chip
+#define DEBUG
+#define FW "fw12" //firmware name
+#define PT6355
+int maxDigits = 6;
+byte segmentEnablePins[] = {16, 7, 0, 1, 2, 3, 4, 5, 6, 8, 9, 11, 10, 12, 13, 14, 15}; //segment enable bits
+#define PIN_CS 22 //12 or 15 // D6 CS_
+#define PIN_CLK 16 // D7 Clock
+#define PIN_SIN 21 // D5 DataIN
+#define PIN_SDA 19
+#define PIN_SCL 23
+#define USE_DALLAS_TEMP
+#define TEMP_DALLAS_PIN 5
+#define ALARMSPEAKER_PIN 26 //Alarm buzzer pin
+#define ALARMBUTTON_PIN 4 //32 //Alarm switch off button pin
+#define ALARM_ON HIGH //How to switch ON alarm buzzer
+#define RADAR_PIN -1
+#define RADAR_TIMEOUT 5 //5min
+#define AP_NAME "UNICLOCK"
+#define AP_PASSWORD ""
+#define WEBNAME "VFD UniClock"
+//#define DEFAULT_SSID ""
+//#define DEFAULT_PSW ""
+//#define USE_WIFIMANAGER
+//#define DISABLE_NIGHT_ANIMATION
+#endif
+
+#ifdef CLOCK_13 //8266 GP PCB v1 clock with 4x IV-16 Numitron tubes + GPS timesync (white box)
+#define DEBUG
+#define FW "fw13" //firmware name
+#define MAXBRIGHTNESS 10
+#define USE_GPS
+#define PCB_VERSION 3 //1,2 or 3
+#define Numitron_4511N
+#define LTBIpin 5
+byte digitEnablePins[] = {13, 12, 14, 16}; //define here the digit enable pins from 4 to 8
+byte ABCDPins[4] = {4, 0, 2, 15};
+#define AP_NAME "UNICLOCK"
+#define AP_PASSWORD ""
+#define WEBNAME "Numitron UniClock"
+//#define DEFAULT_SSID ""
+//#define DEFAULT_PSW ""
+//#define USE_WIFIMANAGER
+//#define DISABLE_NIGHT_ANIMATION
+#endif
+
+#ifdef CLOCK_14 //8266 6X LD8035 VFD-clock
+#define DEBUG
+#define FW "fw14" //firmware name
+#define MAXBRIGHTNESS 100
+#define USE_DALLAS_TEMP
+#define TEMP_DALLAS_PIN 4 //Dallas temp sensor pin. If not used, SET TO -1
+#define MAX6921
+byte segmentEnablePins[] = {7, 9, 2, 4, 8, 5, 6, 3}; //segment enable OUTbits of MAX6921 (a,b,c,d,e,f,g,DP) (You MUST define always 8 Pins!!!)
+byte digitEnablePins[] = {14, 15, 19, 18, 17, 16}; //digit enable OUTbits of MAX6921 (1,2,3,4,5,6,7,8) (You may define any number)
+//MAX6921 pins
+#define PIN_LE 0 // D3 Shift Register Latch Enable
+#define PIN_CLK 2 // D4 Shift Register Clock
+#define PIN_DATA 5 // D1 Shift Register Data
+#define PIN_BL 15 // D8 Shift Register Blank (1=display off 0=display on)
+//#define LIGHT_SENSOR_PIN A0 //Only ADC pins are usable!
+//#define MAXIMUM_LUX 100 //Lux level for maximum tube brightness
+//#define LUX_CALC_SCALAR 12518931 * 1.2
+#define AP_NAME "UNFICLOCK"
+#define AP_PASSWORD ""
+#define WEBNAME "LD 8035 VFD Clock"
+//#define DISABLE_NIGHT_ANIMATION
+#endif
+
+#ifdef CLOCK_15 //8266 6X IV-6 VFD-clock RGB
+#define DEBUG
+#define FW "fw15" //firmware name
+#define MAXBRIGHTNESS 100
+#define USE_NEOPIXEL
+#define NEOPIXEL_PIN 3
+byte tubePixels[] = {0, 1, 2, 3, 4, 5}; //6 tubes, single leds
+#define USE_DALLAS_TEMP
+#define TEMP_DALLAS_PIN 4 //Dallas temp sensor pin. If not used, SET TO -1
+#define MAX6921
+byte segmentEnablePins[] = {7, 9, 2, 4, 8, 5, 6, 3}; //segment enable OUTbits of MAX6921 (a,b,c,d,e,f,g,DP) (You MUST define always 8 Pins!!!)
+byte digitEnablePins[] = {14, 15, 19, 18, 17, 16}; //digit enable OUTbits of MAX6921 (1,2,3,4,5,6,7,8) (You may define any number)
+//MAX6921 pins
+#define PIN_LE 0 // D3 Shift Register Latch Enable
+#define PIN_CLK 2 // D4 Shift Register Clock
+#define PIN_DATA 5 // D1 Shift Register Data
+#define PIN_BL 15 // D8 Shift Register Blank (1=display off 0=display on)
+#define ALARMSPEAKER_PIN 12 //Alarm buzzer pin
+#define ALARMBUTTON_PIN 13 //Alarm switch off button pin
+#define ALARM_ON HIGH //How to switch ON alarm buzzer
+//#define LIGHT_SENSOR_PIN A0 //Only ADC pins are usable!
+//#define MAXIMUM_LUX 100 //Lux level for maximum tube brightness
+//#define LUX_CALC_SCALAR 12518931 * 1.2
+#define AP_NAME "UNFICLOCK"
+#define AP_PASSWORD ""
+#define WEBNAME "IV-6 VFD Clock"
+//#define DISABLE_NIGHT_ANIMATION
+#endif
+
+#ifdef CLOCK_15a //8266 6X IV-6 VFD-clock RGB
+#define DEBUG
+#define FW "fw15A" //firmware name
+#define MAXBRIGHTNESS 100
+//#define USE_NEOPIXEL
+//#define NEOPIXEL_PIN 3
+#define PIN_FLD_BUTTON 16
+#define PIN_SET_BUTTON 13
+#define USE_RTC
+#define PIN_SDA 2
+#define PIN_SCL 0
+byte tubePixels[] = {0, 1, 2, 3, 4, 5}; //6 tubes, single leds
+#define USE_DALLAS_TEMP
+#define TEMP_DALLAS_PIN 5 //Dallas temp sensor pin. If not used, SET TO -1
+#define MAX6921
+byte segmentEnablePins[] = {10, 11, 16, 15, 14, 13, 12, 17}; //segment enable OUTbits of MAX6921 (a,b,c,d,e,f,g,DP) (You MUST define always 8 Pins!!!)
+byte digitEnablePins[] = {6, 7, 8, 9, 19, 18}; //digit enable OUTbits of MAX6921 (1,2,3,4,5,6,7,8) (You may define any number)
+//MAX6921 pins
+#define PIN_LE 12 // D3 Shift Register Latch Enable
+#define PIN_CLK 14 // D4 Shift Register Clock
+#define PIN_DATA 15 // D1 Shift Register Data
+#define PIN_BL 4 // D8 Shift Register Blank (1=display off 0=display on)
+//#define LIGHT_SENSOR_PIN A0 //Only ADC pins are usable!
+//#define MAXIMUM_LUX 100 //Lux level for maximum tube brightness
+//#define LUX_CALC_SCALAR 12518931 * 1.2
+#define AP_NAME "UNFICLOCK"
+#define AP_PASSWORD ""
+#define WEBNAME "IV-6 VFD Clock"
+//#define DISABLE_NIGHT_ANIMATION
+#endif
+
+#ifdef CLOCK_16 //8266 4X IV-6, 1X LD8035
+#define DEBUG
+#define FW "fw16" //firmware name
+#define MAXBRIGHTNESS 100
+#define USE_DALLAS_TEMP
+#define TEMP_DALLAS_PIN 4 //Dallas temp sensor pin. If not used, SET TO -1
+#define MAX6921
+byte segmentEnablePins[] = {7, 9, 2, 4, 8, 5, 6, 3}; //segment enable OUTbits of MAX6921 (a,b,c,d,e,f,g,DP) (You MUST define always 8 Pins!!!)
+byte digitEnablePins[] = {19, 18, 17, 16}; //digit enable OUTbits of MAX6921 (1,2,3,4,5,6,7,8) (You may define any number)
+//MAX6921 pins
+#define PIN_LE 0 // D3 Shift Register Latch Enable
+#define PIN_CLK 2 // D4 Shift Register Clock
+#define PIN_DATA 5 // D1 Shift Register Data
+#define PIN_BL 15 // D8 Shift Register Blank (1=display off 0=display on)
+//#define LIGHT_SENSOR_PIN A0 //Only ADC pins are usable!
+//#define MAXIMUM_LUX 100 //Lux level for maximum tube brightness
+//#define LUX_CALC_SCALAR 12518931 * 1.2
+#define AP_NAME "UNFICLOCK"
+#define AP_PASSWORD ""
+#define WEBNAME "IV-6 VFD Clock"
+//#define DISABLE_NIGHT_ANIMATION
+#endif
+
+#ifdef CLOCK_17 //8266 + PT6311 4X IV-22, russian clock
+#define DEBUG
+#define USE_RTC
+#define FW "fw17" //firmware name
+#define USE_NEOPIXEL
+#define NEOPIXEL_PIN 3
+byte tubePixels[] = {0, 1, 2, 3}; //4 tubes, single leds
+#define MAXBRIGHTNESS 10
+//#define USE_DALLAS_TEMP
+//#define TEMP_DALLAS_PIN 4 //Dallas temp sensor pin. If not used, SET TO -1
+#define USE_RTC
+#define PT6311
+byte segmentEnablePins[] = {4, 3, 1, 7, 6, 5, 2, 0};
+byte digitEnablePins[] = {0, 1, 2, 3};
+//PT6311 pins
+#define PIN_CLK 14 // Clock
+#define PIN_DIN 13 // DataIN
+#define PIN_LOAD 15 // STB pin
+//#define LIGHT_SENSOR_PIN A0 //Only ADC pins are usable!
+//#define MAXIMUM_LUX 100 //Lux level for maximum tube brightness
+//#define LUX_CALC_SCALAR 12518931 * 1.2
+#define PIN_SDA 4
+#define PIN_SCL 5
+#define AP_NAME "UNFICLOCK"
+#define AP_PASSWORD ""
+#define WEBNAME "IV-22 VFD Clock"
+//#define DISABLE_NIGHT_ANIMATION
+#endif
+
+#ifdef CLOCK_18 //8266 IV-18 VFD-clock RGB
+#define DEBUG
+#define FW "fw18" //firmware name
+#define MAXBRIGHTNESS 100
+#define USE_NEOPIXEL
+#define NEOPIXEL_PIN 3
+byte tubePixels[] = {0, 1, 2, 3, 4, 5}; //6 tubes, single leds
+//#define USE_DALLAS_TEMP
+//#define TEMP_DALLAS_PIN -1 //Dallas temp sensor pin. If not used, SET TO -1
+#define USE_DHT_TEMP
+#define DHTTYPE DHT11
+#define TEMP_DHT_PIN 12
+#define MAX6921
+byte segmentEnablePins[] = {13, 15, 18, 19, 17, 14, 16, 12}; //segment enable OUTbits of MAX6921 (a,b,c,d,e,f,g,DP) (You MUST define always 8 Pins!!!)
+byte digitEnablePins[] = {6, 10, 5, 4, 0, 3, 2, 1}; //digit enable OUTbits of MAX6921 (1,2,3,4,5,6,7,8) (You may define any number)
+//MAX6921 pins
+#define PIN_LE 4 // D4 Shift Register Latch Enable
+#define PIN_CLK 5 // D1 Shift Register Clock
+#define PIN_DATA 2 // D4 Shift Register Data
+#define PIN_BL 14 // D5 Shift Register Blank (1=display off 0=display on)
+#define ALARMSPEAKER_PIN 16 //Alarm buzzer pin
+#define ALARMBUTTON_PIN 13 //Alarm switch off button pin
+#define ALARM_ON HIGH //How to switch ON alarm buzzer
+//#define LIGHT_SENSOR_PIN A0 //Only ADC pins are usable!
+//#define MAXIMUM_LUX 100 //Lux level for maximum tube brightness
+//#define LUX_CALC_SCALAR 12518931 * 1.2
+#define AP_NAME "UNFICLOCK"
+#define AP_PASSWORD ""
+#define WEBNAME "IV-18 VFD Clock"
+//#define DISABLE_NIGHT_ANIMATION
+#endif
+
+//____________ P.S. clocks / thermometers ____________________________________________
+#ifdef CLOCK_20 //8266 D1-mini, P.S. PCB 4xIN14 clock-thermometer
+//#define DEBUG
+#define FW "fw20" //firmware name
+#define MAXBRIGHTNESS 100
+#define USE_DALLAS_TEMP
+#define TEMP_DALLAS_PIN 3 //RX
+#define MULTIPLEX74141
+byte digitEnablePins[] = {15, 13, 12, 14};
+byte ABCDPins[4] = {2, 4, 5, 0};
+#define DP_PIN -1 // decimalPoint inside Nixie tube, set -1, if not used!
+#define ENABLE_CLOCK_DISPLAY true //don't display date/time!!!
+#define DATE_REPEAT_MIN 3 //show date only every xxx minute. If zero, datum is never displayed
+#define SHIFT_TUBES_LEFT_BY_1 //shift left IP address by 1 tube the display, if a thermometer is used with spec tube
+#define TEMP_CHARCODE 15
+#define GRAD_CHARCODE 16
+#define PERCENT_CHARCODE 17
+#define COLON_PIN 1 //TX //Blinking Colon pin. If not used, SET TO -1
+#define LED_SWITCH_PIN 16 //external led backlight ON/OFF. If not used, SET TO -1
+#define AP_NAME "Nixie ORA"
+#define AP_PASSWORD ""
+#define WEBNAME "Nixie IN-14 Óra"
+#define DEFAULT_SSID ""
+#define DEFAULT_PSW ""
+//#define USE_WIFIMANAGER
+//#define DISABLE_NIGHT_ANIMATION
+#endif
+
+#ifdef CLOCK_21 //8266 D1-mini, P.S. PCB 3xIN14 1xIN-19A thermometer / humidity
+//#define DEBUG
+#define FW "fw21" //firmware name
+#define MAXBRIGHTNESS 100
+#define USE_DHT_TEMP
+#define DHTTYPE DHT22
+#define TEMP_DHT_PIN 3 //RX
+#define MULTIPLEX74141
+byte digitEnablePins[] = {15, 13, 12, 14};
+byte ABCDPins[4] = {2, 4, 5, 0};
+#define DP_PIN -1 // decimalPoint inside Nixie tube, set -1, if not used!
+#define ENABLE_CLOCK_DISPLAY false //don't display date/time!!!
+#define SHIFT_TUBES_LEFT_BY_1 //shift left IP address by 1 tube the display, if a thermometer is used with spec tube
+#define TEMP_CHARCODE 4
+#define GRAD_CHARCODE 16
+#define PERCENT_CHARCODE 7
+#define LED_SWITCH_PIN 16 //external led backlight ON/OFF. If not used, SET TO -1
+#define DECIMALPOINT_PIN 1 //TX //Nixie decimal point between digits. If not used, SET TO -1
+#define DATE_REPEAT_MIN 0 //show date only every xxx minute. If zero, datum is never displayed
+#define TEMP_START 01
+#define TEMP_END 45
+#define HUMID_START 45
+#define HUMID_END 59
+#define AP_NAME "Nixie Homero"
+#define AP_PASSWORD ""
+#define WEBNAME "Nixie Hőmérő"
+#define DEFAULT_SSID ""
+#define DEFAULT_PSW ""
+//#define USE_WIFIMANAGER
+//#define DISABLE_NIGHT_ANIMATION
+#endif
+
+#ifdef CLOCK_22 //8266 NODEMCU, P.S. PCB 3xIN14 1xIN-19A thermometer / humidity
+//#define DEBUG
+#define FW "fw22" //firmware name
+#define MAXBRIGHTNESS 100
+#define USE_NEOPIXEL
+byte tubePixels[] = {0, 1, 2, 3}; //4 tubes, single leds
+#define USE_DHT_TEMP
+#define DHTTYPE DHT22
+#define TEMP_DHT_PIN 1
+#define MULTIPLEX74141
+byte digitEnablePins[] = {15, 13, 12, 14};
+byte ABCDPins[4] = {2, 4, 5, 0};
+#define DP_PIN -1 // decimalPoint inside Nixie tube, set -1, if not used!
+#define ENABLE_CLOCK_DISPLAY false //don't display date/time!!!
+#define SHIFT_TUBES_LEFT_BY_1 //shift left by 1 tube the display, if a thermometer is used with spec tube
+#define TEMP_CHARCODE 5
+#define GRAD_CHARCODE 16
+#define PERCENT_CHARCODE 6
+#define DECIMALPOINT_PIN 16 //Nixie decimal point between digits. If not used, SET TO -1
+#define DATE_REPEAT_MIN 0 //show date only every xxx minute. If zero, datum is never displayed
+#define TEMP_START 01
+#define TEMP_END 45
+#define HUMID_START 45
+#define HUMID_END 59
+#define AP_NAME "Nixie Homero"
+#define AP_PASSWORD ""
+#define WEBNAME "Nixie Hőmérő"
+#define DEFAULT_SSID ""
+#define DEFAULT_PSW ""
+//#define USE_WIFIMANAGER
+//#define DISABLE_NIGHT_ANIMATION
+#endif
+
+#ifdef CLOCK_23 //ESP32 D1 mini, P.S. PCB 3xIN14 1xIN-19A thermometer / humidity
+#define DEBUG
+#define SIMULATE_TEMPERATURE_SENSOR
+#define FW "fw23" //firmware name
+#define MAXBRIGHTNESS 100
+//#define USE_NEOPIXEL
+//#define NEOPIXEL_PIN 22
+//byte tubePixels[] = {0,1,2,3}; //4 tubes, single leds
+//#define USE_DHT_TEMP
+#define DHTTYPE DHT22
+#define TEMP_DHT_PIN 1
+#define MULTIPLEX74141_ESP32
+//byte digitEnablePins[] = {26,18,33,19}; //Old sw,old ESP32
+byte digitEnablePins[] = {19, 33, 18, 26};
+byte ABCDPins[4] = {32, 16, 4, 17};
+#define DP_PIN -1 // decimalPoint inside Nixie tube, set -1, if not used!
+#define ENABLE_CLOCK_DISPLAY false //don't display date/time!!!
+#define SHIFT_TUBES_LEFT_BY_1 //shift left by 1 tube the display, if a thermometer is used with spec tube
+#define USE_SHT21 //I2C Temperature + humidity
+#define PIN_SDA 25 // you can set the used SDA and SCL pins
+#define PIN_SCL 21 // if it is not default value
+#define TEMP_CHARCODE 5
+#define GRAD_CHARCODE 10
+#define PERCENT_CHARCODE 6
+#define DECIMALPOINT_PIN 23 //Nixie decimal point between digits. If not used, SET TO -1
+#define DATE_REPEAT_MIN 0 //show date only every xxx minute. If zero, datum is never displayed
+#define TEMP_START 01
+#define TEMP_END 45
+#define HUMID_START 45
+#define HUMID_END 59
+#define AP_NAME "Nixie-Homero"
+#define AP_PASSWORD ""
+#define WEBNAME "Nixie Hőmérő"
+#define DEFAULT_SSID "***NET***"
+#define DEFAULT_PSW "Szentatya5"
+//#define USE_WIFIMANAGER
+#define DISABLE_NIGHT_ANIMATION
+//#define DISABLE_BROWNOUT
+//#define USE_MQTT
+#define MQTT_PREFIX "Nixie_Homero"
+#define USE_MASTER_CLOCK //enable it, if you want to get any data from MASTER CLOCK. This will be the sensor#0
+//#define USE_MASTER_TEMP //enable it, if you want to get temperature from MASTER CLOCK
+//#define USE_MASTER_HUMID //enable it, if you want to get humidity from MASTER CLOCK
+#define USE_MASTER_RADAR //enable it, if you want to get radar from MASTER CLOCK
+//#define MASTER_TEMPERATURE_TOPIC "homeassistant/sensor/10521c5e14c4/temperature/state"
+//#define MASTER_HUMIDITY_TOPIC "homeassistant/sensor/10521c5e14c4/humidity/state"
+#define MASTER_RADAR_TOPIC "Nixie_IN-18_Clock/sensor/10521c5e14c4/radar/state"
+#endif
+
+#ifdef CLOCK_23A //ESP32 WROOM, U.Z. PCB 4xZ566M 1xIN-19A thermometer / humidity
+#define DEBUG
+#define FW "fw23A" //firmware name
+#define MAXBRIGHTNESS 100
+byte tubePixels[] = {0, 1, 2, 3}; //4 tubes, single leds
+#define USE_DHT_TEMP
+#define DHTTYPE DHT11
+#define TEMP_DHT_PIN 19
+#define MULTIPLEX74141_ESP32
+byte digitEnablePins[] = {18, 13, 12, 27, 5}; //C/percent - tenths-of-degree (tizedfok) - degree ones (fok1) - tens-of-degree (fok10) - sign (előjel), pin 18
+byte ABCDPins[4] = {26, 33, 32, 25};
+#define COLON_PIN 14 // decimalPoint inside Nixie tube, set -1, if not used!
+#define ENABLE_CLOCK_DISPLAY false //don't display date/time!!!
+#define SHIFT_TUBES_LEFT_BY_1 //shift left by 1 tube the display, if a thermometer is used with spec tube
+#define USE_SHT21 //I2C Temperature + humidity
+#define PIN_SDA 21 // you can set the used SDA and SCL pins
+#define PIN_SCL 22 // if it is not default value
+#define TEMP_CHARCODE 4
+#define GRAD_CHARCODE 10
+#define PERCENT_CHARCODE 7
+#define PLUS_CHARCODE 0
+#define MINUS_CHARCODE 1
+#define DATE_REPEAT_MIN 0 //show date only every xxx minute. If zero, datum is never displayed
+#define TEMP_START 01
+#define TEMP_END 45
+#define HUMID_START 45
+#define HUMID_END 59
+#define AP_NAME "Nixie-Homero"
+#define AP_PASSWORD ""
+#define WEBNAME "Nixie Hőmérő"
+//#define DEFAULT_SSID ""
+//#define DEFAULT_PSW ""
+//#define USE_WIFIMANAGER
+//#define USE_MQTT
+//#define MQTT_PREFIX "Nixie_Homero"
+//#define USE_MASTER_CLOCK //enable it, if you want to get any data from MASTER CLOCK. This will be the sensor#0
+//#define USE_MASTER_TEMP //enable it, if you want to get temperature from MASTER CLOCK
+//#define USE_MASTER_HUMID //enable it, if you want to get humidity from MASTER CLOCK
+//#define USE_MASTER_RADAR //enable it, if you want to get radar from MASTER CLOCK
+//#define MASTER_TEMPERATURE_TOPIC "homeassistant/sensor/10521c5e14c4/temperature/state"
+//#define MASTER_HUMIDITY_TOPIC "homeassistant/sensor/10521c5e14c4/humidity/state"
+//#define MASTER_RADAR_TOPIC "Nixie_IN-18_Clock/sensor/3c6105172188/radar/state"
+//#define DISABLE_NIGHT_ANIMATION
+#endif
+
+#ifdef CLOCK_23B //ESP32 WROOM U.Z. PCB Z571M+4xZ566M+IN-19A clock/thermometer/humidity
+#define DEBUG
+#define FW "fw23B" //firmware name
+#define THERMOMETER_CLOCK
+#define MAXBRIGHTNESS 100
+#define USE_DHT_TEMP
+#define DHTTYPE DHT11
+#define TEMP_DHT_PIN 19
+#define MULTIPLEX74141_ESP32
+byte digitEnablePins[] = {13, 17, 12, 27, 5, 18}; //sign (előjel), pin 18 - tens-of-degree (fok10) - degree ones (fok1) - tenths-of-degree (tizedfok) - hundredths-of-degree (századfok) - C/percent, pin 13
+byte ABCDPins[4] = {26, 33, 32, 25};
+#define COLON_PIN 14 // decimalPoint inside Nixie tube, set -1, if not used!
+#define ENABLE_CLOCK_DISPLAY false //don't display date/time!!!
+#define SHIFT_TUBES_LEFT_BY_1 //shift left by 1 tube the display, if a thermometer is used with spec tube
+#define USE_SHT21 //I2C Temperature + humidity
+#define PIN_SDA 21 // you can set the used SDA and SCL pins
+#define PIN_SCL 22 // if it is not default value
+#define TEMP_CHARCODE 4
+#define PERCENT_CHARCODE 7
+#define PLUS_CHARCODE 0
+#define MINUS_CHARCODE 1
+#define DATE_REPEAT_MIN_1 //show date only every xxx minute. If zero, datum is never displayed
+#define TEMP_START 40
+#define TEMP_END 45
+#define HUMID_START 45
+#define HUMID_END 50
+#define AP_NAME "Nixie-Ora-Homero"
+#define AP_PASSWORD ""
+#define WEBNAME "Nixie Óra-Hőmérő-Páramérő"
+#endif
+
+#ifdef CLOCK_26 //ESP32 D1 mini, UNFI 6 x IV-11 VFD tubes clock, SHT21
+#define DEBUG
+#define FW "fw26" //firmware name
+#define MAXBRIGHTNESS 100
+#define USE_NEOPIXEL
+#define NEOPIXEL_PIN 22
+byte tubePixels[] = {0, 1, 2, 3, 4, 5}; //6 tubes, single leds
+#define USE_DALLAS_TEMP
+#define TEMP_DALLAS_PIN 25 //Dallas temp sensor pin. If not used, SET TO -1
+//#define USE_DHT_TEMP
+//#define DHTTYPE DHT22
+//#define TEMP_DHT_PIN 25 //DHT temp sensor pin. If not used, SET TO -1
+#define USE_BME280 //I2C Temperature + humidity + pressure
+#define USE_BMP280 //I2C Temperature + barometric pressure
+#define USE_AHTX0 //I2C Temperature + humidity
+#define USE_SHT21 //I2C Temperature + humidity
+#define PIN_SDA 26 // you can set the used SDA and SCL pins
+#define PIN_SCL 27 // if it is not default value
+//#define USE_MQTT
+#define MAX6921_ESP32
+byte segmentEnablePins[] = {19, 17, 15, 12, 13, 16, 18, 14}; //segment enable OUTbits of MAX6921 (a,b,c,d,e,f,g,DP) (You MUST define always 8 Pins!!!)
+byte digitEnablePins[] = {9, 8, 7, 2, 1, 0}; //digit enable OUTbits of MAX6921 (1,2,3,4,5,6) (You may define any number)
+#define DOUBLE_BLINK //both separator points are blinking
+#define DATE_REPEAT_MIN 60 //show date only every xxx minute. If zero, datum is never displayed
+#define DATE_START 07
+#define DATE_END 12
+#define TEMP_START 25
+#define TEMP_END 30
+#define HUMID_START 30
+#define HUMID_END 35
+//MAX6921 pins
+#define PIN_LE 4 // Shift Register Latch Enable
+#define PIN_CLK 17 // Shift Register Clock
+#define PIN_DATA 16 // Shift Register Data
+#define PIN_BL 32 // Shift Register Blank (1=display off 0=display on)
+#define ALARMSPEAKER_PIN 2 //Alarm buzzer pin
+#define ALARMBUTTON_PIN 0 //Alarm switch off button pin
+#define ALARM_ON HIGH //How to switch ON alarm buzzer
+#define AP_NAME "VFD-Ora"
+#define AP_PASSWORD ""
+#define WEBNAME "VFD-Óra"
+#define DEFAULT_SSID "***NET***"
+#define DEFAULT_PSW "Szentatya5"
+//#define USE_WIFIMANAGER
+#define DISABLE_NIGHT_ANIMATION
+//#define DISABLE_BROWNOUT
+#define USE_MQTT
+//#define MQTT_PREFIX "VFD-Clock"
+#define USE_MASTER_CLOCK //enable it, if you want to get any data from MASTER CLOCK. This will be the sensor#0
+//#define USE_MASTER_TEMP //enable it, if you want to get temperature from MASTER CLOCK
+//#define USE_MASTER_HUMID //enable it, if you want to get humidity from MASTER CLOCK
+#define USE_MASTER_RADAR //enable it, if you want to get radar from MASTER CLOCK
+//#define MASTER_TEMPERATURE_TOPIC "homeassistant/sensor/10521c5e14c4/temperature/state"
+//#define MASTER_HUMIDITY_TOPIC "homeassistant/sensor/10521c5e14c4/humidity/state"
+#define MASTER_RADAR_TOPIC "Nixie_IN-18_Clock/sensor/10521c5e14c4/radar/state"
+
+#endif
+
+//______________________ESP-32 CLOCKS (2x18pin ESP32 module) ______________________________________________________
+
+#ifdef CLOCK_30 //ESP32, UNFI PCB clock, 6 x IV-11 VFD tubes
+#define DEBUG
+#define FW "fw30" //firmware name
+#define MAXBRIGHTNESS 100
+#define USE_NEOPIXEL
+#define NEOPIXEL_PIN 2
+byte tubePixels[] = {0, 1, 2, 3, 4, 5}; //6 tubes, single leds
+#define DATE_REPEAT_MIN 3 //show date only every 3 minutes. If zero, datum is never displayed
+#define USE_DALLAS_TEMP
+#define TEMP_DALLAS_PIN 23 //15 //Dallas temp sensor pin. If not used, SET TO -1
+#define MAX6921_ESP32
+byte segmentEnablePins[] = {19, 17, 15, 12, 13, 16, 18, 14}; //segment enable OUTbits of MAX6921 (a,b,c,d,e,f,g,DP) (You MUST define always 8 Pins!!!)
+byte digitEnablePins[] = {9, 8, 7, 2, 1, 0}; //digit enable OUTbits of MAX6921 (1,2,3,4,5,6) (You may define any number)
+#define DOUBLE_BLINK //both separator points are blinking
+//MAX6921 pins
+#define PIN_LE 14 // Shift Register Latch Enable
+#define PIN_CLK 13 // Shift Register Clock
+#define PIN_DATA 27 // Shift Register Data
+#define PIN_BL 12 // Shift Register Blank (1=display off 0=display on)
+//#define PIN_SDA 21 // you can set the used SDA and SCL pins
+//#define PIN_SCL 22 // if it is not default value
+#define ALARMSPEAKER_PIN 33 //Alarm buzzer pin
+#define ALARMBUTTON_PIN -1 //32 //Alarm switch off button pin
+#define ALARM_ON HIGH //How to switch ON alarm buzzer
+#define RADAR_PIN -1
+#define RADAR_TIMEOUT 5 //5min
+//#define TUBE_POWER_PIN 25
+//#define TUBE_POWER_ON LOW
+#define LIGHT_SENSOR_PIN 35 //Only ADC pins are usable! for example: 34,35,36,39...
+#define MAXIMUM_LUX 100 //Lux level for maximum tube brightness
+#define LUX_CALC_SCALAR 12518931 * 1.2
+#define AP_NAME "UNICLOCK32"
+#define AP_PASSWORD ""
+#define WEBNAME "GP IV-11"
+//#define DEFAULT_SSID ""
+//#define DEFAULT_PSW ""
+//#define USE_WIFIMANAGER
+//#define DISABLE_NIGHT_ANIMATION
+#endif
+
+#ifdef CLOCK_31 //ESP32, UNFI board, 6 x Z573M Nixie tubes
+#define DEBUG
+#define FW "fw31" //firmware name
+#define MAXBRIGHTNESS 100
+#define USE_NEOPIXEL
+#define NEOPIXEL_PIN 2
+byte tubePixels[] = {0, 1, 2, 3, 4, 5}; //6 tubes, single leds
+#define USE_DALLAS_TEMP
+#define TEMP_DALLAS_PIN 23 //Dallas temp sensor pin. If not used, SET TO -1
+#define MULTIPLEX74141_ESP32
+byte digitEnablePins[] = {4, 16, 17, 5, 18, 19}; //ESP32 6x tube Clock
+byte ABCDPins[4] = {12, 27, 14, 13};
+#define DP_PIN 15 // decimalPoint inside Nixie tube, set -1, if not used!
+#define LEFTDECIMAL false //set true (Z574M), if decimal point is on the left side on the tube. Else set false (Z573M)!
+//#define TEMP_CHARCODE -1 //disable char => shift display right with 1 digit
+//#define GRAD_CHARCODE -1 //disable char => shift display right with 1 digit
+#define ALARMSPEAKER_PIN 33 //Alarm buzzer pin
+#define ALARMBUTTON_PIN 32 //Alarm switch off button pin
+#define ALARM_ON HIGH
+#define AP_NAME "UNICLOCK32"
+#define AP_PASSWORD ""
+#define WEBNAME "ESP32UniClock 3.0"
+//#define DEFAULT_SSID ""
+//#define DEFAULT_PSW ""
+//#define USE_WIFIMANAGER
+//#define DISABLE_NIGHT_ANIMATION
+#endif
+
+
+#ifdef CLOCK_32 //ESP32, UNFI 6 x IV-11 VFD tubes clock, DHT22 sensor
+#define DEBUG
+#define FW "fw32" //firmware name
+#define MAXBRIGHTNESS 100
+#define USE_NEOPIXEL
+#define NEOPIXEL_PIN 2
+#define LIGHT_SENSOR_PIN 23
+byte tubePixels[] = {0, 1, 2, 3, 4, 5}; //6 tubes, single leds
+#define USE_DHT_TEMP
+#define DHTTYPE DHT22
+#define TEMP_DHT_PIN 23 //DHT temp sensor pin. If not used, SET TO -1
+#define MAX6921_ESP32
+byte segmentEnablePins[] = {19, 17, 15, 12, 13, 16, 18, 14}; //segment enable OUTbits of MAX6921 (a,b,c,d,e,f,g,DP) (You MUST define always 8 Pins!!!)
+byte digitEnablePins[] = {9, 8, 7, 2, 1, 0}; //digit enable OUTbits of MAX6921 (1,2,3,4,5,6) (You may define any number)
+//MAX6921 pins
+#define PIN_LE 14 // Shift Register Latch Enable
+#define PIN_CLK 13 // Shift Register Clock
+#define PIN_DATA 27 // Shift Register Data
+#define PIN_BL 12 // Shift Register Blank (1=display off 0=display on)
+//#define ALARMSPEAKER_PIN 33 //Alarm buzzer pin
+#define ALARMBUTTON_PIN 32 //Alarm switch off button pin
+#define ALARM_ON HIGH //How to switch ON alarm buzzer
+#define AP_NAME "UNICLOCK32"
+#define AP_PASSWORD ""
+#define WEBNAME "ESP32UniClock 3.0"
+//#define DEFAULT_SSID ""
+//#define DEFAULT_PSW ""
+//#define USE_WIFIMANAGER
+//#define DISABLE_NIGHT_ANIMATION
+#endif
+
+#ifdef CLOCK_33 //TOM025 ESP32, Palfi S. board, 6 x Z573M Nixie tubes
+#define DEBUG
+#define FW "fw33"
+#define MAXBRIGHTNESS 100
+//#define USE_NEOPIXEL
+//#define NEOPIXEL_PIN 2
+byte tubePixels[] = {0, 1, 2, 3, 4, 5}; //6 tubes, single leds
+//#define USE_DALLAS_TEMP
+#define TEMP_DALLAS_PIN 23 //Dallas temp sensor pin. If not used, SET TO -1
+#define MULTIPLEX74141_ESP32
+byte digitEnablePins[] = {22, 23, 21, 19, 12, 14}; //ESP32 6x tube Clock
+byte ABCDPins[4] = {18, 5, 16, 17};
+#define DP_PIN 15 // decimalPoint inside Nixie tube, set -1, if not used!
+#define LEFTDECIMAL false //set true (Z574M), if decimal point is on the left side on the tube. Else set false (Z573M)!
+//#define TEMP_CHARCODE -1 //disable char => shift display right with 1 digit
+//#define GRAD_CHARCODE -1 //disable char => shift display right with 1 digit
+//#define ALARMSPEAKER_PIN 33 //Alarm buzzer pin
+//#define ALARMBUTTON_PIN 32 //Alarm switch off button pin
+//#define ALARM_ON HIGH
+#define AP_NAME "UNICLOCK32"
+#define AP_PASSWORD ""
+#define WEBNAME "ESP32UniClock 3.0"
+//#define DEFAULT_SSID ""
+//#define DEFAULT_PSW ""
+//#define USE_WIFIMANAGER
+//#define DISABLE_NIGHT_ANIMATION
+#endif
+
+#ifdef CLOCK_34 //Mule V2 ESP32, board, 6 x SA40 LED Display
+#define DEBUG
+#define FW "fw34"
+#define MAXBRIGHTNESS 100
+#define USE_NEOPIXEL
+#define NEOPIXEL_PIN 26
+byte tubePixels[] = {0, 1, 2, 3, 4, 5}; //6 tubes, single leds
+//#define USE_DALLAS_TEMP
+#define TEMP_DALLAS_PIN 25 //Dallas temp sensor pin. If not used, SET TO -1
+#define MULE_V2
+byte digitEnablePins[] = {5, 16, 23}; //CD4028 A,B,C
+byte ABCDPins[4] = {33, 32, 14, 13}; //CD4543 A,B,C,D
+//#define DP_PIN 15 // decimalPoint inside Nixie tube, set -1, if not used!
+//#define LEFTDECIMAL false //set true (Z574M), if decimal point is on the left side on the tube. Else set false (Z573M)!
+//#define TEMP_CHARCODE -1 //disable char => shift display right with 1 digit
+//#define GRAD_CHARCODE -1 //disable char => shift display right with 1 digit
+//#define ALARMSPEAKER_PIN 22 //Alarm buzzer pin
+//#define ALARMBUTTON_PIN 32 //Alarm switch off button pin
+//#define ALARM_ON HIGH
+#define AP_NAME "UNICLOCK32"
+#define AP_PASSWORD ""
+#define WEBNAME "ESP32UniClock 3.0"
+//#define DEFAULT_SSID ""
+//#define DEFAULT_PSW ""
+//#define USE_WIFIMANAGER
+//#define DISABLE_NIGHT_ANIMATION
+#endif
+
+#ifdef CLOCK_35 //ESP32, 6X Z566M and 3x 74595 6x74141 driver (NON-MULTIPLEX)
+#define DEBUG
+#define SIMULATE_TEMPERATURE_SENSOR
+#define FW "fw35" //firmware name
+#define MAXBRIGHTNESS 100
+#define USE_DALLAS_TEMP
+#define TEMP_DALLAS_PIN 32
+#define USE_NEOPIXEL
+#define NEOPIXEL_PIN 25
+//#define USE_DHT_TEMP
+#define TEMP_DHT_PIN 32
+byte tubes[] = {5, 4, 3, 2, 1, 0}; //6 tubes, reverse order
+byte tubePixels[] = {5, 4, 3, 2, 1, 0};
+#define dataPin 14 //D5
+#define latchPin 27 //D6
+#define clkPin 26 //D7
+#define NO_MULTIPLEX_ESP32
+#define COLON_PIN 13 //Blinking Colon pin. If not used, SET TO -1
+#define ALARMSPEAKER_PIN 12 //Alarm buzzer pin
+//#define ALARMBUTTON_PIN 36 //Alarm switch off button pin
+#define ALARM_ON HIGH
+#define LIGHT_SENSOR_PIN 35
+#define MAXIMUM_LUX 100
+#define LUX_CALC_SCALAR 12518931 * 1.2 //Calibrate here
+//#define RADAR_PIN 15
+//#define RADAR_TIMEOUT 5 //min
+#define USE_RTC
+#define PIN_SDA 21
+#define PIN_SCL 22
+#define AP_NAME "UNFICLOCK32"
+#define AP_PASSWORD ""
+#define WEBNAME "Z566M Nixie Clock"
+//#define DEFAULT_SSID ""
+//#define DEFAULT_PSW ""
+//#define USE_WIFIMANAGER
+//#define DISABLE_NIGHT_ANIMATION
+#define DISPLAYTEMP_ONLY_2DIGITS
+#define DISPLAYHUMID_ONLY_2DIGITS
+#define GRAD_CHARCODE 10 //no display grad char
+#define TEMP_CHARCODE 10 //no display temp char
+#define PERCENT_CHARCODE 10 //no % tube
+#define TEMP_START_POSITION 3
+#define HUMID_START_POSITION 3
+#endif
+
+#ifdef CLOCK_36 //ESP32, 4x IN-1 tubes and 4x74141 driver (NON-MULTIPLEX)
+#define DEBUG
+#define FW "fw35" //firmware name
+#define MAXBRIGHTNESS 100
+//#define USE_DALLAS_TEMP
+#define TEMP_DALLAS_PIN -1
+#define USE_NEOPIXEL
+#define dataPin 14 //D5
+#define latchPin 27 //D6
+#define clkPin 26 //D7
+byte tubes[] = {3, 2, 1, 0}; //4 tubes, old clock...
+byte tubePixels[] = {0, 0, 1, 1, 2, 2, 3, 3, 3, 3, 2, 2, 1, 1, 0, 0, 0}; //4 tubes, double row, 17 leds (GP)
+#define NO_MULTIPLEX_ESP32
+#define COLON_PIN 2 //Blinking Colon pin. If not used, SET TO -1 ( old IN-1-Clock: white:2 brown: SDA)
+#define AP_NAME "UNICLOCK32"
+#define AP_PASSWORD ""
+#define WEBNAME "IN-1 Nixie Clock"
+//#define DEFAULT_SSID ""
+//#define DEFAULT_PSW ""
+//#define USE_WIFIMANAGER
+//#define DISABLE_NIGHT_ANIMATION
+#endif
+
+//______________________ESP-32 CLOCKS (2x20pin ESP32 D1 mini module) ______________________________________________________
+#ifdef CLOCK_40 //V1 ESP32, UNFI 6 x IV-11 VFD tubes clock
+#define DEBUG
+#define FW "fw40" //firmware name
+#define MAXBRIGHTNESS 100
+#define USE_NEOPIXEL
+#define NEOPIXEL_PIN 22
+byte tubePixels[] = {0, 1, 2, 3, 4, 5}; //6 tubes, single leds
+#define USE_DALLAS_TEMP
+#define TEMP_DALLAS_PIN 25 //Dallas temp sensor pin. If not used, SET TO -1
+//#define USE_DHT_TEMP
+//#define DHTTYPE DHT22
+//#define TEMP_DHT_PIN 25 //DHT temp sensor pin. If not used, SET TO -1
+#define MAX6921_ESP32
+byte segmentEnablePins[] = {19, 17, 15, 12, 13, 16, 18, 14}; //segment enable OUTbits of MAX6921 (a,b,c,d,e,f,g,DP) (You MUST define always 8 Pins!!!)
+byte digitEnablePins[] = {9, 8, 7, 2, 1, 0}; //digit enable OUTbits of MAX6921 (1,2,3,4,5,6) (You may define any number)
+#define DOUBLE_BLINK //both separator points are blinking
+//MAX6921 pins
+#define PIN_LE 4 // Shift Register Latch Enable
+#define PIN_CLK 17 // Shift Register Clock
+#define PIN_DATA 16 // Shift Register Data
+#define PIN_BL 32 // Shift Register Blank (1=display off 0=display on)
+#define ALARMSPEAKER_PIN 2 //Alarm buzzer pin
+#define ALARMBUTTON_PIN 0 //Alarm switch off button pin
+#define TUBE_POWER_PIN 23
+#define TUBE_POWER_ON HIGH
+#define ALARM_ON HIGH //How to switch ON alarm buzzer
+#define AP_NAME "UNFICLOCK32"
+#define AP_PASSWORD ""
+#define WEBNAME "ESP32 IV-11 VFD-Clock"
+//#define DEFAULT_SSID ""
+//#define DEFAULT_PSW ""
+//#define USE_WIFIMANAGER
+//#define DISABLE_NIGHT_ANIMATION
+#endif
+
+#ifdef CLOCK_41 //V2 ESP32, UNFI 6 x IV-11 VFD tubes clock (jumper wire)
+#define DEBUG
+#define FW "fw41" //firmware name
+#define MAXBRIGHTNESS 100
+#define USE_NEOPIXEL
+#define NEOPIXEL_PIN 32
+byte tubePixels[] = {0, 1, 2, 3, 4, 5}; //6 tubes, single leds
+#define USE_DALLAS_TEMP
+#define TEMP_DALLAS_PIN 4 //Dallas temp sensor pin. If not used, SET TO -1
+//#define USE_DHT_TEMP
+//#define DHTTYPE DHT22
+//#define TEMP_DHT_PIN 4 //DHT temp sensor pin. If not used, SET TO -1
+#define MAX6921_ESP32
+byte segmentEnablePins[] = {19, 17, 15, 12, 13, 16, 18, 14}; //segment enable OUTbits of MAX6921 (a,b,c,d,e,f,g,DP) (You MUST define always 8 Pins!!!)
+byte digitEnablePins[] = {9, 8, 7, 2, 1, 0}; //digit enable OUTbits of MAX6921 (1,2,3,4,5,6) (You may define any number)
+#define DOUBLE_BLINK //both separator points are blinking
+//MAX6921 pins
+#define PIN_LE 22 // Shift Register Latch Enable
+#define PIN_CLK 17 // Shift Register Clock
+#define PIN_DATA 16 // Shift Register Data
+#define PIN_BL 21 // Shift Register Blank (1=display off 0=display on)
+#define ALARMSPEAKER_PIN 2 //Alarm buzzer pin
+#define ALARMBUTTON_PIN 0 //Alarm switch off button pin
+#define TUBE_POWER_PIN 23
+#define TUBE_POWER_ON HIGH
+#define ALARM_ON HIGH //How to switch ON alarm buzzer
+#define AP_NAME "UNFICLOCK32"
+#define AP_PASSWORD ""
+#define WEBNAME "ESP32 IV-11 VFD-Clock"
+//#define DEFAULT_SSID ""
+//#define DEFAULT_PSW ""
+//#define USE_WIFIMANAGER
+//#define DISABLE_NIGHT_ANIMATION
+#endif
+
+#ifdef CLOCK_42 //V3 ESP32, UNFI 6 x IV-11 VFD tubes clock
+#define DEBUG
+#define FW "fw42" //firmware name
+#define MAXBRIGHTNESS 100
+#define USE_NEOPIXEL
+#define NEOPIXEL_PIN 25
+byte tubePixels[] = {0, 1, 2, 3, 4, 5}; //6 tubes, single leds
+#define USE_DALLAS_TEMP
+#define TEMP_DALLAS_PIN 22 //Dallas temp sensor pin. If not used, SET TO -1
+//#define USE_DHT_TEMP
+//#define DHTTYPE DHT22
+//#define TEMP_DHT_PIN 22 //DHT temp sensor pin. If not used, SET TO -1
+#define MAX6921_ESP32
+byte segmentEnablePins[] = {19, 17, 15, 12, 13, 16, 18, 14}; //segment enable OUTbits of MAX6921 (a,b,c,d,e,f,g,DP) (You MUST define always 8 Pins!!!)
+byte digitEnablePins[] = {9, 8, 7, 2, 1, 0}; //digit enable OUTbits of MAX6921 (1,2,3,4,5,6) (You may define any number)
+#define DOUBLE_BLINK //both separator points are blinking
+//MAX6921 pins
+#define PIN_LE 4 // Shift Register Latch Enable
+#define PIN_CLK 17 // Shift Register Clock
+#define PIN_DATA 16 // Shift Register Data
+#define PIN_BL 32 // Shift Register Blank (1=display off 0=display on)
+#define ALARMSPEAKER_PIN 2 //Alarm buzzer pin
+#define ALARMBUTTON_PIN 0 //Alarm switch off button pin
+#define TUBE_POWER_PIN 27
+#define TUBE_POWER_ON HIGH
+#define ALARM_ON HIGH //How to switch ON alarm buzzer
+#define AP_NAME "UNFICLOCK32"
+#define AP_PASSWORD ""
+#define WEBNAME "ESP32 IV-11 VFD-Clock"
+//#define DEFAULT_SSID ""
+//#define DEFAULT_PSW ""
+//#define USE_WIFIMANAGER
+//#define DISABLE_NIGHT_ANIMATION
+#endif
+
+
+#ifdef CLOCK_43 //V1 ESP32, UNFI board, 6 x Z573M Nixie tubes
+#define DEBUG
+//#define SIMULATE_TEMPERATURE_SENSOR
+#define FW "fw43" //firmware name
+#define MAXBRIGHTNESS 100
+#define USE_NEOPIXEL
+#define NEOPIXEL_PIN 22
+byte tubePixels[] = {0, 1, 2, 3, 4, 5}; //6 tubes, single leds
+#define USE_DALLAS_TEMP
+//#define USE_DHT_TEMP
+//#define DHTTYPE DHT22
+//#define TEMP_DHT_PIN 25
+#define TEMP_DALLAS_PIN 25 //Dallas temp sensor pin. If not used, SET TO -1
+#define MULTIPLEX74141_ESP32
+byte DRAM_ATTR digitEnablePins[] = {26, 18, 33, 19, 23, 5}; //ESP32 6x tube Clock
+byte DRAM_ATTR ABCDPins[4] = {32, 16, 4, 17};
+#define DP_PIN 27 // decimalPoint inside Nixie tube, set -1, if not used!
+#define LEFTDECIMAL false //set true (Z574M), if decimal point is on the left side on the tube. Else set false (Z573M)!
+//#define TEMP_CHARCODE -1 //disable char => shift display right with 1 digit
+//#define GRAD_CHARCODE -1 //disable char => shift display right with 1 digit
+#define ALARMSPEAKER_PIN 2 //Alarm buzzer pin
+#define ALARMBUTTON_PIN 0 //Alarm switch off button pin
+#define ALARM_ON HIGH
+#define AP_NAME "UNFICLOCK32"
+#define AP_PASSWORD ""
+#define WEBNAME "Z573M Nixie-Clock"
+//#define DEFAULT_SSID ""
+//#define DEFAULT_PSW ""
+//#define USE_WIFIMANAGER
+//#define DISABLE_NIGHT_ANIMATION
+#endif
+
+#ifdef CLOCK_44 //V2 ESP32, UNFI board, 6 x Z573M Nixie tubes (rewired revision, átkötéses)
+#define DEBUG
+#define FW "fw44" //firmware name
+#define MAXBRIGHTNESS 100
+#define USE_NEOPIXEL
+#define NEOPIXEL_PIN 32
+byte tubePixels[] = {0, 1, 2, 3, 4, 5}; //6 tubes, single leds
+#define USE_DALLAS_TEMP
+//#define USE_DHT_TEMP
+//#define DHTTYPE DHT22
+//#define TEMP_DHT_PIN 4
+#define TEMP_DALLAS_PIN 4 //Dallas temp sensor pin. If not used, SET TO -1
+#define MULTIPLEX74141_ESP32
+byte digitEnablePins[] = {26, 18, 33, 19, 5, 25}; //ESP32 6x tube Clock
+byte ABCDPins[4] = {21, 16, 22, 17};
+#define DP_PIN 27 // decimalPoint inside Nixie tube, set -1, if not used!
+#define LEFTDECIMAL false //set true (Z574M), if decimal point is on the left side on the tube. Else set false (Z573M)!
+//#define TEMP_CHARCODE -1 //disable char => shift display right with 1 digit
+//#define GRAD_CHARCODE -1 //disable char => shift display right with 1 digit
+#define ALARMSPEAKER_PIN 2 //Alarm buzzer pin
+#define ALARMBUTTON_PIN 0 //Alarm switch off button pin
+#define ALARM_ON HIGH
+#define AP_NAME "UNFICLOCK32"
+#define AP_PASSWORD ""
+#define WEBNAME "ESP32 Z573M Nixie-Clock"
+//#define DEFAULT_SSID ""
+//#define DEFAULT_PSW ""
+//#define USE_WIFIMANAGER
+//#define DISABLE_NIGHT_ANIMATION
+#endif
+
+#ifdef CLOCK_45 //V3 ESP32, UNFI board, 6 x Z573M Nixie tubes
+#define DEBUG
+#define FW "fw45" //firmware name
+#define MAXBRIGHTNESS 100
+#define USE_NEOPIXEL
+#define NEOPIXEL_PIN 25
+byte tubePixels[] = {0, 1, 2, 3, 4, 5}; //6 tubes, single leds
+#define USE_DALLAS_TEMP
+//#define USE_DHT_TEMP
+//#define DHTTYPE DHT22
+//#define TEMP_DHT_PIN 22
+#define TEMP_DALLAS_PIN 22 //Dallas temp sensor pin. If not used, SET TO -1
+#define MULTIPLEX74141_ESP32
+byte digitEnablePins[] = {26, 18, 33, 19, 23, 5}; //ESP32 6x tube Clock
+byte ABCDPins[4] = {32, 16, 4, 17};
+#define DP_PIN 27 // decimalPoint inside Nixie tube, set -1, if not used!
+#define LEFTDECIMAL false //set true (Z574M), if decimal point is on the left side on the tube. Else set false (Z573M)!
+//#define TEMP_CHARCODE -1 //disable char => shift display right with 1 digit
+//#define GRAD_CHARCODE -1 //disable char => shift display right with 1 digit
+#define ALARMSPEAKER_PIN 2 //Alarm buzzer pin
+#define ALARMBUTTON_PIN 0 //Alarm switch off button pin
+#define ALARM_ON HIGH
+#define AP_NAME "UNFICLOCK32"
+#define AP_PASSWORD ""
+#define WEBNAME "ESP32 Z573M Nixie-Clock"
+//#define DEFAULT_SSID ""
+//#define DEFAULT_PSW ""
+//#define USE_WIFIMANAGER
+//#define DISABLE_NIGHT_ANIMATION
+#endif
+
+#ifdef CLOCK_46 //ESP32, UNFI board, 6 x IV-9 Numitron tubes
+#define DEBUG
+#define ANIMSPEED 70
+#define FW "fw46" //firmware name
+#define MAXBRIGHTNESS 100
+#define USE_NEOPIXEL
+#define NEOPIXEL_PIN 22
+byte tubePixels[] = {0, 1, 2, 3, 4, 5}; //6 tubes, single leds
+#define USE_DALLAS_TEMP
+//#define USE_DHT_TEMP
+//#define DHTTYPE DHT22
+//#define TEMP_DHT_PIN 27
+#define TEMP_DALLAS_PIN 27 //Dallas temp sensor pin. If not used, SET TO -1
+//#define LIGHT_SENSOR_PIN 35
+#define PIN_SDA 5 // you can set the used SDA and SCL pins
+#define PIN_SCL 0 // if it is not default value
+#define USE_BH1750 //I2C luxmeter sensor
+#define USE_BME280 //I2C Temperature + humidity + pressure
+#define USE_BMP280 //I2C Temperature + barometric pressure
+#define USE_AHTX0 //I2C Temperature + humidity
+#define USE_SHT21 //I2C Temperature + humidity
+#define Numitron_4511N
+#define PCB_VERSION 32
+#define LTBIpin 23
+byte digitEnablePins[] = {4, 16, 17, 32, 21, 25}; //ESP32 6x tube Clock
+byte ABCDPins[4] = {19, 18, 26, 33};
+#define DP_PIN -1 // decimalPoint inside Nixie tube, set -1, if not used!
+#define LEFTDECIMAL false //set true (Z574M), if decimal point is on the left side on the tube. Else set false (Z573M)!
+//#define TEMP_CHARCODE -1 //disable char => shift display right with 1 digit
+//#define GRAD_CHARCODE -1 //disable char => shift display right with 1 digit
+#define ALARMSPEAKER_PIN 2 //Alarm buzzer pin
+//#define ALARMBUTTON_PIN 34 //Alarm switch off button pin
+#define ALARM_ON HIGH
+#define AP_NAME "NUMITRON6_CLOCK"
+#define AP_PASSWORD ""
+#define WEBNAME "ESP32 Numitron-Clock"
+//#define DEFAULT_SSID ""
+//#define DEFAULT_PSW ""
+//#define USE_WIFIMANAGER
+//#define DISABLE_NIGHT_ANIMATION
+#endif
+
+#ifdef CLOCK_47 //V3 ESP32, UNFI 10-LT-50G VFD tubes clock
+#define DEBUG
+#define FW "fw47" //firmware name
+#define MAXBRIGHTNESS 100
+#define USE_DALLAS_TEMP
+#define TEMP_DALLAS_PIN 22 //Dallas temp sensor pin. If not used, SET TO -1
+//#define USE_DHT_TEMP
+//#define DHTTYPE DHT22
+//#define TEMP_DHT_PIN 22 //DHT temp sensor pin. If not used, SET TO -1
+#define MAX6921_ESP32
+byte segmentEnablePins[] = {12, 13, 16, 18, 17, 14, 15, 19}; //segment enable OUTbits of MAX6921 (a,b,c,d,e,f,g,DP) (You MUST define always 8 Pins!!!)
+byte digitEnablePins[] = {10, 9, 8, 7, 2, 1, 0, 11}; //digit enable OUTbits of MAX6921 (1,2,3,4,5,6,7,8) (You may define any number)
+#define DOUBLE_BLINK //both separator points are blinking
+//MAX6921 pins
+#define PIN_LE 4 // Shift Register Latch Enable
+#define PIN_CLK 17 // Shift Register Clock
+#define PIN_DATA 16 // Shift Register Data
+#define PIN_BL 32 // Shift Register Blank (1=display off 0=display on)
+#define ALARMSPEAKER_PIN 2 //Alarm buzzer pin
+#define ALARMBUTTON_PIN 0 //Alarm switch off button pin
+#define TUBE_POWER_PIN 27
+#define TUBE_POWER_ON HIGH
+#define ALARM_ON HIGH //How to switch ON alarm buzzer
+#define AP_NAME "UNFICLOCK32"
+#define AP_PASSWORD ""
+#define WEBNAME "ESP32 10-LT-50G VFD-Clock"
+//#define DISABLE_NIGHT_ANIMATION
+#endif
+
+#ifdef CLOCK_48 //WROOM ESP32, UNFI SN75518 6xIV-17 clock
+#define DEBUG
+#define FW "fw48" //firmware name
+#define MAXBRIGHTNESS 100
+#define USE_NEOPIXEL
+#define NEOPIXEL_PIN 26
+byte tubePixels[] = {0, 1, 2, 3, 4, 5}; //6 tubes, single leds
+#define USE_DALLAS_TEMP
+#define TEMP_DALLAS_PIN 23 //Dallas temp sensor pin. If not used, SET TO -1
+//#define USE_DHT_TEMP
+//#define DHTTYPE DHT22
+//#define TEMP_DHT_PIN 23 //DHT temp sensor pin. If not used, SET TO -1
+//#define LIGHT_SENSOR_PIN 32
+#define LOWER_CIRCLE_CHARCODE 10
+#define UPPER_CIRCLE_CHARCODE 17
+#define PIN_SDA 21 // you can set the used SDA and SCL pins
+#define PIN_SCL 22 // if it is not default value
+#define USE_BH1750 //I2C luxmeter sensor
+#define USE_BME280 //I2C Temperature + humidity + pressure
+#define USE_BMP280 //I2C Temperature + barometric pressure
+#define USE_AHTX0 //I2C Temperature + humidity
+#define USE_SHT21 //I2C Temperature + humidity
+#define SN75518_ESP32
+//Segments: Dutsrpnmkhgfedcba
+byte segmentEnablePins[] = {23, 30, 18, 19, 20, 26, 27, 28, 29, 32, 15, 17, 21, 22, 24, 25, 31}; //segment enable OUTbits of SN75518 (2,3,4,5,6,7,8,9,10,13,14,15,16,17,18,19,20,21) (You MUST define always 8 Pins!!!)
+byte digitEnablePins[] = {6, 5, 4, 3, 2, 1}; //digit enable OUTbits of SN75518 (1,2,3,4,5,6) (You may define any number)
+#define DOUBLE_BLINK //both separator points are blinking
+//SN75518 pins
+#define PIN_LE 13 // Shift Register Latch Enable
+#define PIN_CLK 12 // Shift Register Clock
+#define PIN_DATA 27 // Shift Register Data
+#define PIN_STB 14 // Shift Register Strobe
+#define ALARMSPEAKER_PIN 19 //Alarm buzzer pin
+#define ALARMBUTTON_PIN 33 //Alarm switch off button pin
+//#define TUBE_POWER_PIN
+#define TUBE_POWER_ON HIGH
+#define ALARM_ON HIGH //How to switch ON alarm buzzer
+#define AP_NAME "UNFICLOCK32"
+#define AP_PASSWORD ""
+#define WEBNAME "ESP32 IV-17 VFD-Clock"
+//#define DEFAULT_SSID ""
+//#define DEFAULT_PSW ""
+//#define USE_WIFIMANAGER
+//#define DISABLE_BROWNOUT
+//#define DISABLE_NIGHT_ANIMATION
+#endif
+
+#ifdef CLOCK_49 //WROOM ESP32, UNFI SN75518 6xIV-11 clock
+#define DEBUG
+#define FW "fw49" //firmware name
+#define MAXBRIGHTNESS 100
+#define USE_NEOPIXEL
+#define NEOPIXEL_PIN 26
+byte tubePixels[] = {0, 1, 2, 3, 4, 5}; //6 tubes, single leds
+#define USE_DALLAS_TEMP
+#define TEMP_DALLAS_PIN 23 //Dallas temp sensor pin. If not used, SET TO -1
+//#define USE_DHT_TEMP
+//#define DHTTYPE DHT22
+//#define TEMP_DHT_PIN 23 //DHT temp sensor pin. If not used, SET TO -1
+//#define LIGHT_SENSOR_PIN 32
+#define PIN_SDA 21 // you can set the used SDA and SCL pins
+#define PIN_SCL 22 // if it is not default value
+#define SN75518_ESP32
+//Segments: abcdefgD
+#define SEGMENT8
+byte segmentEnablePins[] = {17, 18, 19, 20, 21, 22, 23, 24}; //IV-11 segment enable OUTbits of SN75518 (You MUST define 8 Pins!!!)
+byte digitEnablePins[] = {27, 28, 29, 30, 31, 32}; //IV-11 digit enable OUTbits of SN75518 (1,2,3,4,5,6) (You may define any number)
+//byte segmentEnablePins[] = {17,18,19,20,21,22,23,24}; //IV-12 segment enable OUTbits of SN75518 (You MUST define 8 Pins!!!)
+//byte digitEnablePins[] = {27,28,29,30,31,32}; //IV-12 digit enable OUTbits of SN75518 (1,2,3,4,5,6) (You may define any number)
+#define DOUBLE_BLINK //both separator points are blinking
+//SN75518 pins
+#define PIN_LE 13 // Shift Register Latch Enable
+#define PIN_CLK 12 // Shift Register Clock
+#define PIN_DATA 27 // Shift Register Data
+#define PIN_STB 14 // Shift Register Strobe
+#define ALARMSPEAKER_PIN 19 //Alarm buzzer pin
+#define ALARMBUTTON_PIN 33 //Alarm switch off button pin
+//#define TUBE_POWER_PIN
+#define TUBE_POWER_ON HIGH
+#define ALARM_ON HIGH //How to switch ON alarm buzzer
+#define AP_NAME "UNFICLOCK32"
+#define AP_PASSWORD ""
+#define WEBNAME "ESP32 IV-11 VFD-Clock"
+//#define DEFAULT_SSID ""
+//#define DEFAULT_PSW ""
+//#define USE_WIFIMANAGER
+//#define DISABLE_BROWNOUT
+//#define DISABLE_NIGHT_ANIMATION
+#endif
+
+//---------------------ESP32 2X HV5122--------------------------------------------------
+
+#ifdef CLOCK_50 //V1 ESP32,UNFI 2xHV5122 PCB version, 6xZ573M clock Example clock!!!
+#define DEBUG
+#define FW "fw50" //firmware name
+#define MAXBRIGHTNESS 100
+#define USE_NEOPIXEL
+#define NEOPIXEL_PIN 27
+byte tubePixels[] = {0, 1, 2, 3, 4, 5}; //6 tubes, single leds
+//#define USE_DALLAS_TEMP
+//#define TEMP_DALLAS_PIN 26 //Dallas temp sensor pin. If not used, SET TO -1
+//#define LIGHT_SENSOR_PIN 23
+#define PIN_SDA 4 // you can set the used SDA and SCL pins
+#define PIN_SCL 32 // if it is not default value
+#define USE_DHT_TEMP
+#define DHTTYPE DHT22
+#define TEMP_DHT_PIN 26
+//#define USE_RTC
+//#define USE_GPS
+#define USE_BME280 //I2C Temperature + humidity + pressure
+#define USE_BMP280 //I2C Temperature + barometric pressure
+#define USE_AHTX0 //I2C Temperature + humidity
+#define USE_SHT21 //I2C Temperature + humidity
+#define USE_BH1750 //I2C luxmeter sensor
+//_______________________________ HV5122 setup ____________________________________________________
+#define NEWHV5122
+#define PIN_DIN 22 // DataIn - chip0 DOUT pin is connected to chip1 DIN pin!
+#define PIN_CLK 17 // Clock
+#define PIN_OE 21 // OutputEnable
+#define MAXDIGITS 6
+int maxDigits = MAXDIGITS;
+byte digitPins[MAXDIGITS + 1][10] = { ////Data pin numbers 100+ means: chip1 pins are used. Chip0's DOUT is connected to chip1's DIN
+ {2, 10, 9, 8, 7, 6, 5, 4, 3, 1}, //sec 1 , chip0 (tube#0)
+ {11, 32, 20, 19, 18, 17, 16, 15, 14, 13}, //sec 10 , chip0 (tube#1)
+ {22, 31, 29, 30, 27, 28, 25, 26, 23, 24}, //min 1 , chip0 (tube#2)
+ {101, 131, 110, 109, 108, 107, 106, 105, 104, 103}, //min 10 , chip1 (tube#3)
+ {111, 132, 120, 119, 118, 117, 116, 115, 114, 113}, //hour 1 , chip1 (tube#4)
+ {122, 129, 130, 127, 128, 125, 126, 123, 124, 121}, //hour 10 , chip1 (tube#5)
+ {0, 12, 21, 102, 112, 0, 0, 0, 0, 0} //extra decimalPoint/blinking dots (tube0...tube6)
+};
+//#define MAKE_BLINKING_DOTS //it means, the extra datapins are used as 4 blinking dot instead of decimal points! #1..#4 positions are used
+//___________________________________________________________________________________
+#define ALARMSPEAKER_PIN 16 //Alarm buzzer pin
+//#define ALARMBUTTON_PIN 23 //Alarm switch off button pin
+#define ALARM_ON HIGH //How to switch ON alarm buzzer
+//#define RADAR_PIN 4
+#define RADAR_TIMEOUT 5 //min
+//#define TUBE_POWER_PIN 4
+#define TUBE_POWER_ON HIGH
+#define ENABLE_CLOCK_DISPLAY true
+#define DATE_REPEAT_MIN 3 //show date only every xxx minute. If zero, datum is never displayed
+//#define SHIFT_TUBES_LEFT_BY_1 //shift left by 1 tube the display, if a thermometer is used with spec tube
+#define TEMP_CHARCODE 10
+#define GRAD_CHARCODE 10
+#define PERCENT_CHARCODE 10
+#define AP_NAME "UNFICLOCK32"
+#define AP_PASSWORD ""
+#define WEBNAME "UNFI Nixie-Clock HV5122"
+//#define DEFAULT_SSID ""
+//#define DEFAULT_PSW ""
+//#define USE_WIFIMANAGER
+//#define DISABLE_NIGHT_ANIMATION
+#endif
+
+#ifdef CLOCK_51 //V2 ESP32, UNFI 2xHV5122 PCB version, 6xZ573M clock Example clock!!!
+#define DEBUG
+#define FW "fw51" //firmware name
+#define MAXBRIGHTNESS 100
+#define USE_NEOPIXEL
+#define NEOPIXEL_PIN 27
+byte tubePixels[] = {0, 1, 2, 3, 4, 5}; //6 tubes, single leds
+#define USE_DALLAS_TEMP
+#define TEMP_DALLAS_PIN 19 //Dallas temp sensor pin. If not used, SET TO -1
+//#define LIGHT_SENSOR_PIN 23
+#define PIN_SDA 4 // you can set the used SDA and SCL pins
+#define PIN_SCL 32 // if it is not default value
+//#define USE_DHT_TEMP
+//#define DHTTYPE DHT11
+//#define TEMP_DHT_PIN 23
+//#define USE_BME280 //I2C Temperature + humidity + pressure
+//#define USE_BMP280 //I2C Temperature + barometric pressure
+//#define USE_AHTX0 //I2C Temperature + humidity
+//#define USE_SHT21 //I2C Temperature + humidity
+//#define USE_BH1750 //I2C luxmeter sensor
+//USE_MQTT
+//#define MQTT_PREFIX "UniClock32"
+//#define USE_MASTER_CLOCK //enable it, if you want to get any data from MASTER CLOCK. This will be the sensor#0
+//#define USE_MASTER_TEMP //enable it, if you want to get temperature from MASTER CLOCK
+//#define USE_MASTER_HUMID //enable it, if you want to get humidity from MASTER CLOCK
+//#define USE_MASTER_RADAR //enable it, if you want to get radar from MASTER CLOCK
+//#define MASTER_TEMPERATURE_TOPIC "homeassistant/sensor/10521c5e14c4/temperature/state"
+//#define MASTER_HUMIDITY_TOPIC "homeassistant/sensor/10521c5e14c4/humidity/state"
+//#define MASTER_RADAR_TOPIC "homeassistant/sensor/10521c5e14c4/radar/state"
+//_______________________________ HV5122 setup ____________________________________________________
+#define NEWHV5122
+#define PIN_DIN 22 // DataIn - chip0 DOUT pin is connected to chip1 DIN pin!
+#define PIN_CLK 17 // Clock
+#define PIN_OE 21 // OutputEnable
+#define MAXDIGITS 6
+int maxDigits = MAXDIGITS;
+byte digitPins[MAXDIGITS + 1][10] = {
+ {2, 10, 9, 8, 7, 6, 5, 4, 3, 1}, //sec 1 , chip0 (tube#0)
+ {11, 32, 20, 19, 18, 17, 16, 15, 14, 13}, //sec 10 , chip0 (tube#1)
+ {22, 31, 29, 30, 27, 28, 25, 26, 23, 24}, //min 1 , chip0 (tube#2)
+ {101, 131, 110, 109, 108, 107, 106, 105, 104, 103}, //min 10 , chip1 (tube#3)
+ {111, 132, 120, 119, 118, 117, 116, 115, 114, 113}, //hour 1 , chip1 (tube#4)
+ {122, 129, 130, 127, 128, 125, 126, 123, 124, 121}, //hour 10 , chip1 (tube#5)
+ {0, 21, 12, 112, 102, 0, 0, 0, 0, 0} //extra decimalPoint/blinking dots (tube0...tube6)
+};
+//#define MAKE_BLINKING_DOTS //it means, the extra datapins are used as 4 blinking dot instead of decimal points! #1..#4 positions are used
+//___________________________________________________________________________________
+#define ALARMSPEAKER_PIN 23 //Alarm buzzer pin
+#define ALARMBUTTON_PIN 16 //Alarm switch off button pin
+#define ALARM_ON HIGH //How to switch ON alarm buzzer
+//#define RADAR_PIN 4
+#define RADAR_TIMEOUT 5 //min
+//#define TUBE_POWER_PIN 4
+#define TUBE_POWER_ON HIGH
+#define ENABLE_CLOCK_DISPLAY true
+#define DATE_REPEAT_MIN 1 //show date only every xxx minute. If zero, datum is never displayed
+//#define SHIFT_TUBES_LEFT_BY_1 //shift left by 1 tube the display, if a thermometer is used with spec tube
+#define TEMP_CHARCODE 10
+#define GRAD_CHARCODE 10
+#define PERCENT_CHARCODE 10
+#define AP_NAME "UNICLOCK32"
+#define AP_PASSWORD ""
+#define WEBNAME "Nixie-Clock"
+//#define DEFAULT_SSID ""
+//#define DEFAULT_PSW ""
+//#define USE_WIFIMANAGER
+//#define DISABLE_NIGHT_ANIMATION
+#endif
+
+#ifdef CLOCK_52 //ESP32, P.S. 2xHV5122 PCB version, 6xIN18 clock PROTOTYPE TESTING!!!
+#define DEBUG
+#define FW "fw52" //firmware name
+#define MAXBRIGHTNESS 100
+#define USE_NEOPIXEL
+#define NEOPIXEL_PIN 22
+byte tubePixels[] = {0, 1, 2, 3, 4, 5}; //6 tubes, single leds
+//#define USE_DALLAS_TEMP
+//#define TEMP_DALLAS_PIN 26 //Dallas temp sensor pin. If not used, SET TO -1
+//#define USE_DHT_TEMP
+//#define DHTTYPE DHT11
+//#define TEMP_DHT_PIN 23
+//#define USE_BME280 //I2C Temperature + humidity + pressure
+//#define USE_BMP280 //I2C Temperature + barometric pressure
+//#define USE_AHTX0 //I2C Temperature + humidity
+//#define USE_SHT21 //I2C Temperature + humidity
+//#define PIN_SDA 4 // you can set the used SDA and SCL pins
+//#define PIN_SCL 32 // if it is not default value
+#define PIN_EXTINP1 4 //extra input pin (not used)
+#define PIN_EXTINP2 32 //extra input pin (not used)
+#define USE_RTC
+#define PIN_SDA 27 //D1 SDA/SCL swapped
+#define PIN_SCL 26 //D2
+//_______________________________ HV5122 setup ____________________________________________________
+#define NEWHV5122
+#define PIN_DIN 16 // DataIn - chip0 DOUT pin is connected to chip1 DIN pin!
+#define PIN_CLK 17 // Clock
+#define PIN_OE 21 // OutputEnable
+#define MAXDIGITS 6
+int maxDigits = MAXDIGITS;
+byte digitPins[MAXDIGITS + 1][10] = { ////Data pin numbers 100+ means: chip1 pins are used. Chip0's DOUT is connected to chip1's DIN
+ {126, 127, 128, 130, 124, 123, 132, 131, 129, 125}, //hour 10 , CHIP0
+ {116, 117, 118, 120, 114, 113, 122, 121, 119, 115}, //hour 1 , CHIP0
+ {104, 105, 106, 108, 102, 101, 110, 109, 107, 103}, //min 10 , CHIP0
+ {26, 27, 28, 30, 24, 23, 32, 31, 29, 25}, //min 1 , CHIP1
+ {16, 17, 18, 20, 14, 13, 22, 21, 19, 15}, //second 10 , CHIP1
+ {4, 5, 6, 8, 2, 1, 10, 9, 7, 3}, //second 1 , CHIP1
+ {0, 111, 112, 11, 12, 0, 0, 0, 0, 0} //extra GL dots //GL1,GL2,GL3,GL4
+};
+#define MAKE_BLINKING_DOTS //it means, the extra datapins are used as 4 blinking dot instead of decimal points! #1..#4 positions are used, #0. and #5..#9. positions are zero!
+//_______________________________________________________________________________________
+#define ALARMSPEAKER_PIN 23 //Alarm buzzer pin
+//#define ALARMBUTTON_PIN 23 //Alarm switch off button pin
+#define ALARM_ON HIGH //How to switch ON alarm buzzer
+#define LIGHT_SENSOR_PIN 34
+#define RADAR_PIN 19
+#define RADAR_TIMEOUT 5 //min
+//#define TUBE_POWER_PIN 4
+//#define TUBE_POWER_ON LOW
+#define ENABLE_CLOCK_DISPLAY true
+#define DATE_REPEAT_MIN 60 //show date only every xxx minute. If zero, datum is never displayed
+#define DATE_START 07
+#define DATE_END 12
+#define TEMP_START 25
+#define TEMP_END 30
+#define HUMID_START 30
+#define HUMID_END 35
+#define SHIFT_TUBES_LEFT_BY_1 //shift left by 1 tube the display, if a thermometer is used with spec tube
+#define TEMP_CHARCODE 10
+#define GRAD_CHARCODE 10
+#define PERCENT_CHARCODE 10
+#define AP_NAME "IN-18-Clock"
+#define AP_PASSWORD ""
+#define WEBNAME "Nixie IN-18 Clock"
+//#define DEFAULT_SSID ""
+//#define DEFAULT_PSW ""
+//#define USE_WIFIMANAGER
+//#define DISABLE_NIGHT_ANIMATION
+#endif
+
+#ifdef CLOCK_53 //ESP8266 Wemos D1 mini,UNFI 2xHV5122 PCB version, 6xIN18 clock
+#define DEBUG
+#define FW "fw53" //firmware name
+#define MAXBRIGHTNESS 100
+#define USE_NEOPIXEL
+#define NEOPIXEL_PIN 3
+byte tubePixels[] = {0, 1, 2, 3, 4, 5}; //6 tubes, single leds
+#define USE_DALLAS_TEMP
+#define TEMP_DALLAS_PIN 12 //Dallas temp sensor pin. If not used, SET TO -1
+//#define LIGHT_SENSOR_PIN 23
+//#define USE_DHT_TEMP
+//#define DHTTYPE DHT11
+//#define TEMP_DHT_PIN 23
+
+//_______________________________ HV5122 setup ____________________________________________________
+#define NEWHV5122
+#define PIN_DIN 5 // DataIn - chip0 DOUT pin is connected to chip1 DIN pin!
+#define PIN_CLK 0 // Clock
+#define PIN_OE 4 // OutputEnable
+#define MAXDIGITS 6
+int maxDigits = MAXDIGITS;
+byte digitPins[MAXDIGITS + 1][10] = { ////Data pin numbers 100+ means: chip1 pins are used. Chip0's DOUT is connected to chip1's DIN
+ {2, 10, 9, 8, 7, 6, 5, 4, 3, 1}, //sec 1 , chip0
+ {11, 32, 20, 19, 18, 17, 16, 15, 14, 13}, //sec 10 , chip0
+ {22, 31, 29, 30, 27, 28, 25, 26, 23, 24}, //min 1 , chip0
+ {101, 131, 110, 109, 108, 107, 106, 105, 104, 103}, //min 10 , chip1
+ {111, 132, 120, 119, 118, 117, 116, 115, 114, 113}, //hour 1 , chip1
+ {122, 129, 130, 127, 128, 125, 126, 123, 124, 121}, //hour 10 , chip1
+ {0, 12, 21, 102, 112, 0, 0, 0, 0, 0} //extra decimal point (tube0...tube6)
+};
+//#define MAKE_BLINKING_DOTS //it means, the extra datapins are used as 4 blinking dot instead of decimal points! #1..#4 positions are used, #0. and #5..#9. positions are zero!
+//__________________________________________________________________________________________________
+#define ALARMSPEAKER_PIN 2 //Alarm buzzer pin
+#define ALARMBUTTON_PIN 13 //Alarm switch off button pin
+#define ALARM_ON HIGH //How to switch ON alarm buzzer
+//#define RADAR_PIN 4
+#define RADAR_TIMEOUT 5 //min
+//#define TUBE_POWER_PIN 4
+#define TUBE_POWER_ON LOW
+#define ENABLE_CLOCK_DISPLAY true
+#define DATE_REPEAT_MIN 1 //show date only every xxx minute. If zero, datum is never displayed
+//#define SHIFT_TUBES_LEFT_BY_1 //shift left by 1 tube the display, if a thermometer is used with spec tube
+#define TEMP_CHARCODE 10
+#define GRAD_CHARCODE 10
+#define PERCENT_CHARCODE 10
+#define AP_NAME "UNFICLOCK"
+#define AP_PASSWORD ""
+#define WEBNAME "UNFI Nixie Clock HV5122"
+//#define DEFAULT_SSID ""
+//#define DEFAULT_PSW ""
+//#define USE_WIFIMANAGER
+//#define DISABLE_NIGHT_ANIMATION
+#endif
+
+#ifdef CLOCK_54 // D1 R32 ESP32 (Uno compatible), + NCS312 Nixie clock KIT!!!
+#define DEBUG
+#define FW "fw54" //firmware name
+#define MAXBRIGHTNESS 100
+#define DOUBLE_BLINK
+//#define USE_NEOPIXEL
+#define NEOPIXEL_PIN 22
+byte tubePixels[] = {0, 1, 2, 3, 4, 5}; //6 tubes, single leds
+#define USE_DALLAS_TEMP
+#define TEMP_DALLAS_PIN 14 //Dallas temp sensor pin. If not used, SET TO -1
+//#define LIGHT_SENSOR_PIN 23
+//#define PIN_SDA 4 // you can set the used SDA and SCL pins
+//#define PIN_SCL 32 // if it is not default value
+//#define USE_DHT_TEMP
+//#define DHTTYPE DHT11
+//#define TEMP_DHT_PIN 23
+//#define USE_BME280 //I2C Temperature + humidity + pressure
+//#define USE_BMP280 //I2C Temperature + barometric pressure
+//#define USE_AHTX0 //I2C Temperature + humidity
+//#define USE_SHT21 //I2C Temperature + humidity
+#define NEWHV5122
+#define PIN_DIN 23 // DataIn - chip0 DOUT pin is connected to chip1 DIN pin!
+#define PIN_CLK 18 // Clock
+#define PIN_OE 5 // OutputEnable
+#define MAXDIGITS 6
+int maxDigits = MAXDIGITS;
+byte digitPins[MAXDIGITS + 1][10] = { ////Data pin numbers 100+ means: chip1 pins are used. Chip0's DOUT is connected to chip1's DIN
+ {121, 122, 123, 124, 125, 126, 127, 128, 129, 130}, //sec 1 , chip1
+ {111, 112, 113, 114, 115, 116, 117, 118, 119, 120}, //sec 10 , chip1
+ {101, 102, 103, 104, 105, 106, 107, 108, 109, 110}, //min 1 , chip1
+ {21, 22, 23, 24, 25, 26, 27, 28, 29, 30}, //min 10 , chip0
+ {11, 12, 13, 14, 15, 16, 17, 18, 19, 20}, //hour 1 , chip0
+ {1, 2, 3, 4, 5, 6, 7, 8, 9, 10}, //hour 10 , chip0
+ {0, 131, 132, 31, 32, 0, 0, 0, 0, 0} //extra GL dots
+};
+#define MAKE_BLINKING_DOTS
+#define USE_PWMLEDS
+#define PWM1_PIN 27 //Uno: D6
+#define PWM2_PIN 13 //Uno: D9
+#define PWM3_PIN 25 //Uno: D3
+//#define ALARMSPEAKER_PIN 26 //Alarm buzzer pin
+//#define ALARMBUTTON_PIN 2 //Alarm switch off button pin
+#define ALARM_ON HIGH //How to switch ON alarm buzzer
+//#define RADAR_PIN 4
+//#define RADAR_TIMEOUT 5 //min
+//#define TUBE_POWER_PIN 16
+#define TUBE_POWER_ON LOW
+#define ENABLE_CLOCK_DISPLAY true
+#define TEMP_CHARCODE 10
+#define GRAD_CHARCODE 10
+#define PERCENT_CHARCODE 10
+#define AP_NAME "UNICLOCK32"
+#define AP_PASSWORD ""
+#define WEBNAME "NCS312 Nixie Clock"
+//#define DEFAULT_SSID ""
+//#define DEFAULT_PSW ""
+//#define USE_WIFIMANAGER
+//#define DISABLE_NIGHT_ANIMATION
+#endif
+
+#ifdef CLOCK_55 //ESP8266 D1 Mini,UNFI 2xHV5122 PCB version, 6xZ573M clock
+#define DEBUG
+#define FW "fw55" //firmware name
+#define MAXBRIGHTNESS 100
+#define USE_NEOPIXEL
+#define NEOPIXEL_PIN 3
+byte tubePixels[] = {0, 1, 2, 3, 4, 5}; //6 tubes, single leds
+#define USE_DALLAS_TEMP
+#define TEMP_DALLAS_PIN 12 //Dallas temp sensor pin. If not used, SET TO -1
+//#define LIGHT_SENSOR_PIN 23
+//#define PIN_SDA 4 // you can set the used SDA and SCL pins
+//#define PIN_SCL 32 // if it is not default value
+//#define USE_DHT_TEMP
+//#define DHTTYPE DHT11
+//#define TEMP_DHT_PIN 23
+//#define USE_BME280 //I2C Temperature + humidity + pressure
+//#define USE_BMP280 //I2C Temperature + barometric pressure
+//#define USE_AHTX0 //I2C Temperature + humidity
+//#define USE_SHT21 //I2C Temperature + humidity
+//#define USE_BH1750 //I2C luxmeter sensor
+//_______________________________ HV5122 setup ____________________________________________________
+#define NEWHV5122
+#define PIN_DIN 5 // DataIn - chip0 DOUT pin is connected to chip1 DIN pin!
+#define PIN_CLK 0 // Clock
+#define PIN_OE 4 // OutputEnable
+#define MAXDIGITS 6
+int maxDigits = MAXDIGITS;
+byte digitPins[MAXDIGITS + 1][10] = { ////Data pin numbers 100+ means: chip1 pins are used. Chip0's DOUT is connected to chip1's DIN
+ {2, 10, 9, 8, 7, 6, 5, 4, 3, 1}, //sec 1 , chip0 (tube#0)
+ {11, 32, 20, 19, 18, 17, 16, 15, 14, 13}, //sec 10 , chip0 (tube#1)
+ {22, 31, 29, 30, 27, 28, 25, 26, 23, 24}, //min 1 , chip0 (tube#2)
+ {101, 131, 110, 109, 108, 107, 106, 105, 104, 103}, //min 10 , chip1 (tube#3)
+ {111, 132, 120, 119, 118, 117, 116, 115, 114, 113}, //hour 1 , chip1 (tube#4)
+ {122, 129, 130, 127, 128, 125, 126, 123, 124, 121}, //hour 10 , chip1 (tube#5)
+ {0, 12, 21, 102, 112, 0, 0, 0, 0, 0} //extra decimalPoint/blinking dots (tube0...tube6)
+};
+//#define MAKE_BLINKING_DOTS //it means, the extra datapins are used as 4 blinking dot instead of decimal points! #1..#4 positions are used
+//___________________________________________________________________________________
+#define ALARMSPEAKER_PIN 2 //Alarm buzzer pin
+#define ALARMBUTTON_PIN 13 //Alarm switch off button pin
+#define ALARM_ON HIGH //How to switch ON alarm buzzer
+//#define RADAR_PIN 4
+#define RADAR_TIMEOUT 5 //5min
+//#define TUBE_POWER_PIN 4
+#define TUBE_POWER_ON LOW
+#define ENABLE_CLOCK_DISPLAY true
+//#define DATE_REPEAT_MIN 1 //show date only every xxx minute. If zero, datum is never displayed
+//#define SHIFT_TUBES_LEFT_BY_1 //shift left by 1 tube the display, if a thermometer is used with spec tube
+#define TEMP_CHARCODE 10
+#define GRAD_CHARCODE 10
+#define PERCENT_CHARCODE 10
+#define AP_NAME "UNFICLOCK32"
+#define AP_PASSWORD ""
+#define WEBNAME "UNFI Nixie Clock HV5122"
+//#define DEFAULT_SSID ""
+//#define DEFAULT_PSW ""
+//#define USE_WIFIMANAGER
+//#define DISABLE_NIGHT_ANIMATION
+#endif
+
+#ifdef CLOCK_56 //WROOM ESP32, UNFI 2xHV5122 PCB version, 6xZ573M clock Example clock!!!
+#define DEBUG
+#define FW "fw56" //firmware name
+#define MAXBRIGHTNESS 100
+#define USE_NEOPIXEL
+#define NEOPIXEL_PIN 26
+byte tubePixels[] = {0, 1, 2, 3, 4, 5}; //6 tubes, single leds
+#define USE_DALLAS_TEMP
+#define TEMP_DALLAS_PIN 23 //Dallas temp sensor pin. If not used, SET TO -1
+//#define LIGHT_SENSOR_PIN 18
+#define PIN_SDA 21 // you can set the used SDA and SCL pins
+#define PIN_SCL 22 // if it is not default value
+//#define USE_DHT_TEMP
+//#define DHTTYPE DHT11
+//#define TEMP_DHT_PIN 23
+//#define USE_BME280 //I2C Temperature + humidity + pressure
+//#define USE_BMP280 //I2C Temperature + barometric pressure
+//#define USE_AHTX0 //I2C Temperature + humidity
+//#define USE_SHT21 //I2C Temperature + humidity
+//#define USE_BH1750 //I2C luxmeter sensor
+//_______________________________ HV5122 setup ____________________________________________________
+#define NEWHV5122
+#define PIN_DIN 14 // DataIn - chip0 DOUT pin is connected to chip1 DIN pin!
+#define PIN_CLK 12 // Clock
+#define PIN_OE 27 // OutputEnable
+#define MAXDIGITS 6
+int maxDigits = MAXDIGITS;
+byte digitPins[MAXDIGITS + 1][10] = {
+ {129, 128, 127, 126, 125, 124, 123, 132, 131, 130}, //sec 1 , chip1 (tube#0)
+ {118, 117, 116, 115, 114, 113, 112, 122, 121, 119}, //sec 10 , chip1 (tube#1)
+ {107, 106, 105, 104, 103, 102, 101, 111, 110, 108}, //min 1 , chip1 (tube#2)
+ {28, 27, 26, 25, 24, 23, 22, 32, 31, 29}, //min 10 , chip0 (tube#3)
+ {17, 16, 15, 14, 13, 12, 11, 21, 20, 18}, //hour 10 , chip0 (tube#4)
+ {7, 6, 5, 4, 3, 2, 1, 10, 9, 8}, //hour 1 , chip0 (tube#5)
+ {0, 0, 109, 30, 19, 0, 0, 0, 0, 0} //extra decimalPoint/blinking dots (tube0...tube6)
+};
+//#define MAKE_BLINKING_DOTS //it means, the extra datapins are used as 4 blinking dot instead of decimal points! #1..#4 positions are used
+//___________________________________________________________________________________
+#define ALARMSPEAKER_PIN 32 //Alarm buzzer pin
+#define ALARMBUTTON_PIN 33 //Alarm switch off button pin
+#define ALARM_ON HIGH //How to switch ON alarm buzzer
+//#define RADAR_PIN 4
+#define RADAR_TIMEOUT 5 //min
+//#define TUBE_POWER_PIN 4
+#define TUBE_POWER_ON HIGH
+#define ENABLE_CLOCK_DISPLAY true
+#define DATE_REPEAT_MIN 1 //show date only every xxx minute. If zero, datum is never displayed
+//#define SHIFT_TUBES_LEFT_BY_1 //shift left by 1 tube the display, if a thermometer is used with spec tube
+#define TEMP_CHARCODE 10
+#define GRAD_CHARCODE 10
+#define PERCENT_CHARCODE 10
+#define AP_NAME "UNICLOCK32"
+#define AP_PASSWORD "uniclock"
+#define WEBNAME "UNFI Nixie-Clock HV5122"
+#define DEFAULT_SSID "Unferdorben_2"
+#define DEFAULT_PSW "4073404540734"
+#define USE_WIFIMANAGER
+#define DISABLE_NIGHT_ANIMATION //in night mode, no animation allowed
+#endif
+
+#ifdef CLOCK_57 //WROOM ESP32, UNFI 2xHV5122 PCB version, 6xZM1000 clock PLCC44 socket!
+#define DEBUG
+#define FW "fw57" //firmware name
+#define MAXBRIGHTNESS 100
+#define FACTORYRESET_PIN 25
+#define USE_NEOPIXEL
+#define NEOPIXEL_PIN 26
+byte tubePixels[] = {0, 1, 2, 3, 4, 5}; //6 tubes, single leds
+#define USE_DALLAS_TEMP
+#define TEMP_DALLAS_PIN 23 //Dallas temp sensor pin. If not used, SET TO -1
+//#define LIGHT_SENSOR_PIN 18
+#define PIN_SDA 21 // you can set the used SDA and SCL pins
+#define PIN_SCL 22 // if it is not default value
+//#define USE_DHT_TEMP
+//#define DHTTYPE DHT11
+//#define TEMP_DHT_PIN 23
+//#define USE_BME280 //I2C Temperature + humidity + pressure
+//#define USE_BMP280 //I2C Temperature + barometric pressure
+//#define USE_AHTX0 //I2C Temperature + humidity
+//#define USE_SHT21 //I2C Temperature + humidity
+//#define USE_BH1750 //I2C luxmeter sensor
+//#define USE_MQTT
+//_______________________________ HV5122 setup ____________________________________________________
+#define NEWHV5122
+#define PIN_DIN 14 // DataIn - chip0 DOUT pin is connected to chip1 DIN pin!
+#define PIN_CLK 13 // Clock
+#define PIN_OE 27 // OutputEnable
+#define MAXDIGITS 6
+int maxDigits = MAXDIGITS;
+byte digitPins[MAXDIGITS + 1][10] = {
+ {130, 128, 126, 124, 123, 125, 127, 129, 131, 132}, //sec 1 , chip1 (tube#0) 0-1-2-3-4-5-6-7-8-9
+ {120, 118, 116, 114, 113, 115, 117, 119, 121, 122}, //sec 10 , chip1 (tube#1)
+ {109, 107, 105, 103, 102, 104, 106, 108, 110, 111}, //min 1 , chip1 (tube#2)
+ {30, 28, 26, 24, 23, 25, 27, 29, 31, 32}, //min 10 , chip0 (tube#3)
+ {19, 17, 15, 13, 12, 14, 16, 18, 20, 21}, //hour 10 , chip0 (tube#4)
+ {8, 6, 4, 2, 1, 3, 5, 7, 9, 10}, //hour 1 , chip0 (tube#5)
+ {0, 112, 101, 22, 11, 0} //Z574M {0,101,112,11,22,0}blinking dots (tube0...tube6)Z573M {0,112,101,22,11,0}
+};
+//#define MAKE_BLINKING_DOTS //it means, the extra datapins are used as 4 blinking dot instead of decimal points! #1..#4 positions are used
+//___________________________________________________________________________________
+#define ALARMSPEAKER_PIN 19 //Alarm buzzer pin
+#define ALARMBUTTON_PIN 33 //Alarm switch off button pin
+#define ALARM_ON HIGH //How to switch ON alarm buzzer
+//#define RADAR_PIN 4
+#define RADAR_TIMEOUT 5 //min
+//#define TUBE_POWER_PIN 4
+#define TUBE_POWER_ON HIGH
+//#define ENABLE_CLOCK_DISPLAY true
+//#define DATE_REPEAT_MIN 1 //show date only every xxx minute. If zero, datum is never displayed
+//#define SHIFT_TUBES_LEFT_BY_1 //shift left by 1 tube the display, if a thermometer is used with spec tube
+//#define TEMP_CHARCODE 10
+//#define GRAD_CHARCODE 10
+//#define PERCENT_CHARCODE 10
+#define AP_NAME "UNFICLOCK32"
+#define AP_PASSWORD "uniclock"
+#define WEBNAME " Nixie-Clock"
+#define DEFAULT_SSID ""
+#define DEFAULT_PSW ""
+//#define USE_WIFIMANAGER
+#endif
+
+#ifdef CLOCK_57A //WROOM ESP32, UNFI 2xHV5222 PCB version, 6xZ57xM clock Example clock!!!
+#define DEBUG
+#define FW "fw57A" //firmware name
+#define MAXBRIGHTNESS 100
+#define USE_NEOPIXEL
+#define NEOPIXEL_PIN 26
+byte tubePixels[] = {0, 1, 2, 3, 4, 5}; //6 tubes, single leds
+#define USE_DALLAS_TEMP
+#define TEMP_DALLAS_PIN 23 //Dallas temp sensor pin. If not used, SET TO -1
+//#define LIGHT_SENSOR_PIN 18
+#define PIN_SDA 21 // you can set the used SDA and SCL pins
+#define PIN_SCL 22 // if it is not default value
+//#define USE_DHT_TEMP
+//#define DHTTYPE DHT11
+//#define TEMP_DHT_PIN 23
+//#define USE_BME280 //I2C Temperature + humidity + pressure
+//#define USE_BMP280 //I2C Temperature + barometric pressure
+//#define USE_AHTX0 //I2C Temperature + humidity
+//#define USE_SHT21 //I2C Temperature + humidity
+//#define USE_BH1750 //I2C luxmeter sensor
+//#define USE_MQTT
+//_______________________________ HV5122 setup ____________________________________________________
+#define NEWHV5122
+#define PIN_DIN 14 // DataIn - chip0 DOUT pin is connected to chip1 DIN pin!
+#define PIN_CLK 13 // Clock
+#define PIN_OE 27 // OutputEnable
+#define MAXDIGITS 6
+int maxDigits = MAXDIGITS;
+byte digitPins[MAXDIGITS + 1][10] = {
+ {103, 105, 107, 109, 110, 108, 106, 104, 102, 101}, //sec 1 , chip1 (tube#0) 0-1-2-3-4-5-6-7-8-9
+ {113, 115, 117, 119, 120, 118, 116, 114, 112, 111}, //sec 10 , chip1 (tube#1)
+ {124, 126, 128, 130, 131, 129, 127, 125, 123, 122}, //min 1 , chip1 (tube#2)
+ {3, 5, 7, 9, 10, 8, 6, 4, 2, 1}, //min 10 , chip0 (tube#3)
+ {14, 16, 18, 20, 21, 19, 17, 15, 13, 12}, //hour 10 , chip0 (tube#4)
+ {25, 27, 29, 31, 32, 30, 28, 26, 24, 23}, //hour 1 , chip0 (tube#5)
+ {0, 121, 132, 11, 22, 0, 0, 0, 0} //extra decimalPoint/blinking dots (tube0...tube6)
+};
+//#define MAKE_BLINKING_DOTS //it means, the extra datapins are used as 4 blinking dot instead of decimal points! #1..#4 positions are used
+//___________________________________________________________________________________
+#define ALARMSPEAKER_PIN 19 //Alarm buzzer pin
+#define ALARMBUTTON_PIN 33 //Alarm switch off button pin
+#define ALARM_ON HIGH //How to switch ON alarm buzzer
+//#define RADAR_PIN 4
+#define RADAR_TIMEOUT 5 //min
+//#define TUBE_POWER_PIN 4
+#define TUBE_POWER_ON HIGH
+//#define ENABLE_CLOCK_DISPLAY true
+//#define DATE_REPEAT_MIN 1 //show date only every xxx minute. If zero, datum is never displayed
+//#define SHIFT_TUBES_LEFT_BY_1 //shift left by 1 tube the display, if a thermometer is used with spec tube
+//#define TEMP_CHARCODE 10
+//#define GRAD_CHARCODE 10
+//#define PERCENT_CHARCODE 10
+#define AP_NAME "UNFICLOCK"
+#define AP_PASSWORD "uniclock"
+#define WEBNAME "ZM1336 Nixie-Clock"
+#define DEFAULT_SSID ""
+#define DEFAULT_PSW ""
+//#define USE_WIFIMANAGER
+#endif
+
+#ifdef CLOCK_70 //1 tube esp8266 Nixie Clock by UNFI
+#define DEBUG
+#define FW "fw70" //firmware name
+#define MAXBRIGHTNESS 100
+#define TUBE1CLOCK
+byte ABCDPins[4] = {2, 4, 5, 15};
+#define AP_NAME "1TUBE_CLOCK"
+#define AP_PASSWORD ""
+#define WEBNAME "1TUBE Nixie Clock"
+//#define DISABLE_NIGHT_ANIMATION
+#endif
+
+#ifdef CLOCK_71 //2x VQC10 clock by UNFI
+#define DEBUG
+#define PIN_FLD_BUTTON 12
+#define PIN_SET_BUTTON 13
+//#define USE_GPS
+#define USE_RTC //no RTC exist, but the push buttons are defined here, too
+#define USE_DALLAS_TEMP
+#define TEMP_DALLAS_PIN 15 //PIN16 not working!!!
+#define FW "fw71" //firmware name
+#define MAXBRIGHTNESS 100
+#define VQC10
+#define latchPin 5 //D2 RCK
+#define clockPin 4 //D1 SCK
+#define dataPin 2 //D4 DIN
+#define ledOffpin 0 //D5
+#define D5pin 14
+#define AP_NAME "VQC10_LED_CLOCK"
+#define AP_PASSWORD ""
+#define WEBNAME "VQC10 LED Clock"
+//#define DEFAULT_SSID "xxx" //factory reset default wifi ssid/psw
+//#define DEFAULT_PSW "yyy"
+//#define USE_WIFIMANAGER
+//#define DISABLE_NIGHT_ANIMATION
+#endif
+
+// *** This is the CURRENTLY ACTIVE clock (CLOCK_72 is the one un-commented in the list at the top of
+// this file). See ZM1500.ino for the driver. ***
+#ifdef CLOCK_72 //ESP32, UNFI board, ZM1500
+#define DEBUG
+#define ANIMSPEED 50
+#define FW "fw72" //firmware name
+#define MAXBRIGHTNESS 255
+//#define SIMULATE_TEMPERATURE_SENSOR
+//#define USE_DALLAS_TEMP
+//#define USE_DHT_TEMP
+//#define DHTTYPE DHT22
+//#define TEMP_DHT_PIN 27
+#define TEMP_DALLAS_PIN 27 //Dallas temp sensor pin. If not used, SET TO -1
+#define ZM1500_ESP32
+byte digitEnablePins[] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11}; //{11,10,9,8,7,6,5,4,3,2,1,0}; //12 tubes selection values
+byte segmentEnablePins[] = {7, 6, 5, 4, 3, 2, 1, 0};
+#define dataPin 23 //D5
+#define latchPin 18 //D6
+#define clkPin 19 //D7
+//#define TEMP_CHARCODE -1 //disable char => shift display right with 1 digit
+//#define GRAD_CHARCODE -1 //disable char => shift display right with 1 digit
+#define AP_NAME "ZM1500_CLOCK"
+#define AP_PASSWORD ""
+#define WEBNAME "ZM1500-Clock"
+//#define DEFAULT_SSID ""
+//#define DEFAULT_PSW ""
+//#define USE_WIFIMANAGER
+//#define DISABLE_NIGHT_ANIMATION
+#endif
+
+#ifdef CLOCK_80 //word clock, english version
+#define DEBUG
+//#define SIMULATE_TEMPERATURE_SENSOR //for test only!!!
+#define NEOPIXEL_PIN 4
+//#define PIN_FLD_BUTTON 27
+//#define PIN_SET_BUTTON 14
+#define LED1_PIN 25
+#define LED2_PIN 26
+#define LIGHT_SENSOR_PIN 35
+#define RADAR_PIN 16
+#define RADAR_TIMEOUT 5 //min
+//#define USE_GPS
+#define PIN_SDA 21 // you can set the used SDA and SCL pins
+#define PIN_SCL 22 // if it is not default value
+#define USE_RTC //RTC + push buttons are defined here
+#define USE_BME280 //I2C Temperature + humidity + pressure
+#define USE_AHTX0 //I2C Temperature + humidity
+#define USE_SHT21 //I2C Temperature + humidity
+#define FW "fw80" //firmware name
+#define START_CORNER LEFTUP //RIGHTUP, LEFTUP, RIGHTDOWN, LEFTDOWN : starting corner of led stripe
+#define MAXBRIGHTNESS 255 //maximum led brightness, 100..255
+#define MAXIMUM_LUX 40
+#define WORDCLOCK
+#define AP_NAME "WORD_CLOCK"
+#define AP_PASSWORD ""
+#define WEBNAME "WordClock"
+#define MDNSNAME "wordclock"
+//#define DEFAULT_SSID "xxx" //factory reset default wifi ssid/psw
+//#define DEFAULT_PSW "yyy"
+//#define USE_WIFIMANAGER
+//#define DISABLE_NIGHT_ANIMATION
+//
+//----- WORD CLOCK TEXT DEFINITIONS -----------------
+#define WORD_LENGTH 15 //maximum length of any word
+struct wordDef {
+ char txt[WORD_LENGTH + 1]; //readable word
+ int startX; //led X positions
+ int startY; //led Y position
+ int len; //number of characters
+};
+#define XMAX 11 //Clock dimensions
+#define YMAX 11
+int extraDots[4] = {XMAX*YMAX + 3, XMAX*YMAX + 2, XMAX*YMAX + 1, XMAX * YMAX}; //extra dots led numbers
+#define WEEKDAY_POSX 1 //start of weekdays
+#define WEEKDAY_POSY 11 //start of weekdays
+#define SENSOR_POSX 8 //start position of extra dots
+#define SENSOR_POSY 11
+
+const char matrix[YMAX][XMAX + 1] = {
+ {"itLisASwifi"},
+ {"quarterXset"},
+ {"twentyfiveX"},
+ {"halfBtenFto"},
+ {"pastERUNINE"},
+ {"ONESIXTHREE"},
+ {"FOURFIVETWO"},
+ {"EIGHTELEVEN"},
+ {"SEVENTWELVE"},
+ {"TENSEoclock"},
+ {"SMTWTFSCF%W"}
+};
+
+const char sequence[12][40] = { //#1 = actual hour, #2 = actual hour + 1
+ "it is #1 oclock",
+ "it is five past #1",
+ "it is ten past #1",
+ "it is quarter past #1",
+ "it is twenty past #1",
+ "it is twenty five past #1",
+ "it is half past #1",
+ "it is twenty five to #2",
+ "it is twenty to #2",
+ "it is quarter to #2",
+ "it is ten to #2",
+ "it is five to #2"
+};
+
+struct wordDef hours[] {
+ {"O"}, {"ONE"}, {"TWO"}, {"THREE"}, {"FOUR"}, {"FIVE"}, {"SIX"}, {"SEVEN"}, {"EIGHT"}, {"NINE"}, {"TEN"}, {"ELEVEN"}, {"TWELVE"},
+};
+
+struct wordDef words[] {
+ {"it"}, {"is"}, {"wifi"}, {"ip"}, {"twenty"}, {"five"}, {"ten"}, {"quarter"}, {"half"}, {"to"}, {"past"}, {"oclock"}, {"SMTWTFS"}, {"CF%W"}
+};
+#endif
+
+#ifdef CLOCK_81 //word clock, german version
+#define DEBUG
+#define NEOPIXEL_PIN 4
+//#define PIN_FLD_BUTTON 27
+//#define PIN_SET_BUTTON 14
+#define LED1_PIN 25
+#define LED2_PIN 26
+#define LIGHT_SENSOR_PIN 35
+#define RADAR_PIN 16
+#define RADAR_TIMEOUT 5 //min
+//#define USE_GPS
+#define PIN_SDA 21 // you can set the used SDA and SCL pins
+#define PIN_SCL 22 // if it is not default value
+#define USE_RTC //RTC + push buttons are defined here
+#define USE_BME280 //I2C Temperature + humidity + pressure
+#define USE_AHTX0 //I2C Temperature + humidity
+#define USE_SHT21 //I2C Temperature + humidity
+#define FW "fw81" //firmware name
+#define START_CORNER RIGHTUP //RIGHTUP, LEFTUP, RIGHTDOWN, LEFTDOWN : starting corner of led stripe
+#define MAXBRIGHTNESS 255 //led maximum brightness 100...255
+#define MAXIMUM_LUX 40
+#define WORDCLOCK
+#define AP_NAME "WORD_CLOCK"
+#define AP_PASSWORD ""
+#define WEBNAME "wordclock"
+//#define DEFAULT_SSID "xxx" //factory reset default wifi ssid/psw
+//#define DEFAULT_PSW "yyy"
+//#define USE_WIFIMANAGER
+//#define DISABLE_NIGHT_ANIMATION
+//
+//----- WORD CLOCK TEXT DEFINITIONS -----------------
+#define WORD_LENGTH 15 //maximum length of any word
+struct wordDef {
+ char txt[WORD_LENGTH + 1]; //readable word
+ int startX; //led X positions
+ int startY; //led Y position
+ int len; //number of characters
+};
+
+#define XMAX 11 //Clock dimensions
+#define YMAX 11
+int extraDots[4] = {XMAX*YMAX + 3, XMAX*YMAX + 2, XMAX*YMAX + 1, XMAX * YMAX}; //extra dots led numbers
+#define WEEKDAY_POSX 1 //start of weekdays
+#define WEEKDAY_POSY 11 //start of weekdays
+#define SENSOR_POSX 8 //start position of extra dots
+#define SENSOR_POSY 11
+
+const char matrix[YMAX][XMAX + 1] = {
+ {"esListAwifi"},
+ {"zehnzwanzig"},
+ {"dreiviertel"},
+ {"funfvornach"},
+ {"halbOELFUNF"},
+ {"EINSXAMZWEI"},
+ {"DREIAUJVIER"},
+ {"SECHSNLACHT"},
+ {"SIEBENZWOLF"},
+ {"ZEHNEUNKuhr"},
+ {"SMTWTFSCF%W"}
+};
+
+const char sequence[12][40] = { //#1 = actual hour, #2 = actual hour + 1
+ "es ist #1 uhr",
+ "es ist funf nach #1",
+ "es ist zehn nach #1",
+ "es ist viertel #2",
+ "es ist zehn vor halb #2",
+ "es ist funf vor halb #2",
+ "es ist halb #2",
+ "es ist funf nach halb #2",
+ "es ist zehn nach halb #2",
+ "es ist drei viertel #2",
+ "es ist zehn vor #2",
+ "es ist funf vor #2"
+};
+
+struct wordDef hours[] {
+ {"O"}, {"EIN"}, {"ZWEI"}, {"DREI"}, {"FIER"}, {"FUNF"}, {"SECHS"}, {"SIEBEN"}, {"ACHT"}, {"NEUN"}, {"ZEHN"}, {"ELF"}, {"ZWOLF"},
+};
+
+struct wordDef words[] {
+ {"es"}, {"ist"}, {"zwanzig"}, {"funf"}, {"zehn"}, {"drei"}, {"viertel"}, {"halb"}, {"vor"}, {"nach"}, {"uhr"}, {"SMDMDFS"}, {"CF%W"}, {"wifi"}
+};
+#endif
+
+#ifdef CLOCK_82 //word clock, hungarian version
+#define DEBUG
+#define NEOPIXEL_PIN 4
+//#define PIN_FLD_BUTTON 27
+//#define PIN_SET_BUTTON 14
+#define LED1_PIN 25
+#define LED2_PIN 26
+#define LIGHT_SENSOR_PIN 35
+#define RADAR_PIN 16
+#define RADAR_TIMEOUT 5 //min
+//#define USE_GPS
+#define PIN_SDA 21 // you can set the used SDA and SCL pins
+#define PIN_SCL 22 // if it is not default value
+#define USE_RTC //RTC + push buttons are defined here
+#define USE_BME280 //I2C Temperature + humidity + pressure
+#define USE_AHTX0 //I2C Temperature + humidity
+#define USE_SHT21 //I2C Temperature + humidity
+#define FW "fw82" //firmware name
+#define START_CORNER RIGHTUP //RIGHTUP, LEFTUP, RIGHTDOWN, LEFTDOWN : starting corner of led stripe
+#define MAXBRIGHTNESS 255 //led max brightness 100...255
+#define MAXIMUM_LUX 40
+#define WORDCLOCK
+#define AP_NAME "WORD_CLOCK"
+#define AP_PASSWORD ""
+#define WEBNAME "wordclock"
+//#define DEFAULT_SSID "xxx" //factory reset default wifi ssid/psw
+//#define DEFAULT_PSW "yyy"
+//#define USE_WIFIMANAGER
+//#define DISABLE_NIGHT_ANIMATION
+//
+//----- WORD CLOCK TEXT DEFINITIONS -----------------
+#define WORD_LENGTH 15 //maximum length of any word
+struct wordDef {
+ char txt[WORD_LENGTH + 1]; //readable word
+ int startX; //led X positions
+ int startY; //led Y position
+ int len; //number of characters
+};
+
+#define XMAX 11 //Clock dimensions
+#define YMAX 11
+int extraDots[4] = {XMAX*YMAX + 3, XMAX*YMAX + 2, XMAX*YMAX + 1, XMAX * YMAX}; //extra dots led numbers
+#define WEEKDAY_POSX 1 //start of weekdays
+#define WEEKDAY_POSY 11 //start of weekdays
+#define SENSOR_POSX 8 //start position of extra dots
+#define SENSOR_POSY 11
+
+const char matrix[YMAX][XMAX + 1] = {
+ {"otBtizJwifi"},
+ {"perccelEWip"},
+ {"multmulvaQZ"},
+ {"haromUXfelJ"},
+ {"negyedXHHAT"},
+ {"NEGYAHAROMS"},
+ {"NYOLCOTZHET"},
+ {"TIZEN__1EBC"},
+ {"TIZENKETTOA"},
+ {"KILENCForaP"},
+ {"SMTWTFSCF%W"}
+};
+
+const char sequence[12][40] = { //#1 = actual hour, #2 = actual hour + 1
+ "#1 ora",
+ "ot perccel mult #1 ora",
+ "tiz perccel mult #1 ora",
+ "negyed #2",
+ "tiz perc mulva fel #2",
+ "ot perc mulva fel #2",
+ "fel #2",
+ "ot perccel mult fel #2",
+ "tiz perccel mult fel #2",
+ "harom negyed #2",
+ "tiz perc mulva #2 ora",
+ "ot perc mulva #2 ora"
+};
+
+struct wordDef hours[] {
+ {"O"}, {"__1"}, {"KETTO"}, {"HAROM"}, {"NEGY"}, {"OT"}, {"HAT"}, {"HET"}, {"NYOLC"}, {"KILENC"}, {"TIZ"}, {"TIZEN__1"}, {"TIZENKETTO"},
+};
+
+struct wordDef words[] {
+ {"ora"}, {"ot"}, {"tiz"}, {"harom"}, {"negyed"}, {"fel"}, {"perc"}, {"perccel"}, {"mulva"}, {"mult"}, {"wifi"}, {"ip"}, {"VHKSCPS"}
+};
+#endif
+
+#ifdef CLOCK_99 //Dummy clock, sensor box with MQTT connector
+#define DEBUG
+#define FW "fw99" //firmware name
+#define MAXBRIGHTNESS 100
+//#define USE_DALLAS_TEMP
+#define TEMP_DALLAS_PIN 26 //Dallas temp sensor pin. If not used, SET TO -1
+//#define LIGHT_SENSOR_PIN 23
+#define PIN_SDA 4 // you can set the used SDA and SCL pins
+#define PIN_SCL 32 // if it is not default value
+#define USE_DHT_TEMP
+#define DHTTYPE DHT11
+#define TEMP_DHT_PIN 26
+//#define USE_RTC
+//#define USE_GPS
+#define USE_BME280 //I2C Temperature + humidity + pressure
+#define USE_BMP280 //I2C Temperature + barometric pressure
+#define USE_AHTX0 //I2C Temperature + humidity
+#define USE_SHT21 //I2C Temperature + humidity
+#define USE_BH1750 //I2C luxmeter sensor
+#define USE_MQTT
+//#define USE_MASTER_CLOCK //if used, this will be the sensor#0
+//#define USE_MASTER_TEMP
+//#define USE_MASTER_HUMID
+//#define MASTER_TEMPERATURE_TOPIC "homeassistant/sensor/10521c5e14c4/temperature/state"
+//#define MASTER_HUMIDITY_TOPIC "homeassistant/sensor/10521c5e14c4/humidity/state"
+//#define MASTER_RADAR_TOPIC "homeassistant/sensor/10521c5e14c4/radar/state"
+//_______________________________ Driver setup ____________________________________________________
+#define DUMMY
+//___________________________________________________________________________________
+#define ALARMSPEAKER_PIN 16 //Alarm buzzer pin
+//#define ALARMBUTTON_PIN 23 //Alarm switch off button pin
+#define ALARM_ON HIGH //How to switch ON alarm buzzer
+#define RADAR_PIN 22
+#define RADAR_TIMEOUT 5 //min
+#define ENABLE_CLOCK_DISPLAY true
+#define DATE_REPEAT_MIN 3 //show date only every xxx minute. If zero, datum is never displayed
+//#define SHIFT_TUBES_LEFT_BY_1 //shift left by 1 tube the display, if a thermometer is used with spec tube
+#define TEMP_CHARCODE 10
+#define GRAD_CHARCODE 10
+#define PERCENT_CHARCODE 10
+#define AP_NAME "SENSORBOX"
+#define AP_PASSWORD ""
+#define WEBNAME "SENSORBOX"
+//#define DEFAULT_SSID ""
+//#define DEFAULT_PSW ""
+//#define USE_WIFIMANAGER
+#endif
+
+
+
+//__________________________________________________________________________________________________________________________
+//if not defined, here is defined as NOT USED
+//
+// FALLBACK DEFAULTS SECTION: every "#ifndef X / #define X " pair below fires only if the
+// active CLOCK_NN block above did NOT already define that macro. This is what keeps each CLOCK_NN
+// block short (it only needs to override what's actually different from these defaults). Pin
+// defaults are -1, which the core .ino and driver files everywhere treat as "not connected / feature
+// disabled" (e.g. "#if RADAR_PIN >= 0" guards). If you add a new optional parameter to clocks.h,
+// add its fallback here too so old CLOCK_NN blocks that predate it keep compiling correctly.
+#ifndef COLON_PIN
+#define COLON_PIN -1 //Blinking Colon pin.
+#endif
+
+#ifndef TEMP_DALLAS_PIN
+#define TEMP_DALLAS_PIN -1 //Dallas temp sensor pin.
+#endif
+
+#ifndef TEMP_DHT_PIN
+#define TEMP_DHT_PIN -1 //DHT temp sensor pin.
+#endif
+
+#if defined(USE_BME280) || defined(USE_BMP280) || defined(USE_AHTX0) || defined(USE_SHT21) || defined(USE_BH1750) || defined(USE_RTC)
+#if defined(PIN_SDA) && defined(PIN_SCL)
+#define USE_I2CSENSORS
+#else
+#warning "PIN_SDA or PIN_SCL is not defined!"
+#endif
+#endif
+
+#ifndef LED_SWITCH_PIN
+#define LED_SWITCH_PIN -1 //external led backlight ON/OFF.
+#endif
+
+#ifndef DECIMALPOINT_PIN
+#define DECIMALPOINT_PIN -1 //Nixie decimal point between digits.
+#endif
+
+#ifndef ALARMSPEAKER_PIN
+#define ALARMSPEAKER_PIN -1 //Alarm buzzer pin
+#endif
+#ifndef ALARMBUTTON_PIN
+#define ALARMBUTTON_PIN -1 //Alarm switch off button pin
+#endif
+
+#ifdef USE_NEOPIXEL
+#ifndef NEOPIXEL_PIN
+#define NEOPIXEL_PIN 3
+#endif
+#endif
+#ifndef RGB_MIN_BRIGHTNESS
+#define RGB_MIN_BRIGHTNESS 8 //Neopixel leds minimum brightness
+#endif
+#ifndef RGB_MAX_BRIGHTNESS
+#define RGB_MAX_BRIGHTNESS 255 //Neopixel leds maximum brightness
+#endif
+
+#ifndef TUBE_POWER_PIN
+#define TUBE_POWER_PIN -1 //Tube filament or HV ON/OFF
+#endif
+// POSSIBLE BUG: this guard checks "#ifndef TUBE_POWER_PIN" again (always false here, since the block
+// right above just defined it unconditionally) instead of "#ifndef TUBE_POWER_ON" - so this fallback
+// for TUBE_POWER_ON never actually fires. In practice this is harmless today: every CLOCK_NN block
+// that leaves TUBE_POWER_PIN undefined (i.e. feature disabled, defaults to -1 above) either also
+// defines TUBE_POWER_ON itself anyway, or never references it (all TUBE_POWER_ON usage in the core
+// .ino is gated behind "#if TUBE_POWER_PIN >= 0"). But if a future CLOCK_NN block used TUBE_POWER_PIN
+// without also setting TUBE_POWER_ON, this would leave TUBE_POWER_ON completely undefined and fail
+// to compile at the digitalWrite(TUBE_POWER_PIN, TUBE_POWER_ON) call site in ESP32_UniClock2.ino.
+#ifndef TUBE_POWER_PIN
+#define TUBE_POWER_ON LOW //Tube Power On is HIGH or LOW?
+#endif
+
+#ifndef RADAR_PIN
+#define RADAR_PIN -1 //Radar sensor for automatic ON/OFF the TUBE_POWER
+#endif
+
+#ifndef LIGHT_SENSOR_PIN
+#define LIGHT_SENSOR_PIN -1 ///Environment light sensor, analog input pin
+#endif
+
+#ifndef RADAR_TIMEOUT
+#define RADAR_TIMEOUT 5 //Automatic switch off tubes (without radar detecting somebody) after xxx min
+#endif
+
+#ifndef ALARM_ON
+#define ALARM_ON HIGH
+#endif
+
+#ifndef MAXBRIGHTNESS
+#define MAXBRIGHTNESS 10 //do not change this value
+#endif
+
+//Display temperature and date in every minute between START..END seconds
+#ifndef ENABLE_CLOCK_DISPLAY
+#define ENABLE_CLOCK_DISPLAY true //false, if no clock display is needed (for example: thermometer + humidity only)
+#endif
+
+#ifndef TEMP_START
+#define TEMP_START 35
+#endif
+
+#ifndef TEMP_END
+#define TEMP_END 40
+#endif
+
+#ifndef HUMID_START
+#define HUMID_START 40
+#endif
+
+#ifndef HUMID_END
+#define HUMID_END 45
+#endif
+
+#ifndef DATE_START
+#define DATE_START 45
+#endif
+
+#ifndef DATE_END
+#define DATE_END 50
+#endif
+
+#ifndef ANIMSPEED
+#define ANIMSPEED 50 //Animation speed in millisec
+#endif
+
+#ifndef TEMP_CHARCODE
+#define TEMP_CHARCODE 15
+#endif
+
+#ifndef GRAD_CHARCODE
+#define GRAD_CHARCODE 16
+#endif
+
+#ifndef PERCENT_CHARCODE
+#define PERCENT_CHARCODE 17
+#endif
+
+#ifndef UPPER_CIRCLE_CHARCODE
+#define UPPER_CIRCLE_CHARCODE 16
+#endif
+
+#ifndef LOWER_CIRCLE_CHARCODE
+#define LOWER_CIRCLE_CHARCODE 18
+#endif
+
+
+#ifndef LEFTDECIMAL
+#define LEFTDECIMAL false //set true (Z574M), if decimal point is on the left side on the tube. Else set false (Z573M)!
+#endif
+
+//______________________________________________________________________________________
+//--------------- LDR LIGHT_SENSOR LUXMETER parameters ------------
+#ifndef LIGHT_SENSOR_PIN
+#define LIGHT_SENSOR_PIN -1
+#endif
+#if defined(ESP32)
+#define MAX_ADC_READING 4095 //ESP32 ADC_REF_VOLTAGE = 3.3V !!
+#else
+#define MAX_ADC_READING 1023 //8266 ADC_REF_VOLTAGE = 1.0V !!
+#endif
+#ifndef REF_RESISTANCE
+#define REF_RESISTANCE 10000.0 // Resistor value is 10k, between LDR sensor and GND
+#endif
+#ifndef LUX_CALC_SCALAR
+#define LUX_CALC_SCALAR 12518931 * 1.2 //Calibrate here
+#endif
+#ifndef LUX_CALC_EXPONENT
+#define LUX_CALC_EXPONENT -1.405
+#endif
+#ifndef MAXIMUM_LUX
+#define MAXIMUM_LUX 100
+#endif
+#ifndef PWM1_PIN
+#define PWM1_PIN -1
+#endif
+#ifndef PWM2_PIN
+#define PWM2_PIN -1
+#endif
+#ifndef PWM3_PIN
+#define PWM3_PIN -1
+#endif
+#ifndef DATE_REPEAT_MIN
+#define DATE_REPEAT_MIN 1
+#endif
+#ifndef FW
+#define FW "UNKNOWN"
+#endif
+#ifndef DP_PIN
+#define DP_PIN -1 //Decimal Point pin.
+#endif
+//______________________________________________________________________________________
diff --git a/firmware/uniclock/ESP32_UniClock2/dallas_temp.ino b/firmware/uniclock/ESP32_UniClock2/dallas_temp.ino
new file mode 100644
index 0000000..1dd2358
--- /dev/null
+++ b/firmware/uniclock/ESP32_UniClock2/dallas_temp.ino
@@ -0,0 +1,222 @@
+// =================================================================================================
+// dallas_temp.ino - DS18B20 (1-Wire) digital temperature sensor driver
+// -------------------------------------------------------------------------------------------------
+// Uses the OneWire + DallasTemperature libraries (libraries/OneWire.zip, libraries/DallasTemperature.zip)
+// to talk to one or two DS18B20 sensors sharing a SINGLE 1-Wire data bus on pin TEMP_DALLAS_PIN
+// (a 4.7k pull-up resistor between DATA and VCC is required on the bus, as usual for 1-Wire).
+// Enabled per-clock via USE_DALLAS_TEMP in clocks.h; the pin itself is set via TEMP_DALLAS_PIN.
+// Used (as of this writing) by CLOCK_3, CLOCK_5, CLOCK_6A, CLOCK_7, CLOCK_12, CLOCK_14, CLOCK_15,
+// CLOCK_15a, CLOCK_16, CLOCK_20, CLOCK_26, CLOCK_30, CLOCK_31, CLOCK_35, CLOCK_40..CLOCK_49 (Nixie/VFD
+// UNFI board revisions), CLOCK_51, CLOCK_53..CLOCK_57A and CLOCK_71 - see clocks.h for the exact list.
+//
+// Because a DS18B20 conversion takes up to ~750ms, readings are NOT taken with a blocking call.
+// Instead a request/poll pattern is used: requestDallasTemp() kicks off a conversion, and getTemp()
+// is called repeatedly (every ~200ms from timeProgram() in the core .ino) until DS_delay has elapsed,
+// at which point the converted value is actually fetched from the sensor. See the two functions below.
+// =================================================================================================
+#ifdef USE_DALLAS_TEMP //DS18B20 sensor
+
+// Change HERE 0<>1, to swap sensors!!!
+// NOTE: these two macros are not referenced anywhere else in this file (or the rest of the project) -
+// looks like dead/leftover code; swapping sensor order today has to be done differently (see below).
+#define SENSOR1 0
+#define SENSOR2 1
+#define TEMPERATURE_PRECISION 12 // DS18B20 resolution in bits (9-12); 12 bits = 0.0625C steps, ~750ms conversion time
+
+#include
+#include
+OneWire oneWire(TEMP_DALLAS_PIN); // Set up a OneWire instance to communicate with OneWire devices
+DallasTemperature tempSensors(&oneWire); // Create an instance of the temperature sensor class
+DeviceAddress thermometer1, thermometer2; // 64-bit ROM addresses of sensor #1 / #2, read once in setupDallasTemp()
+boolean requested = false; // true while a conversion is in progress (request sent, result not yet fetched)
+unsigned long lastRequest = millis(); // millis() timestamp of the last requestTemperatures() call
+boolean DallasOK = true; //is any measure from Dallas thermometer?
+float lastTemperature = 0; // NOTE: written nowhere, read nowhere else in the project - appears to be dead/unused
+char lastTemperatureStr[5] = "----"; // NOTE: only ever written (see getTemp() below), never read back anywhere - looks unused/vestigial
+unsigned long intervalTemp = 30000; // Do a temperature measurement every 30sec
+unsigned long DS_delay = 800; // Reading the temperature from the DS18x20 can take up to 750ms
+
+// Indexes into the shared temperature[]/humid[] arrays (declared in ESP32_UniClock2.ino) assigned to
+// this driver's sensor(s) at setup time. Multiple sensor drivers (Dallas/DHT/I2C) share those arrays,
+// each claiming the next free slot via the global useTemp counter - see setupDallasTemp() below.
+byte d1Ptr = 0;
+byte d2Ptr = 0;
+
+void setupDallasTemp() {
+ regPin(TEMP_DALLAS_PIN,"TEMP_DALLAS_PIN");
+ pinMode(TEMP_DALLAS_PIN,OUTPUT);
+ #if defined(ESP8266)
+ oneWire.reset(); // ESP8266 needs an explicit bus reset before the first transaction; ESP32 does not
+ delay(200); //200ms
+ #endif
+ tempSensors.begin(); // Start the temperature sensor
+ tempSensors.setResolution(TEMPERATURE_PRECISION);
+ tempSensors.setWaitForConversion(false); // Don't block the program while the temperature sensor is reading
+ delay(100); //100ms
+ // Blocking probe done ONCE at boot: request a conversion and wait out the full 800ms here (setup()
+ // isn't time-critical), just so we can count/enumerate the sensors below. During normal operation
+ // (requestDallasTemp()/getTemp()) the same conversion is done non-blocking instead.
+ tempSensors.requestTemperatures();
+ delay(800);
+ int counter = 0;
+ while ((tempSensors.getDeviceCount() == 0) && (counter<5)) {
+ if (counter>3) {
+ DPRINT("No DS18x20 temperature sensor found on GPIO"); DPRINTLN(TEMP_DALLAS_PIN);
+ resetSensors();
+ }
+ counter++;
+ delay(100);
+ } //end while
+
+ // useDallasTemp: 0 = no sensor, 1 = single sensor, 2 = two sensors on the same bus.
+ // The two DS18B20s are NOT told apart by their ROM address/serial number - they are simply
+ // enumerated by their scan order on the bus (DallasTemperature index 0 and index 1, see
+ // getAddress()/getTempCByIndex() below), which is whatever order the library's internal 1-Wire
+ // search finds them in. There is no user-facing way here to pin a physical sensor to a fixed
+ // index; the SENSOR1/SENSOR2 macros above look like an abandoned attempt at that.
+ useDallasTemp = tempSensors.getDeviceCount();
+ if (useDallasTemp>0) {
+ d1Ptr = useTemp; //remember the pointers of sensors
+ useTemp++;
+ if (useDallasTemp>1) {
+ d2Ptr = useTemp;
+ useTemp++;
+ }
+ DPRINT("DS18B20 sensors found:"); DPRINTLN(useDallasTemp);
+ if (!tempSensors.getAddress(thermometer1, 0)) DPRINTLN("Unable to find address for Device 0");
+ if ((useDallasTemp>1) && !tempSensors.getAddress(thermometer2, 1)) DPRINTLN("Unable to find address for Device 1");
+ // Kick off one more conversion right away (rather than waiting for the normal once-a-minute
+ // trigger in requestDallasTemp()) so a real reading is available soon after boot.
+ // POSSIBLE BUG: this only delays 200ms, but getTemp() below requires DS_delay (800ms) to have
+ // elapsed since the request before it will actually fetch anything - with only 200ms elapsed,
+ // getTemp() just returns immediately here without reading temperature[]. `requested` stays true,
+ // so the real fetch only happens later, whenever timeProgram()'s periodic getTemp() call is
+ // reached ~800ms after this request - i.e. this call does not actually pre-populate temperature[]
+ // before setup() returns, despite appearances.
+ requestDallasTemp(true);
+ delay(200);
+ getTemp();
+ }
+}
+
+// Called every ~200ms from timeProgram() (with force=false). Does NOT block: requestTemperatures()
+// with setWaitForConversion(false) (set in setupDallasTemp()) just tells the sensor(s) to start
+// converting and returns immediately; the actual value is picked up later by getTemp() once enough
+// time has passed. EEPROMsaving is checked because 1-Wire bus timing is sensitive to interrupts/delays,
+// and EEPROMsaving also doubles as a "display refresh suspended" flag elsewhere in the project.
+void requestDallasTemp(boolean force) {
+ if (EEPROMsaving) return;
+
+ // Old, no-longer-used scheme: re-request every intervalTemp (30s) regardless of clock time.
+ //if (force || (!requested && ((millis()-lastRequest) > intervalTemp))) { //request a new reading
+ // Current scheme: instead of a fixed interval, a new conversion is requested once per minute,
+ // exactly 1 second before prm.tempStart (the second-of-minute when the display starts showing the
+ // temperature) - so by the time the display wants to show it, the ~750-800ms conversion has had
+ // time to finish and getTemp() already has a fresh value ready.
+ if (force || (!requested && (second()==prm.tempStart-1))) { //request a new reading
+ tempSensors.requestTemperatures(); // Request the temperature from the sensor (it takes some time to read it)
+ requested = true;
+ lastRequest = millis();
+ DPRINTLN("Temperature requested");
+ }
+}
+
+// Called every ~200ms from timeProgram() (and once directly from setupDallasTemp()). This is the
+// other half of the async request/read pattern: it does nothing until DS_delay has elapsed since the
+// matching requestDallasTemp() call, at which point the conversion is guaranteed finished and the
+// (already-converted) value can be read from the sensor over 1-Wire without blocking/waiting.
+void getTemp() { //get the earlier requested temperature data
+static int errors = 0;
+float tmp_temp;
+
+ if (!requested) return; // no conversion pending - nothing to fetch yet
+ if ((millis()- lastRequest) < DS_delay) return; // 1000 ms after requesting the temperature
+ // ^ comment says 1000ms but DS_delay is actually 800ms (see top of file) - conversion itself needs
+ // up to ~750ms at 12-bit resolution, so 800ms leaves a small safety margin.
+ if (EEPROMsaving) return; // bus busy (flash write in progress) - try again on the next ~200ms tick
+
+ requested = false; // mark conversion consumed; a new request is needed before we read again
+ if (tempSensors.getDeviceCount() == 0) {
+ // Sensor(s) no longer answering on the bus (unplugged, wiring fault, bus glitch, etc.)
+ // POSSIBLE BUG: temperature[d2Ptr] is zeroed unconditionally here, even when useDallasTemp<=1
+ // (i.e. only one sensor was ever configured and d2Ptr was never assigned an index, so it is
+ // still 0 - its C++ default). If some other sensor driver's reading happens to live at slot 0
+ // and this Dallas sensor's own slot (d1Ptr) is not 0, this line would stomp on that other
+ // sensor's temperature[] entry instead of (or in addition to) this one. Contrast with the
+ // success path below, which correctly guards the second-sensor update with `if (useDallasTemp>1)`.
+ temperature[d1Ptr] = 0; temperature[d2Ptr] = 0;
+ DallasOK = false;
+ resetSensors();
+ DPRINTLN("Missing TEMP sensor!");
+ // NOTE: decrementing lastRequest here has no actual effect on when the retry happens, because
+ // the trigger condition in requestDallasTemp() is `second()==prm.tempStart-1`, a fixed point in
+ // the minute, not a millis()-based interval check anymore (that scheme is the commented-out
+ // line above). So despite the "retry ASAP!" comment, the next request still only fires at the
+ // next occurrence of second()==prm.tempStart-1 (i.e. up to ~60s later), same as `errors` below.
+ lastRequest -= intervalTemp; //retry ASAP!
+ }
+ else {
+ delay(10);
+ tmp_temp = tempSensors.getTempCByIndex(0); // Get the temperature from the sensor
+ // DallasTemperature reports -127C (DEVICE_DISCONNECTED_C) when a read fails - checked here
+ // via "> -127" rather than comparing to the library's named constant.
+ if (tmp_temp > -127) {
+ tmp_temp = round(tmp_temp * 10.0) / 10.0; // round temperature to 1 digits
+ DallasOK = true;
+ // POSSIBLE BUG: writes to temperature[0] literally instead of temperature[d1Ptr] (used
+ // everywhere else, e.g. the DPRINT right below and in the "sensor missing" branch above).
+ // This happens to be equivalent today because setupDallasTemp() is always called first in
+ // setup() (see ESP32_UniClock2.ino), so d1Ptr is always 0 - but it's fragile: if any other
+ // sensor driver (DHT/I2C) were ever initialized before this one, or a config combined Dallas
+ // with another sensor type such that useTemp>0 at the point setupDallasTemp() runs, this
+ // sensor's reading would land in the wrong slot in temperature[], while d1Ptr would still
+ // point at its correctly-reserved (but now unwritten) slot.
+ temperature[0] = tmp_temp;
+ DPRINT("Dallas#1 temp:"); DPRINTLN(temperature[d1Ptr]);
+ dtostrf(temperature[0], 4, 1, lastTemperatureStr); // formats temperature[0] into lastTemperatureStr, which (see top of file) is never read anywhere
+ errors = 0; // any successful read clears the consecutive-error counter
+ if (useDallasTemp>1) {
+ temperature[d2Ptr] = round(tempSensors.getTempCByIndex(1) * 10.0) / 10.0; // round temperature to 1 digits
+ DPRINT("Dallas#2 temp:"); DPRINTLN(temperature[d2Ptr]);
+ }
+ }
+ else {
+ // Sensor answered on the bus but returned a bad/disconnected reading (-127C) this cycle.
+ DPRINT("Temp sensor error!!!");
+ DPRINTLN(tmp_temp);
+ errors++;
+ lastRequest -= intervalTemp; //retry ASAP! (see NOTE above - has no effect on the actual retry timing)
+ resetSensors();
+ }
+ }
+ // After 20 consecutive failed reads (~20 minutes, since retries are gated to once/minute), give up
+ // on the sensor entirely so the UI/display stops trying to show a Dallas temperature.
+ if (errors>20)
+ useDallasTemp = 0; //forget BAD sensor!!!
+}
+
+// Attempts to recover a 1-Wire bus that has stopped responding (called from getTemp() whenever a
+// sensor is missing or returns a bad reading). Blocking (delay(500) x2 = ~1s total), but it's only
+// invoked on error paths, not on every normal read cycle, so it doesn't affect steady-state timing.
+void resetSensors() {
+ if (EEPROMsaving) return;
+ DPRINTLN("Reset sensors...");
+ //#if defined(ESP8266)
+ oneWire.reset();
+ //#endif
+ delay(500); //200ms
+
+ tempSensors.begin(); //try to restart sensor
+ tempSensors.setWaitForConversion(false);
+ tempSensors.getDeviceCount();
+ delay(500); //200ms
+}
+//-----------------------------------------------------------------------------------------------------
+#else
+// USE_DALLAS_TEMP not defined for this CLOCK_NN config: compile out to no-op stubs so the rest of the
+// codebase (setup()/timeProgram() in ESP32_UniClock2.ino) can call these unconditionally.
+void setupDallasTemp() {}
+void requestDallasTemp(boolean force) {}
+void getTemp() {}
+void resetSensors() {}
+#endif
diff --git a/firmware/uniclock/ESP32_UniClock2/data/favicon.ico b/firmware/uniclock/ESP32_UniClock2/data/favicon.ico
new file mode 100644
index 0000000000000000000000000000000000000000..0cbd449ee6d09edce941c6d1b94cd7102e80ccfa
GIT binary patch
literal 318
zcma)1K?;B{42wna=py5s_F}7G5U;?lZiEu*q
zU#sg5
+
+
+ Uniclock 3.0
+
+
+
+
+
+
+
+
+
+
+
+
X
+
Random Input
+
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.