Import ESP_UniClock (upstream 1189810)
Vendored from https://github.com/gpeter62/ESP_UniClock: the sketch source, the web UI and the libraries the ESP8266 build needs, unpacked from the bundled zips. Adds a PlatformIO project for the ESP-12. The source is unchanged; local changes follow as separate commits. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
8efa810e4e
commit
b347f1c63f
843 files changed
+122061
No files matched your search
@@ -0,0 +1,306 @@
|
||||
/*
|
||||
* ======================================================================
|
||||
* 1TUBE.INO - driver for the UNFI "1TUBE" single-Nixie-tube clock
|
||||
* ======================================================================
|
||||
* Hardware: ONE physical Nixie tube (e.g. IN-16/Z-series) fed by a single
|
||||
* BCD-to-decimal decoder/driver IC (e.g. 74141/K155ID1-class chip) wired
|
||||
* to the 4 "ABCDPins" GPIOs. Because there is only one tube, the classic
|
||||
* simultaneous-multiplex approach used by multi-tube drivers cannot show
|
||||
* several digits at once; instead this driver walks through the 4 logical
|
||||
* digit positions (H,H,M,M held in digit[0..3]) ONE AT A TIME, holding
|
||||
* each on the single tube for a while and cross-fading (PWM wipe) between
|
||||
* the old and new character of that position before advancing.
|
||||
* Board target: ESP8266 (uses the ESP8266 timer1 API: timer1_write(),
|
||||
* ICACHE_RAM_ATTR); this driver is NOT built for ESP32.
|
||||
* Protocol: 4 parallel GPIO lines (ABCDPins[0..3]) carry the BCD nibble
|
||||
* that the decoder IC translates into "light cathode N" on the tube.
|
||||
* BCD codes 10 (0xA) and 15 (0xF) are both outside the valid 0-9 range,
|
||||
* so the decoder lights no cathode for them - used here as "blank" codes.
|
||||
* Used by CLOCK_NN configs (see clocks.h): CLOCK_70 ("1 tube esp8266
|
||||
* Nixie Clock by UNFI") is the only config that #defines TUBE1CLOCK.
|
||||
* ======================================================================
|
||||
*/
|
||||
#ifdef TUBE1CLOCK
|
||||
char tubeDriver[] = "1TUBE";
|
||||
//used by UNFI 1TUBE Clock
|
||||
|
||||
//byte ABCDPins[4] = {2,5,4,15}; //defined in clocks.h
|
||||
|
||||
int maxDigits = 4; //4 LOGICAL digit positions (H,H,M,M) even though there is only 1 PHYSICAL tube
|
||||
int pos = 0; //pointer of show phase - which of the 4 logical digit positions is currently on the single tube
|
||||
|
||||
int PWMrefresh = 15000; //msec, multiplex time period. Greater value => slower multiplex frequency
|
||||
int PWM_min = 2000;
|
||||
int PWM_max = 15000;
|
||||
int maxDig=maxDigits;
|
||||
|
||||
void setup_pins() {
|
||||
char tmp[30];
|
||||
|
||||
DPRINTLN("1TUBE Clock - setup pins");
|
||||
|
||||
for (int i=0;i<4;i++) {
|
||||
pinMode(ABCDPins[i], OUTPUT);
|
||||
sprintf(tmp,"Pin[%c]",char('A'+i));
|
||||
regPin(ABCDPins[i],tmp);
|
||||
}
|
||||
startTimer();
|
||||
}
|
||||
|
||||
void ICACHE_RAM_ATTR writeBits(byte num) {
|
||||
//Send the 4-bit BCD code to the decoder IC, LSB first on ABCDPins[0].
|
||||
//Note: digitalWrite() treats ANY non-zero value as HIGH, so passing the
|
||||
//raw masked bit (0,1,2,4,8) instead of a strict 0/1 is fine here.
|
||||
for (int j=0;j<4;j++) {
|
||||
digitalWrite(ABCDPins[j],num & (1<<j));
|
||||
}
|
||||
}
|
||||
|
||||
void ICACHE_RAM_ATTR writeDisplay(){ //https://circuits4you.com/2018/01/02/esp8266-timer-ticker-example/
|
||||
//This is the ESP8266 hardware-timer1 ISR (single-shot mode: it must
|
||||
//re-arm itself via timer1_write() at the end of every call - see
|
||||
//startTimer() in the core .ino, which sets timer1 to TIM_SINGLE).
|
||||
//Because there is only ONE physical tube, "multiplex" here does not mean
|
||||
//cycling through several tubes per refresh - it means running a tiny
|
||||
//state machine (0/1/2) that, for the CURRENT logical position `pos`
|
||||
//only, PWM-blends between the old and new character (cross-fade "wipe"
|
||||
//animation) and then blanks the tube for the rest of the PWM period to
|
||||
//control brightness. Advancing `pos` itself is NOT done here; it is
|
||||
//done by writeDisplay2()/makeAnim(), which run from the main loop.
|
||||
static volatile byte state=0;
|
||||
static int timer = PWMrefresh;
|
||||
static int brightness;
|
||||
static int PWMtimeBrightness=PWM_min;
|
||||
byte animM;
|
||||
byte num;
|
||||
|
||||
if (EEPROMsaving) { //stop refresh while an EEPROM write is in progress!
|
||||
timer1_write(PWMrefresh);
|
||||
return;
|
||||
}
|
||||
//POSSIBLE BUG (ISR race condition): digit[], newDigit[], oldDigit[], animMask[]
|
||||
//and `pos` are all read here from the timer ISR while makeAnim()/writeDisplay2()
|
||||
//write to the very same globals from the main loop (no lock, no `volatile`
|
||||
//on the arrays). On ESP8266 a torn read of an int-sized array element is
|
||||
//unlikely but not architecturally guaranteed; worth a closer look if visual
|
||||
//glitches during animation are ever reported.
|
||||
animM = 0;
|
||||
if (animMask[pos] > 0) animM = animMask[pos];
|
||||
brightness = displayON ? prm.dayBright : prm.nightBright;
|
||||
if (brightness>MAXBRIGHTNESS) brightness = MAXBRIGHTNESS; //only for safety
|
||||
|
||||
if ((brightness ==0) || !radarON){
|
||||
writeBits(0xA); //black display - BCD code 10 is outside 0-9, decoder lights no cathode
|
||||
|
||||
#if DECIMALPOINT_PIN>=0
|
||||
digitalWrite(DECIMALPOINT_PIN,LOW);
|
||||
#endif
|
||||
#if COLON_PIN >= 0
|
||||
digitalWrite(COLON_PIN,LOW); // colon pin OFF
|
||||
#endif
|
||||
timer1_write(100*PWMrefresh); //re-check only occasionally while display is fully off (saves ISR overhead)
|
||||
return;
|
||||
}
|
||||
else {
|
||||
digit[4] = 0xA; //extra "5th" slot (blank) used as the makeAnim() end-of-cycle marker/target position - see writeDisplay2()
|
||||
if (state==0) {
|
||||
if (animMask[pos]==0) num = digit[pos];
|
||||
else num = oldDigit[pos]; //mid-animation: still fading OUT the previous character
|
||||
}
|
||||
else if (state==1) num = newDigit[pos]; //mid-animation: now fading IN the new character
|
||||
else num = 0xF; //BCD code 15 - also outside 0-9, another "blank" pattern (belt-and-braces vs 0xA)
|
||||
|
||||
writeBits(num);
|
||||
switch (state) { //state machine: 0=show current/old char, 1=show new char (animation only), 2=blank (brightness/PWM off-time)
|
||||
case 0:
|
||||
#if DECIMALPOINT_PIN>=0
|
||||
if (decimalpointON) digitalWrite(DECIMALPOINT_PIN,HIGH);
|
||||
#endif
|
||||
#if COLON_PIN>=0
|
||||
digitalWrite(COLON_PIN,colonBlinkState); // Blink colon pin
|
||||
#endif
|
||||
if (autoBrightness && displayON) {
|
||||
PWMtimeBrightness = max(PWM_min,PWM_max*lx/MAXIMUM_LUX); //ambient-light-driven ON-time within the PWM period
|
||||
}
|
||||
else {
|
||||
PWMtimeBrightness = max(PWM_min,PWM_max*brightness/MAXBRIGHTNESS); //fixed day/night brightness setting -> ON-time
|
||||
}
|
||||
if (animM > 0) { //Animation running for this position? animM (1..19) is the wipe progress from animMask[pos]
|
||||
timer = (PWMtimeBrightness * (20-animM))/20; //old-character share of the ON-time shrinks as animM grows
|
||||
state = 1; //next state is: show newDigits
|
||||
}
|
||||
else {
|
||||
timer = PWMtimeBrightness;
|
||||
if (brightness>=MAXBRIGHTNESS) state = 0; //at max brightness skip the blank phase entirely (always-on, no flicker)
|
||||
else state = 2; //default next state is: BLANK display
|
||||
}
|
||||
break;
|
||||
case 1: //show new character, if animation
|
||||
timer = (PWMtimeBrightness * animM)/20; //new-character share of the ON-time grows as animM grows
|
||||
if (brightness>=MAXBRIGHTNESS) state = 0;
|
||||
else state = 2; //default next state is: BLANK display
|
||||
break;
|
||||
case 2: //blank display
|
||||
#if DECIMALPOINT_PIN>=0
|
||||
digitalWrite(DECIMALPOINT_PIN,LOW);
|
||||
#endif
|
||||
#if COLON_PIN>=0
|
||||
digitalWrite(COLON_PIN,LOW); // colon pin OFF
|
||||
#endif
|
||||
state = 0;
|
||||
timer = PWMrefresh-PWMtimeBrightness; //remainder of the fixed-length PWM period -> keeps overall refresh rate constant
|
||||
break;
|
||||
} //end switch
|
||||
} //end else
|
||||
|
||||
if ((brightness == 0) || (!radarON)) {timer = PWM_max; state = 0;} //no time sharing is needed
|
||||
if (timer<500) timer = 500; //safety only... (timer1 min practical reload value; too-short reload could starve other ISRs/WiFi)
|
||||
timer1_write(timer);
|
||||
}
|
||||
|
||||
//Cooperative/non-blocking-ish delay: since the whole time-showing sequence
|
||||
//in writeDisplay2()/makeAnim() is written as a straight-line blocking loop
|
||||
//(one call per animation step), we can't just call delay(d) - that would
|
||||
//starve WiFi, the captive-portal DNS, the alarm buzzer and GPS parsing for
|
||||
//the whole animation. So Adelay() busy-waits but keeps servicing them.
|
||||
void Adelay(unsigned long d) {
|
||||
unsigned long dStart = millis();
|
||||
|
||||
doAnimationMakuna();
|
||||
while ((millis() - dStart) < d) {
|
||||
if (WiFi.getMode() == 2) dnsServer.processNextRequest(); //WiFi.getMode()==2 is WIFI_AP: service the captive-portal DNS while in AP config mode
|
||||
alarmSound();
|
||||
#ifdef USE_GPS
|
||||
smartDelay(1); //feed GPS
|
||||
#endif
|
||||
yield(); //let the ESP8266 core service WiFi/SDK background tasks (required to avoid WDT resets)
|
||||
}
|
||||
}
|
||||
|
||||
//Drives the crossfade of the SINGLE current position `pos` from its old
|
||||
//character to its new one. This is only ever meaningful for one tube at a
|
||||
//time (unlike multi-tube drivers' changeDigit(), which is bypassed
|
||||
//entirely for TUBE1CLOCK - see core .ino changeDigit()). Called once per
|
||||
//position from writeDisplay2()'s position-walk loop.
|
||||
void makeAnim() {
|
||||
static byte oldDig = 10; //remembers the digit that was actually shown last time THIS function ran (persists across positions/cycles)
|
||||
|
||||
if (cathodeProtRunning) {
|
||||
//Anti-cathode-poisoning routine: just snap to the target digit, no fade,
|
||||
//no PWM animation bookkeeping - doCathodeProtect() drives digit[] itself.
|
||||
memset(animMask,0,sizeof(animMask));
|
||||
digit[pos] = newDigit[pos];
|
||||
return;
|
||||
|
||||
}
|
||||
else if ((prm.animMode==0) || ipShowRunning ){
|
||||
//Animation disabled by user setting, or showing the boot-time IP address: snap instantly.
|
||||
digit[pos] = newDigit[pos];
|
||||
memset(animMask,0,sizeof(animMask));
|
||||
Adelay(ANIMSPEED*20);
|
||||
return;
|
||||
}
|
||||
|
||||
//Time showing
|
||||
if (oldDig == newDigit[pos]) oldDig = 10; //if the old digit == new digit, use space instead (avoids a pointless fade from a digit onto an identical digit)
|
||||
oldDigit[pos] = oldDig; //last displayed digit
|
||||
//DPRINT(oldDigit[pos]); DPRINT("/"); DPRINTLN(newDigit[pos]);
|
||||
for (int i = 1; i < 20; i++) {
|
||||
animMask[pos] = i; //drives the writeDisplay() ISR's PWM wipe ratio (old-share shrinks, new-share grows) for this position, step by step
|
||||
Adelay(ANIMSPEED);
|
||||
}
|
||||
oldDig = newDigit[pos]; //save last displayed digit
|
||||
digit[pos] = newDigit[pos];
|
||||
animMask[pos] = 0;
|
||||
Adelay(5*ANIMSPEED);
|
||||
if (pos==2) {
|
||||
//pos==2 is the "hours ones" digit - digit[3]=hour tens, digit[2]=hour ones,
|
||||
//digit[1]=minute tens, digit[0]=minute ones (set in core .ino's time-digit
|
||||
//code). The walk in writeDisplay2() goes 3,2,1,0, so after showing the HH
|
||||
//part (positions 3 and 2) we insert a blank gap here so the HH/MM halves
|
||||
//of the time are visually separated on the single tube instead of running together.
|
||||
digit[pos] = 10;
|
||||
oldDigit[pos] = 10;
|
||||
oldDig = 10;
|
||||
Adelay(20*ANIMSPEED);
|
||||
}
|
||||
}
|
||||
|
||||
//Required common-interface function ("blocking, non-interrupt, one-shot
|
||||
//write of the display buffer") - but for this single-tube driver the
|
||||
//actual pixel/segment output is always produced asynchronously by the
|
||||
//writeDisplay() timer ISR, driven by `pos`. So this implementation does
|
||||
//NOT push bits to hardware itself; it only redirects `pos` to the single
|
||||
//physical tube's slot (index 3, reused as the cathode-protect "digit")
|
||||
//so that the next ISR tick renders whatever doCathodeProtect() just wrote
|
||||
//into digit[3]/newDigit[3]. Outside cathode-protect it is a no-op, so
|
||||
//normal time updates still rely entirely on writeDisplay2() below
|
||||
//(called from the main loop - see changeDigit() in the core .ino, which
|
||||
//for TUBE1CLOCK short-circuits to just calling this function and returning).
|
||||
void writeDisplaySingle() {
|
||||
if (cathodeProtRunning) {
|
||||
pos = 3;
|
||||
}
|
||||
}
|
||||
|
||||
//NOT part of the required driver interface (setup_pins/writeDisplay/
|
||||
//writeDisplaySingle/clearTubes/maxDigits/tubeDriver) - this is an EXTRA
|
||||
//function that the core .ino calls directly (only for TUBE1CLOCK/VQC10/
|
||||
//WORDCLOCK, see the `writeDisplay2` stub at the bottom of the core file)
|
||||
//from the main loop to drive the single tube's sequential "flying digit"
|
||||
//display, since a single tube can't be updated purely by changeDigit()'s
|
||||
//normal per-position-array animation.
|
||||
void writeDisplay2(void){
|
||||
static unsigned long lastShow = 0;
|
||||
|
||||
if (cathodeProtRunning) { //show cathode protect, only one digit
|
||||
pos = 3;
|
||||
return;
|
||||
}
|
||||
|
||||
if (ipShowRunning) { //boot-time diagnostic: show the device's IP address, 3 digits at a time, on the single tube
|
||||
memset(oldDigit,10,sizeof(oldDigit));
|
||||
newDigit[3] = 10;
|
||||
digit[3] = 10;
|
||||
for (int i=2;i>=0;i--) {
|
||||
pos = i;
|
||||
makeAnim();
|
||||
digit[pos] = 10;
|
||||
Adelay(200);
|
||||
}
|
||||
pos = 3;
|
||||
makeAnim();
|
||||
pos = 0;
|
||||
ipShowRunning = false;
|
||||
memset(digit,10,sizeof(digit));
|
||||
memset(newDigit,10,sizeof(newDigit));
|
||||
memset(oldDigit,10,sizeof(oldDigit));
|
||||
memset(animMask,0,sizeof(animMask));
|
||||
Adelay(500);
|
||||
return;
|
||||
}
|
||||
|
||||
if ((millis()-lastShow)<3000) return; //show time - only start a new HH/MM sweep once every 3s; between sweeps the last-shown digit just stays lit via the ISR
|
||||
for (int i=3;i>=0;i--) {
|
||||
pos = i;
|
||||
makeAnim();
|
||||
if (prm.animMode==0) { //make some pause
|
||||
digit[pos] = 10;
|
||||
Adelay((i==2)? 400 : 200); //extra-long pause after the HH half (i==2) to emphasize the HH/MM boundary
|
||||
}
|
||||
}
|
||||
newDigit[4] = 10;
|
||||
pos = 4; //parks the state machine on the unused 5th slot (always 0xA/blank, see digit[4]=0xA in writeDisplay()) so the tube goes dark between sweeps
|
||||
makeAnim();
|
||||
memcpy(digit,newDigit,sizeof(digit));
|
||||
memcpy(oldDigit,newDigit,sizeof(oldDigit));
|
||||
memset(animMask,0,sizeof(animMask));
|
||||
lastShow = millis();
|
||||
}
|
||||
|
||||
//Required common-interface function, but intentionally a no-op here: the core .ino no longer has
|
||||
//any caller for clearTubes() at all (its only former call site, disableDisplay(), has been removed
|
||||
//entirely as dead code), so there is nothing to implement. Actual blanking of the single tube is
|
||||
//handled inside writeDisplay()'s own brightness==0 / !radarON branch instead.
|
||||
void clearTubes() {}
|
||||
#endif
|
||||
@@ -0,0 +1,132 @@
|
||||
// =================================================================================================
|
||||
// DHT_temp.ino - DHT11 / DHT22 (AM2302/AM2321) / DHT21 combined temperature+humidity sensor driver
|
||||
// -------------------------------------------------------------------------------------------------
|
||||
// Single-wire (not 1-Wire protocol, just one signal wire) sensor read via the Adafruit DHT sensor
|
||||
// library (libraries/DHT_sensor_library.zip), on pin TEMP_DHT_PIN with sensor type DHTTYPE.
|
||||
// Enabled per-clock via USE_DHT_TEMP in clocks.h (pin: TEMP_DHT_PIN, type: DHTTYPE, default DHT22).
|
||||
// Used (as of this writing) by CLOCK_18, CLOCK_21, CLOCK_22, CLOCK_23A, CLOCK_23B, CLOCK_32, CLOCK_50
|
||||
// and CLOCK_99 - see clocks.h for the exact list. Unlike dallas_temp.ino, this driver only ever
|
||||
// supports a single physical sensor (one temperature + one humidity reading).
|
||||
//
|
||||
// DHT sensors are slow and must not be polled too often (Adafruit recommends >=2s between reads,
|
||||
// otherwise you get stale or garbage data back) - getDHTemp() below enforces its own minimum gap
|
||||
// between reads independent of the DHT library (which does not enforce this itself).
|
||||
// =================================================================================================
|
||||
#ifdef USE_DHT_TEMP
|
||||
|
||||
// REQUIRES the following Arduino libraries:
|
||||
// - DHT Sensor Library: https://github.com/adafruit/DHT-sensor-library
|
||||
// - Adafruit Unified Sensor Lib: https://github.com/adafruit/Adafruit_Sensor
|
||||
|
||||
#include "DHT.h"
|
||||
// TEMP_DHT_PIN is used to connect the sensor
|
||||
// Feather HUZZAH ESP8266 note: use pins 3, 4, 5, 12, 13 or 14 --
|
||||
// Pin 15 can work but DHT must be disconnected during program upload.
|
||||
|
||||
// Uncomment whatever type you're using!
|
||||
//#define DHTTYPE DHT11 // DHT 11
|
||||
#ifndef DHTTYPE
|
||||
#define DHTTYPE DHT22 // DHT 22 (AM2302), AM2321 //default DHT type
|
||||
#endif
|
||||
//#define DHTTYPE DHT21 // DHT 21 (AM2301)
|
||||
|
||||
// Connect pin 1 (on the left) of the sensor to +5V
|
||||
// NOTE: If using a board with 3.3V logic like ESP8266 or ESP32 connect pin 1
|
||||
// to 3.3V instead of 5V!
|
||||
// Connect pin 2 of the sensor to whatever your DHTPIN is
|
||||
// Connect pin 4 (on the right) of the sensor to GROUND
|
||||
// Connect a 10K resistor from pin 2 (data) to pin 1 (power) of the sensor
|
||||
|
||||
|
||||
DHT dht(TEMP_DHT_PIN, DHTTYPE);
|
||||
int DHTtempPtr = -1; // index into the shared temperature[] array reserved for this sensor; -1 = "no sensor found / not set up"
|
||||
int DHThumidPtr = -1; // index into the shared humid[] array reserved for this sensor; -1 = "no sensor found / not set up"
|
||||
|
||||
// Probes for a DHT sensor at boot. Up to 5 attempts, 1 second apart (DHT sensors need time between
|
||||
// reads to settle - see getDHTemp() below for the steady-state rate limit), stopping as soon as one
|
||||
// attempt returns a valid (non-NaN) temperature. Only DHTtempPtr/DHThumidPtr are left at -1 (their
|
||||
// initialized value) if no sensor ever responds, which is how getDHTemp() knows to do nothing later.
|
||||
void setupDHTemp() {
|
||||
float tempTMP, humidTMP;
|
||||
regPin(TEMP_DHT_PIN,"TEMP_DHT_PIN");
|
||||
dht.begin();
|
||||
int i= 0;
|
||||
while (i<5) {
|
||||
humidTMP = dht.readHumidity();
|
||||
tempTMP = dht.readTemperature();
|
||||
DPRINT("DHT Test:"); DPRINT(tempTMP); DPRINT("/"); DPRINTLN(humidTMP);
|
||||
i++;
|
||||
if (!isnan(tempTMP)) // note: only the temperature reading is checked here, humidity's NaN state is not verified
|
||||
break;
|
||||
delay(1000);
|
||||
}
|
||||
if (isnan(tempTMP)) {
|
||||
DPRINT("No DHTxx sensor found on GPIO"); DPRINTLN(TEMP_DHT_PIN);
|
||||
return; // DHTtempPtr/DHThumidPtr stay -1 and useTemp/useHumid are not incremented - sensor is effectively disabled
|
||||
}
|
||||
DHTtempPtr = useTemp; //remember my ID-s
|
||||
DHThumidPtr = useHumid;
|
||||
useTemp++; //increase the number of sensors
|
||||
useHumid++;
|
||||
}
|
||||
|
||||
// Called every ~200ms from timeProgram(), AND once directly from setup() right after pin init (see
|
||||
// ESP32_UniClock2.ino) to get an initial reading before the first display refresh.
|
||||
// Unlike the DS18B20 driver, the DHT library's readTemperature()/readHumidity() calls themselves
|
||||
// block for ~250ms (bit-banged single-wire protocol, no async/non-blocking mode available) - there is
|
||||
// no separate request/poll split here, it's a straightforward "gate how often we call it" scheme instead.
|
||||
void getDHTemp() {
|
||||
static unsigned long lastRun = 0;
|
||||
float tempTMP, humidTMP;
|
||||
|
||||
if (EEPROMsaving) return;
|
||||
if (DHTtempPtr<0) return; //no sensor
|
||||
// Rate limiting: DHT11/DHT22 sensors must not be read faster than roughly once every 2 seconds, or
|
||||
// they return stale/garbage data - readings are gated to once per minute, at the exact second
|
||||
// prm.tempStart (the same "start of temperature display window" second used to schedule the Dallas
|
||||
// driver's request), which is always >=2.5s apart in practice for any sane tempStart. The 2500ms
|
||||
// millis()-based check below is really just a safety net (e.g. against `second()` briefly matching
|
||||
// more than once, or the caller re-entering the same second) - the schedule itself already respects
|
||||
// the sensor's minimum read interval.
|
||||
//if (((millis()-lastRun)<2500) || (second()!=prm.tempStart)) return;
|
||||
// `&& (lastRun !=0)` makes the very first call (from setup(), before lastRun has ever been set)
|
||||
// bypass both the 2.5s and the second()==tempStart gating, so a reading is taken immediately at boot
|
||||
// instead of waiting for the clock to reach prm.tempStart.
|
||||
if ((((millis()-lastRun)<2500) || (second()!=prm.tempStart)) && (lastRun !=0)) return;
|
||||
lastRun = millis();
|
||||
|
||||
// Reading temperature or humidity takes about 250 milliseconds!
|
||||
// Sensor readings may also be up to 2 seconds 'old' (its a very slow sensor)
|
||||
humidTMP = round1(dht.readHumidity());
|
||||
// Range check doubles as the NaN/error guard: a NaN humidity fails both comparisons (NaN vs any value
|
||||
// is always false in C/C++), so a failed read simply leaves humid[DHThumidPtr] holding its previous value.
|
||||
if ((humidTMP>0) && (humidTMP<=100)) humid[DHThumidPtr] = humidTMP;
|
||||
tempTMP = round1(dht.readTemperature()); // Read temperature as Celsius (the default)
|
||||
// Same implicit-NaN-guard trick as above: NaN<99 is false, so a failed read is silently skipped and
|
||||
// temperature[DHTtempPtr] keeps its last good value rather than being overwritten with garbage.
|
||||
if (tempTMP<99) temperature[DHTtempPtr] = tempTMP;
|
||||
|
||||
// Read temperature as Fahrenheit (isFahrenheit = true)
|
||||
//float f = dht.readTemperature(true); Compute heat index in Fahrenheit (the default)
|
||||
//float hif = dht.computeHeatIndex(f, h);
|
||||
|
||||
//float hic = dht.computeHeatIndex(t, h, false); // Compute heat index in Celsius (isFahreheit = false)
|
||||
|
||||
// NOTE: this isnan() check runs AFTER the assignments above have already decided (via the range
|
||||
// checks) whether to update temperature[]/humid[] - by this point the display has already been
|
||||
// re-enabled too. So this block is purely diagnostic logging on the raw (pre-round1) values, it does
|
||||
// not itself prevent a bad reading from being used (that already happened, or didn't, above).
|
||||
if (isnan(humidTMP) || isnan(tempTMP)) { // Check if any reads failed and exit early (to try again).
|
||||
DPRINTLN("DHT sensor reading error!");
|
||||
//temperature[DHTtempPtr] = 99.9f;
|
||||
//humid[DHThumidPtr] = 0;
|
||||
}
|
||||
else {
|
||||
DPRINT("DHTxx Temperature:"); DPRINT(temperature[DHTtempPtr]); DPRINT("C Humidity:"); DPRINT(humid[DHThumidPtr]); DPRINTLN(" %");
|
||||
}
|
||||
}
|
||||
|
||||
#else
|
||||
void setupDHTemp() {}
|
||||
void getDHTemp() {}
|
||||
#endif
|
||||
File diff suppressed because it is too large.
Load diff
@@ -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 = "<br>HV5122 pin settings:<br>";
|
||||
for (int i=0;i<maxDigits+1;i++) {
|
||||
driverSetupStr += String("{");
|
||||
for (int j=0;j<10;j++) {
|
||||
driverSetupStr += String(digitPins[i][j]);
|
||||
driverSetupStr += (j==9) ? "}," : ",";
|
||||
}
|
||||
driverSetupStr += String("<br>");
|
||||
}
|
||||
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<<cnt; bitBuffer1 = 1<<cnt; cnt++; if (cnt>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
|
||||
@@ -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 = "<br>HV5122 pin settings:<br>";
|
||||
for (int i=0;i<maxDigits+1;i++) {
|
||||
driverSetupStr += String("{");
|
||||
for (int j=0;j<10;j++) {
|
||||
driverSetupStr += String(digitPins[i][j]);
|
||||
driverSetupStr += (j==9) ? "}," : ",";
|
||||
}
|
||||
driverSetupStr += String("<br>");
|
||||
}
|
||||
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
|
||||
@@ -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 <Wire.h>
|
||||
#include <SPI.h>
|
||||
|
||||
#ifdef USE_BME280
|
||||
#include <Adafruit_BME280.h>
|
||||
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 <Adafruit_BMP280.h>
|
||||
// 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 <Adafruit_AHTX0.h> //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 <SHT21.h> //I2C address: 0x80 and 0x81
|
||||
SHT21 sht;
|
||||
float SHT21temp,SHT21humid =0;
|
||||
byte SHT21tempPtr;
|
||||
byte SHT21humidPtr;
|
||||
#endif
|
||||
|
||||
#ifdef USE_BH1750
|
||||
#include <BH1750.h>
|
||||
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 <Sensor>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 <SHT21.h> 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
|
||||
@@ -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;i<d;i++)
|
||||
asm volatile ("nop");
|
||||
}
|
||||
|
||||
|
||||
void setup_pins() {
|
||||
|
||||
#if defined(ESP8266)
|
||||
#else
|
||||
#error "Board is not supported! For ESP32 use MAX6921_ESP32 !"
|
||||
#endif
|
||||
|
||||
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;j<sizeof(segmentEnablePins);j++) {
|
||||
driverSetupStr += String(segmentEnablePins[j]) + ",";
|
||||
}
|
||||
driverSetupStr += "<br>MAX6921 digitEnablePins:";
|
||||
for (int j=0;j<sizeof(digitEnablePins);j++) {
|
||||
driverSetupStr += String(digitEnablePins[j]) + ",";
|
||||
}
|
||||
driverSetupStr += String("<br>");
|
||||
|
||||
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;i<MAXSEGMENTS;i++) {
|
||||
segmentEnableBits[i] = uint32_t(1<<segmentEnablePins[i]); //one-hot bit for each of the 8 segment output pins (a..g,DP)
|
||||
//DPRINT(i); DPRINT(": "); DPRINTLN(segmentEnableBits[i],BIN);
|
||||
}
|
||||
// Precompute cumulative "wipe" animation masks (animationMode 6 / the "up->down->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<<segmentEnablePins[0]); //a
|
||||
animationMaskBits[1] = animationMaskBits[0] | uint32_t(1<<segmentEnablePins[1]) | uint32_t(1<<segmentEnablePins[5]); //bf
|
||||
animationMaskBits[2] = animationMaskBits[1] | uint32_t(1<<segmentEnablePins[6]); //g
|
||||
animationMaskBits[3] = animationMaskBits[2] | uint32_t(1<<segmentEnablePins[4]) | uint32_t(1<<segmentEnablePins[2]); //ec
|
||||
animationMaskBits[4] = animationMaskBits[3] | uint32_t(1<<segmentEnablePins[3]); //d
|
||||
for (int i=0;i<5;i++) {
|
||||
animationMaskBits[i] = ~animationMaskBits[i]; //invert bits -- stored inverted so writeDisplay() can apply them with a plain AND (val &= mask) to blank out the not-yet-revealed segments
|
||||
//DPRINTLN(animationMaskBits[i],HEX);
|
||||
}
|
||||
DPRINTLN("--- Generating digit pins bitmap ---");
|
||||
for (int i=0;i<maxDigits;i++) {
|
||||
digitEnableBits[i] = uint32_t(1 << digitEnablePins[i]); //one-hot bit for each tube's grid/anode-enable output
|
||||
//DPRINT(i); DPRINT(": "); DPRINTLN( digitEnableBits[i],BIN);
|
||||
}
|
||||
|
||||
DPRINTLN("---- Generated Character / Pins table -----");
|
||||
for (int i=0;i<MAXCHARS;i++) {
|
||||
out = 0;
|
||||
//DPRINT(i); DPRINT(": ");
|
||||
//DPRINT(charDefinition[i],BIN); //DPRINT(" = ");
|
||||
for (int j=0;j<=7;j++) //from a to g (j=0..6 are segments a..g, j=7 is DP; bit(7-j) walks charDefinition MSB->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
|
||||
@@ -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;j<sizeof(segmentEnablePins);j++) {
|
||||
driverSetupStr += String(segmentEnablePins[j]) + ",";
|
||||
}
|
||||
driverSetupStr += "<br>MAX6921 digitEnablePins:";
|
||||
for (int j=0;j<sizeof(digitEnablePins);j++) {
|
||||
driverSetupStr += String(digitEnablePins[j]) + ",";
|
||||
}
|
||||
driverSetupStr += String("<br>");
|
||||
|
||||
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;i<MAXSEGMENTS;i++) {
|
||||
segmentEnableBits[i] = uint32_t(1<<segmentEnablePins[i]); //one-hot bit for each of the 8 segment output pins (a..g,DP)
|
||||
//DPRINT(i); DPRINT(": "); DPRINTLN(segmentEnableBits[i],BIN);
|
||||
}
|
||||
// Precompute cumulative "wipe" animation masks (animationMode 6 / the "up->down->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<<segmentEnablePins[0]); //a
|
||||
animationMaskBits[1] = animationMaskBits[0] | uint32_t(1<<segmentEnablePins[1]) | uint32_t(1<<segmentEnablePins[5]); //bf
|
||||
animationMaskBits[2] = animationMaskBits[1] | uint32_t(1<<segmentEnablePins[6]); //g
|
||||
animationMaskBits[3] = animationMaskBits[2] | uint32_t(1<<segmentEnablePins[4]) | uint32_t(1<<segmentEnablePins[2]); //ec
|
||||
animationMaskBits[4] = animationMaskBits[3] | uint32_t(1<<segmentEnablePins[3]); //d
|
||||
for (int i=0;i<5;i++) {
|
||||
animationMaskBits[i] = ~animationMaskBits[i]; //invert bits -- stored inverted so writeDisplay() can apply them with a plain AND (val &= mask) to blank out the not-yet-revealed segments
|
||||
//DPRINTLN(animationMaskBits[i],HEX);
|
||||
}
|
||||
DPRINTLN("--- Generating digit pins bitmap ---");
|
||||
for (int i=0;i<maxDigits;i++) {
|
||||
digitEnableBits[i] = uint32_t(1 << digitEnablePins[i]); //one-hot bit for each tube's grid/anode-enable output
|
||||
//DPRINT(i); DPRINT(": "); DPRINTLN( digitEnableBits[i],BIN);
|
||||
}
|
||||
|
||||
DPRINTLN("---- Generated Character / Pins table -----");
|
||||
for (int i=0;i<MAXCHARS;i++) {
|
||||
out = 0;
|
||||
//DPRINT(i); DPRINT(": ");
|
||||
//DPRINT(charDefinition[i],BIN); //DPRINT(" = ");
|
||||
for (int j=0;j<=7;j++) //from a to g (j=0..6 are segments a..g, j=7 is DP; bit(7-j) walks charDefinition MSB->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
|
||||
@@ -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<maxDigits;i++) {
|
||||
bitBuffer = 0;
|
||||
for (int j=0;j<=7;j++) //from a to g
|
||||
if ((charDefinition[digit[tubes[i]]] & 1<<(7-j)) != 0) {
|
||||
bitBuffer |= 1<<segmentEnablePins[j];
|
||||
}
|
||||
if (digitDP[tubes[i]]) bitBuffer |= 1<<segmentEnablePins[7]; //Decimal Point
|
||||
sendBits(i+1, bitBuffer); // MAX7219 digit registers are addressed 1..8 (DIG0..DIG7), hence i+1
|
||||
} //end for i
|
||||
}
|
||||
|
||||
void clearTubes() {} // no-op: since there's no refresh ISR, "clearing" would require an explicit
|
||||
// blank frame write, which this driver leaves to the next writeDisplaySingle()
|
||||
// call (or REG_SHUTDOWN could be used, but isn't here)
|
||||
#endif
|
||||
@@ -0,0 +1,278 @@
|
||||
/*==============================================================================
|
||||
MM5450.ino - driver for the MM5450 (National Semi / clone) LED segment driver IC.
|
||||
|
||||
Hardware: MM5450 is a 34-output constant-current LED sink driver with a
|
||||
simple synchronous serial input (start bit + 34 data bits, latched with a
|
||||
LE/latch-enable pulse). It has NO on-chip digit multiplexing and NO
|
||||
brightness/intensity register - unlike MAX7219. In this board design a
|
||||
single MM5450 is time-shared between TWO groups of 4x 7-segment+DP digits
|
||||
(32 segment lines + 2 "side enable" lines = 34 outputs), and brightness is
|
||||
produced entirely in software via PWM (see writeDisplay()/PWMrefresh below),
|
||||
driven from the periodic hardware-timer ISR started by startTimer().
|
||||
|
||||
Wiring/protocol: DATA/CLK/LE are bit-banged (not hardware SPI) directly via
|
||||
ESP8266 GPIO-set/clear peripheral registers for speed, because writeBits()
|
||||
runs inside the timer ISR. Bits are shifted out in ascending bitBuffer[]
|
||||
index order (index 0 first), each element corresponding 1:1 to one MM5450
|
||||
output (index = output pin number as defined by the MM5450 datasheet,
|
||||
1..34), with bitBuffer[0] used as the fixed "start bit" required by the
|
||||
chip's serial protocol to frame the following 34 data bits.
|
||||
|
||||
Used by (see clocks.h): CLOCK_11 - "8266 LED Clock with MM5450 chip"
|
||||
(clocks.h defines MM5450 only inside the #ifdef CLOCK_11 block).
|
||||
==============================================================================*/
|
||||
#ifdef MM5450
|
||||
char tubeDriver[] = "MM5450";
|
||||
int maxDigits = 6;
|
||||
|
||||
//Fill this table with the pin numbers of the MM5450 chip!
|
||||
// Maps [digit-group 0..3][segment a..g,DP] -> 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
|
||||
@@ -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;i<maxDig;i++) {
|
||||
pinMode(digitEnablePins[i], OUTPUT);
|
||||
sprintf(tmp,"digitEnablePins[%d]",i);
|
||||
regPin(digitEnablePins[i],tmp);
|
||||
}
|
||||
for (int i=0;i<4;i++) {
|
||||
// ABCDPins is always 4 wide (one GPIO per BCD bit, A=LSB..D=MSB) regardless
|
||||
// of how many tube positions maxDig has - it is the shared bus, not per-digit.
|
||||
pinMode(ABCDPins[i], OUTPUT);
|
||||
sprintf(tmp,"Pin[%c]",char('A'+i));
|
||||
regPin(ABCDPins[i],tmp);
|
||||
}
|
||||
startTimer(); //arms the hardware timer ISR; writeDisplay() re-arms itself with a new period on every call (see below)
|
||||
}
|
||||
|
||||
#if defined(ESP32)
|
||||
// Hardware-timer ISR. IMPORTANT: unlike most other drivers in this project,
|
||||
// this ISR does NOT scan/multiplex individual tube positions and does NOT
|
||||
// touch digit[]/ABCDPins/digitEnablePins at all - the 4511 latches hold their
|
||||
// last-written BCD value continuously (see writeDisplaySingle()), so all
|
||||
// digits stay lit simultaneously with no scanning artifacts. Instead this ISR
|
||||
// implements a slow square-wave "mark-space" PWM purely on the shared
|
||||
// LTBIpin (panel blanking) to control overall brightness, and re-arms its own
|
||||
// timer with a different period every call - i.e. it is a self-rescheduling
|
||||
// software PWM, not a fixed-frequency multiplex tick.
|
||||
void IRAM_ATTR writeDisplay() { //void IRAM_ATTR writeDisplay(){
|
||||
static DRAM_ATTR volatile int timer = PWMrefresh;
|
||||
static DRAM_ATTR volatile int dpCounter = 0; //decimal point stepper -- NOTE: currently unused; this driver has no per-position decimal-point/dot output (no DP_PIN wiring), so digitDP[] is never consulted here.
|
||||
static DRAM_ATTR boolean state=true;
|
||||
int PWMtimeBrightness;
|
||||
int brightness;
|
||||
int num; // POSSIBLE BUG (minor): declared but never used in this function - likely leftover from an earlier version, or copy-paste from writeDisplaySingle(). Harmless, just dead.
|
||||
|
||||
if (EEPROMsaving) { //stop refresh, while EEPROM write is in progress!
|
||||
return;
|
||||
}
|
||||
|
||||
portENTER_CRITICAL_ISR(&timerMux);
|
||||
noInterrupts();
|
||||
intCounter++;
|
||||
brightness = displayON ? prm.dayBright : prm.nightBright;
|
||||
if (brightness>MAXBRIGHTNESS) 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<maxDig; pos++) {
|
||||
num = digit[pos];
|
||||
// POSSIBLE BUG: strobe order looks inverted for CD4511/74HC4511 latch
|
||||
// semantics. On that chip LE (Latch Enable) is active HIGH = "transparent"
|
||||
// (Q outputs follow BCD inputs live) and LE LOW = "latched/frozen" (holds
|
||||
// last value regardless of later BCD changes). The expected safe sequence
|
||||
// to update one digit without disturbing others on the shared bus is:
|
||||
// LE HIGH (transparent) -> 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<<j)); } //bit j of num (0=A/LSB .. 3=D/MSB) drives ABCDPins[j]; num is expected to be a BCD digit 0-9 (values 10=BLANK/11=dash/12=DP from digit[] are NOT valid BCD and will decode to whatever the 4511 maps undefined codes to - typically blank/garbage segments)
|
||||
digitalWrite(digitEnablePins[pos],HIGH);
|
||||
} //end for
|
||||
}
|
||||
//__________________ ESP8266 driver __________________________
|
||||
#else
|
||||
// panelVersion reflects the PCB_VERSION #define set per-clock in clocks.h
|
||||
// (1, 2 or 3 for the ESP8266 GP Numitron boards). NOTE: on the current live
|
||||
// code path below, panelVersion is computed but never actually read - it is
|
||||
// only consulted inside the commented-out legacy implementation further
|
||||
// down, so today PCB_VERSION has no effect on ESP8266 runtime behavior
|
||||
// (brightness/blanking is handled uniformly for all panel versions via the
|
||||
// PWM+LTBIpin scheme, same as the ESP32 branch above).
|
||||
#ifdef PCB_VERSION
|
||||
byte panelVersion = PCB_VERSION;
|
||||
#else
|
||||
byte panelVersion = 1;
|
||||
#endif
|
||||
|
||||
void ICACHE_RAM_ATTR writeDisplay() { //https://circuits4you.com/2018/01/02/esp8266-timer-ticker-example/
|
||||
static int timer = PWMrefresh;
|
||||
static int brightness;
|
||||
static int PWMtimeBrightness = PWM_min;
|
||||
static boolean state = true;
|
||||
int num; // POSSIBLE BUG (minor): declared but only used inside the commented-out block below; dead in the active code path.
|
||||
|
||||
if (EEPROMsaving) { //stop refresh, while EEPROM write is in progress!
|
||||
timer1_write(PWMrefresh);
|
||||
return;
|
||||
}
|
||||
|
||||
intCounter++;
|
||||
// --- Legacy ESP8266 implementation (kept for reference, NOT compiled) ---
|
||||
// This older approach did per-tick brightness stepping by re-writing every
|
||||
// digit's BCD value each ISR call and blanking individual digits (num=10)
|
||||
// once a rolling brightCounter exceeded the configured brightness - i.e. a
|
||||
// true duty-cycle PWM done by rewriting the tube data itself, gated by
|
||||
// panelVersion>=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)<brightCounter)) || (!radarON)) {
|
||||
digitalWrite(LTBIpin,LOW); //Blank display
|
||||
}
|
||||
else {
|
||||
digitalWrite(LTBIpin,HIGH); //enable display
|
||||
for (int pos=0; pos<=maxDig; pos++) {
|
||||
num = digit[pos];
|
||||
if ((displayON ? prm.dayBright : prm.nightBright)<brightCounter) num = 10; //clear digit
|
||||
digitalWrite(digitEnablePins[pos],LOW); //latch enable
|
||||
for (int j=0;j<4;j++) {digitalWrite(ABCDPins[j],num & (1<<j)); }
|
||||
digitalWrite(digitEnablePins[pos],HIGH);
|
||||
} //end for
|
||||
} //end else
|
||||
brightCounter++; if (brightCounter>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; pos<maxDig; pos++) {
|
||||
num = digit[pos];
|
||||
// POSSIBLE BUG: same LE strobe-order concern as the ESP32 twin above
|
||||
// (LOW->set 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<<j)); }
|
||||
digitalWrite(digitEnablePins[pos],HIGH);
|
||||
} //end for
|
||||
}
|
||||
#endif //esp8266 end
|
||||
|
||||
void clearTubes() {
|
||||
// Body is entirely commented out: currently a no-op. Comment says "not
|
||||
// necessary to use", presumably because writeDisplay()'s LTBIpin blanking
|
||||
// (brightness==0 case) already visually blanks the panel without needing to
|
||||
// rewrite every digit's latch to a blank BCD code. Kept here only as
|
||||
// reference for how a real digit-level clear would be done (set all ABCD
|
||||
// lines HIGH == BCD 15, an invalid/blank code on the 4511) if LTBIpin ever
|
||||
// becomes unavailable/unsuitable for a given panel revision.
|
||||
/* //not necessary to use
|
||||
if (panelVersion==3) {
|
||||
digitalWrite(LTBIpin,LOW);
|
||||
}
|
||||
else {
|
||||
for (int pos=0; pos<=maxDigits; pos++) {
|
||||
digitalWrite(digitEnablePins[pos],LOW);
|
||||
for (int i=0;i<4;i++) digitalWrite(ABCDPins[i],HIGH);
|
||||
digitalWrite(digitEnablePins[pos],HIGH);
|
||||
}
|
||||
}
|
||||
*/
|
||||
}
|
||||
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,361 @@
|
||||
#ifdef PT6311
|
||||
//===========================================================================================
|
||||
// Driver for the Princeton Technology PT6311 VFD controller/driver IC.
|
||||
//
|
||||
// Hardware / protocol:
|
||||
// The PT6311 is a "smart" VFD grid/segment controller: unlike a dumb shift-register driver
|
||||
// (e.g. MAX6921) it contains its own display RAM and internal multiplex/scan generator, so
|
||||
// the MCU only has to push a full frame into it after something changes -- the chip itself
|
||||
// keeps refreshing the tubes autonomously in the background. That is why writeDisplay()
|
||||
// (the timer-ISR hook used by "dumb" drivers) is an empty stub here and setup_pins() never
|
||||
// calls startTimer(): there is no ISR-driven multiplex loop for this driver, all display
|
||||
// updates go through the blocking writeDisplaySingle().
|
||||
// 3-wire serial bus (similar to SPI, chip-specific framing):
|
||||
// PIN_LOAD (STB/CS) - LOW while a command/data byte is being shifted in, HIGH between
|
||||
// bytes to latch/strobe it. Pulled LOW again for every single byte,
|
||||
// i.e. each byte shifted is individually framed by its own STB pulse
|
||||
// (see shift()/sendCommand()/writeDisplaySingle()).
|
||||
// PIN_CLK - one pulse per bit.
|
||||
// PIN_DIN - serial data, LSB-first per byte (see shift(): loop starts at
|
||||
// bit 0), which is the opposite bit order of the MAX6921 driver.
|
||||
// Command bytes (C1..C4) select a mode: C1 sets 8-digit/20-segment display mode, C2 selects
|
||||
// "write display data, auto-increment address", C3 sets the RAM write address, C4 sets
|
||||
// brightness/on-off. Display RAM is addressed per digit in steps of 3 (see
|
||||
// C3_ADDRESS_SETTING + i*3 in writeDisplaySingle()) because the chip reserves 3 bytes per
|
||||
// digit position in its internal RAM regardless of how many of those bits are actually
|
||||
// wired to segments on this PCB.
|
||||
//
|
||||
// Used by (see clocks.h, search for "#define PT6311"):
|
||||
// CLOCK_17 - ESP8266 + PT6311, 4x IV-22 tubes, "russian clock"
|
||||
//===========================================================================================
|
||||
char tubeDriver[] = "PT6311";
|
||||
int maxDigits = sizeof(digitEnablePins); //digitEnablePins is a byte[] (1 byte/element), so sizeof() == element count here
|
||||
int wt = 1; //Serial timing
|
||||
|
||||
//#define PIN_CLK 14 // Clock
|
||||
//#define PIN_DIN 13 // DataIN
|
||||
//#define PIN_LOAD 15 // STB pin
|
||||
|
||||
#define SEGMENT8 //if not used, 17 segments or more
|
||||
//#define TESTMODE //for testing the segments of a new type VFD module
|
||||
|
||||
//PT6311 Commands
|
||||
|
||||
// Display-control command per brightness step 0(OFF)..10. Bits 3-1 of the low nibble select
|
||||
// one of the chip's 8 pulse-width/duty codes (000..111); with 10 non-zero UI brightness
|
||||
// levels mapped onto only 8 hardware codes, two steps had to collapse onto the same code.
|
||||
// POSSIBLE BUG: index 5 and 6 (below) both encode 0B10001100, and index 8 and 9 both encode
|
||||
// 0B10001110 -- i.e. brightness levels 5/6 and 8/9 are indistinguishable on the hardware.
|
||||
// This may be intentional (skipping a step to line the mapping up some other way) or a
|
||||
// copy-paste slip when the table was typed out; worth confirming against the datasheet.
|
||||
byte DISPLAY_MODE[11] = {
|
||||
0B00000000, //OFF
|
||||
0B10001000, //1
|
||||
0B10001001, //2
|
||||
0B10001010, //3
|
||||
0B10001011, //4
|
||||
0B10001100, //5
|
||||
0B10001100, //6 // POSSIBLE BUG: identical to level 5 above -- see note above the array
|
||||
0B10001101, //7
|
||||
0B10001110, //8
|
||||
0B10001110, //9 // POSSIBLE BUG: identical to level 8 above -- see note above the array
|
||||
0B10001111, //10
|
||||
};
|
||||
|
||||
#define C1_DISPLAY_MODE_SETTING B00000000 //8 digit, 20 segments
|
||||
#define C2_DATA_SETTING B01000000 //write data to display, addr increment
|
||||
#define C2_DATA_SETTING_TESTMODE B01001000 //write data to display, addr increment
|
||||
#define C3_ADDRESS_SETTING B11000000 //Address setting to 0
|
||||
#define C4_DISPLAY_CONTROL B10001111 //max brightness, ON (kept for reference; actual brightness command is looked up from DISPLAY_MODE[] instead)
|
||||
|
||||
#ifdef SEGMENT8
|
||||
//------------------abcdefgDP---------------- definition of different characters
|
||||
// 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) -- this indexing matches the digit[]/newDigit[] encoding
|
||||
// used by the core firmware (see ESP32_UniClock2.ino), so digit[] values map 1:1 into this
|
||||
// table without any translation layer (unlike the ASCII/16-segment branch below).
|
||||
#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 C1_DISPLAY_MODE_SETTING B00000000 //8 digit, 20 segments
|
||||
#define MAXSEGMENTS 17
|
||||
boolean ASCIImode = true;
|
||||
// Full 16-segment-star + DP font, indexed by ASCII code - 32 (i.e. index 0 = space/0x20).
|
||||
// Since digit[]/newDigit[] hold the compact 0-9/BLANK/minus/DP/glyph codes (not ASCII), the
|
||||
// ASCIImode path in writeDisplaySingle() first maps digit[i] through asciiConvert[] to get an
|
||||
// actual ASCII character, then subtracts 32 to index into this table.
|
||||
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) 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<<i
|
||||
// from the low end) -- the PT6311 expects the least-significant bit of each command/data byte
|
||||
// first, which is the reverse of many other shift-register based drivers in this project.
|
||||
void inline shift(byte Data) {
|
||||
for (int i=0;i<8;i++) {
|
||||
digitalWrite(PIN_CLK,LOW);
|
||||
delayMicroseconds(2);
|
||||
digitalWrite(PIN_DIN, Data & (1<<i));
|
||||
delayMicroseconds(5);
|
||||
digitalWrite(PIN_CLK,HIGH);
|
||||
delayMicroseconds(5);
|
||||
digitalWrite(PIN_CLK,LOW);
|
||||
delayMicroseconds(2);
|
||||
}
|
||||
}
|
||||
|
||||
// Sends a single command/address byte framed by its own STB (PIN_LOAD) pulse: LOW while
|
||||
// shifting, HIGH afterwards to latch it into the chip. Used for the C1/C3 setup commands;
|
||||
// writeDisplaySingle() below repeats this same LOW/shift/HIGH framing manually per-byte
|
||||
// instead of reusing this helper, since it needs to keep PIN_LOAD low across the multi-byte
|
||||
// per-digit data burst.
|
||||
void ICACHE_RAM_ATTR sendCommand(byte val){ //for commands C1..C3
|
||||
digitalWrite(PIN_LOAD,LOW);
|
||||
delayMicroseconds(2);
|
||||
shift(val);
|
||||
digitalWrite(PIN_LOAD,HIGH);
|
||||
delayMicroseconds(2);
|
||||
|
||||
//DPRINT(address,HEX); DPRINT(","); DPRINTLN(val,HEX);
|
||||
}
|
||||
|
||||
void setup_pins() {
|
||||
DPRINTLN("PT6311 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");
|
||||
digitalWrite(PIN_CLK, HIGH);
|
||||
digitalWrite(PIN_LOAD, HIGH);
|
||||
// Hardcoded to match the SEGMENT8 charDefinition table currently active above (only digits
|
||||
// + '%' are used by the core when digitsOnly==true, see ESP32_UniClock2.ino). If SEGMENT8
|
||||
// is ever disabled to switch this driver to the 16-segment/ASCIImode table, this should
|
||||
// probably become "digitsOnly = !ASCIImode;" instead of staying hardcoded true.
|
||||
digitsOnly = true;
|
||||
delay(10);
|
||||
sendCommand(C1_DISPLAY_MODE_SETTING);
|
||||
}
|
||||
|
||||
#if defined(ESP32)
|
||||
void IRAM_ATTR writeDisplaySingle(){
|
||||
#else
|
||||
void ICACHE_RAM_ATTR writeDisplaySingle(){
|
||||
#endif
|
||||
byte newBright = 0;
|
||||
uint32_t bitBuffer;
|
||||
int dispChar;
|
||||
static int count = 15;
|
||||
|
||||
newBright = displayON ? prm.dayBright : prm.nightBright;
|
||||
if (newBright>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<maxDigits;i++) {
|
||||
dispChar = digit[i];
|
||||
if (ASCIImode) {
|
||||
// FIXED: previously "dispChar -= 32" ran unconditionally, even when the bounds check just
|
||||
// below skipped the asciiConvert[] lookup (dispChar >= 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<sizeof(asciiConvert)-1) { //-1 excludes the string's NUL terminator (see asciiConvert[] above)
|
||||
dispChar = asciiConvert[dispChar] - 32;
|
||||
} else {
|
||||
dispChar = 0; //unmapped/out-of-range code -> 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<MAXSEGMENTS;j++)
|
||||
if ((charDefinition[dispChar] & (uint32_t)1<<(MAXSEGMENTS-1-j)) != 0) {
|
||||
bitBuffer |= (uint32_t)1<<segmentEnablePins[j];
|
||||
}
|
||||
if (digitDP[i]) bitBuffer |= (uint32_t)1<<segmentEnablePins[7]; //Decimal Point
|
||||
//DPRINT(charDefinition[dispChar],BIN); DPRINT(" / "); DPRINTLN(bitBuffer,BIN);
|
||||
if (newBright==0) bitBuffer = 0; //switch OFF display
|
||||
// Always shifts 2 (or 3) bytes per digit regardless of MAXSEGMENTS, even in 8-segment
|
||||
// mode where only the low byte carries real segment bits -- the chip expects a fixed
|
||||
// per-digit byte count matching the RAM stride set above (i*3), so the extra byte(s) of
|
||||
// padding are required protocol filler, not wasted work.
|
||||
shift(bitBuffer & 0xFF);
|
||||
shift(bitBuffer >>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
|
||||
@@ -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<<i from the
|
||||
// low end) -- same bit order as the PT6311 driver, opposite of e.g. the MAX6921 driver.
|
||||
// SIN/CLK are passed as parameters (not read from #define'd globals like PT6311's shift())
|
||||
// even though only one SIN/CLK pair is ever used in this file; harmless but slightly redundant.
|
||||
void inline shift(byte SIN, byte CLK, byte Data) {
|
||||
for (int i=0;i<8;i++) {
|
||||
digitalWrite(CLK,LOW);
|
||||
delayMicroseconds(wt);
|
||||
digitalWrite(SIN, Data & (1<<i));
|
||||
delayMicroseconds(wt);
|
||||
digitalWrite(CLK,HIGH);
|
||||
delayMicroseconds(wt);
|
||||
}
|
||||
delayMicroseconds(wt);
|
||||
}
|
||||
|
||||
void ICACHE_RAM_ATTR sendCommand(byte val){ //for commands C1..C3
|
||||
startBits(PIN_CS);
|
||||
shift(PIN_SIN, PIN_CLK, val);
|
||||
stopBits(PIN_CS);
|
||||
|
||||
//DPRINT(address,HEX); DPRINT(","); DPRINTLN(val,HEX);
|
||||
}
|
||||
|
||||
void setup_pins() {
|
||||
DPRINTLN("PT6355 Clock - Setup pins...");
|
||||
pinMode(PIN_CS,OUTPUT); regPin(PIN_CS,"PIN_CS");
|
||||
pinMode(PIN_SIN, OUTPUT); regPin(PIN_SIN,"PIN_SIN");
|
||||
pinMode(PIN_CLK, OUTPUT); regPin(PIN_CLK,"PIN_CLK");
|
||||
digitalWrite(PIN_CS, HIGH);
|
||||
digitalWrite(PIN_CLK, HIGH);
|
||||
// Hardcoded false (i.e. "full ASCII glyph set usable") to match the 16-segment/ASCIImode
|
||||
// table currently active above (SEGMENT8 is commented out at the top of this file). As with
|
||||
// the analogous line in PT6311.ino, if SEGMENT8 is ever enabled for this driver this value
|
||||
// is NOT automatically updated to match -- it would need to become "digitsOnly = false;" or
|
||||
// be reconsidered by hand depending on what glyphs the target clock model actually needs.
|
||||
digitsOnly = false;
|
||||
delay(1);
|
||||
|
||||
sendCommand(C1_DISPLAY_STATE_SETTING);
|
||||
sendCommand(C2_GRID_SELECTION);
|
||||
#ifdef TESTMODE
|
||||
// POSSIBLE BUG (dead code): this first while(true) never returns, so the for-loop and
|
||||
// the second while(true) below it can never execute. Presumably the intent was to run the
|
||||
// for-loop (fill digit[] with glyph 1) BEFORE looping writeDisplaySingle() forever, i.e.
|
||||
// this first while(true) block looks misplaced/leftover from editing. Only affects the
|
||||
// TESTMODE debug build, not normal operation.
|
||||
while (true) {writeDisplaySingle(); delay(10000);}
|
||||
for (int i=0;i<10;i++) digit[i] = 1; // " char
|
||||
while (true) {writeDisplaySingle(); delay(10000);}
|
||||
#endif
|
||||
|
||||
}
|
||||
|
||||
#if defined(ESP32)
|
||||
void IRAM_ATTR writeDisplaySingle(){
|
||||
#else
|
||||
void ICACHE_RAM_ATTR writeDisplaySingle(){
|
||||
#endif
|
||||
static byte oldBright = 0;
|
||||
static byte newBright = 0;
|
||||
byte dispState;
|
||||
uint32_t bitBuffer;
|
||||
int dispChar;
|
||||
static int count = 15;
|
||||
|
||||
#ifdef TESTMODE
|
||||
DPRINT("count:"); DPRINTLN(count);
|
||||
#endif
|
||||
newBright = displayON ? prm.dayBright : prm.nightBright;
|
||||
if (newBright != oldBright) {
|
||||
// Coarse 3-level duty mapping (not a per-step 0..10 lookup table like PT6311's
|
||||
// DISPLAY_MODE[]): bit0 just turns the display on/off, and bits 1-2 (mask 0b110) are set
|
||||
// as one all-or-nothing group once newBright>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<maxDigits;i++) {
|
||||
dispChar = digit[i];
|
||||
if (ASCIImode) {
|
||||
// FIXED: same issue PT6311.ino had - "dispChar -= 32" used to run unconditionally, even
|
||||
// when the bounds check just below skipped the asciiConvert[] lookup (dispChar >=
|
||||
// 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<sizeof(asciiConvert)-1) { //-1 excludes the string's NUL terminator
|
||||
dispChar = asciiConvert[dispChar] - 32;
|
||||
} else {
|
||||
dispChar = 0; //unmapped/out-of-range code -> blank glyph (charDefinition[0])
|
||||
}
|
||||
}
|
||||
bitBuffer = 0;
|
||||
for (int j=0;j<MAXSEGMENTS;j++)
|
||||
if ((charDefinition[dispChar] & (uint32_t)1<<(MAXSEGMENTS-1-j)) != 0) {
|
||||
bitBuffer |= (uint32_t)1<<segmentEnablePins[j];
|
||||
}
|
||||
// POSSIBLE BUG / disabled feature: decimal-point output is commented out here, unlike the
|
||||
// equivalent line in PT6311.ino ("if (digitDP[i]) bitBuffer |= ...segmentEnablePins[7]").
|
||||
// As written, digitDP[] is completely ignored on this driver -- decimal points/colon dots
|
||||
// set by the core firmware will never actually light up on PT6355-based clocks. This may
|
||||
// be intentional (e.g. this clock's wiring has no DP output driven through segmentEnablePins[7]),
|
||||
// but it is worth confirming since it silently drops a feature other drivers implement.
|
||||
//if (digitDP[i]) bitBuffer |= (uint32_t)1<<segmentEnablePins[7]; //Decimal Point
|
||||
#ifdef TESTMODE
|
||||
bitBuffer = (uint32_t)1<<count; //walk a single lit segment/grid bit through the frame to identify wiring
|
||||
#endif
|
||||
//DPRINT(charDefinition[dispChar],BIN); DPRINT(" / "); DPRINTLN(bitBuffer,BIN);
|
||||
//bitBuffer = 0x03FFFF;
|
||||
shift(PIN_SIN, PIN_CLK, bitBuffer & 0xFF);
|
||||
shift(PIN_SIN, PIN_CLK, bitBuffer >>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
|
||||
@@ -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<<pin) for use with the low-level ESP8266 WRITE_PERI_REG()/
|
||||
//PIN_OUT_SET/PIN_OUT_CLEAR register access in writeDisplay() - much faster than digitalWrite(),
|
||||
//needed because the whole 24-bit frame must be bit-banged out within one multiplex refresh slot.
|
||||
#define PIN_LE_BIT 1<<PIN_LE
|
||||
#define PIN_CLK_BIT 1<<PIN_CLK
|
||||
#define PIN_DATA_BIT 1<<PIN_DATA
|
||||
#define PIN_STROBE_BIT 1<<PIN_STROBE
|
||||
|
||||
|
||||
|
||||
//------------------abcdefgDP---------------- definition of different characters
|
||||
//Each byte is a 7-segment+DP mask, MSB=a ... LSB=DP (bit7=a, bit6=b, ... bit1=g, bit0=DP).
|
||||
//generateBitTable() below expands these into the actual 24-bit shift-register bitmaps
|
||||
//(charTable[]) using segmentEnablePins[] as the a..DP -> 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<DATABITS; i++) {
|
||||
if ((val & (uint32_t)1 << (DATABITS -1 - i)) ) {
|
||||
WRITE_PERI_REG( PIN_OUT_SET, PIN_DATA_BIT );
|
||||
for (int t=0; t<4;t++) asm volatile ("nop");
|
||||
}
|
||||
else {
|
||||
WRITE_PERI_REG( PIN_OUT_CLEAR, PIN_DATA_BIT );
|
||||
for (int t=0; t<8;t++) asm volatile ("nop");
|
||||
}
|
||||
WRITE_PERI_REG( PIN_OUT_SET, PIN_CLK_BIT );
|
||||
for (int t=0; t<8;t++) asm volatile ("nop");
|
||||
WRITE_PERI_REG( PIN_OUT_CLEAR, PIN_CLK_BIT );
|
||||
for (int t=0; t<4;t++) asm volatile ("nop");
|
||||
} //end for
|
||||
|
||||
//Latch: pulse LE high then low to transfer the just-shifted 24 bits from the SN75512's
|
||||
//internal shift register into its output latch - this is what actually updates the VFD
|
||||
//outputs; the outputs stay unchanged (no visible glitching) while bits are still shifting.
|
||||
WRITE_PERI_REG( PIN_OUT_SET, PIN_LE_BIT );
|
||||
for (int t=0; t<4;t++) asm volatile ("nop");
|
||||
WRITE_PERI_REG( PIN_OUT_CLEAR, PIN_LE_BIT );
|
||||
|
||||
pos++; if (pos >= 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<MAXSEGMENTS;i++) {
|
||||
segmentEnableBits[i] = (uint32_t)1<<segmentEnablePins[i];
|
||||
//DPRINT(i); DPRINT(": "); DPRINTLN(segmentEnableBits[i],BIN);
|
||||
}
|
||||
|
||||
DPRINTLN("--- Generating digit pins bitmap ---");
|
||||
for (int i=0;i<maxDigits;i++) {
|
||||
digitEnableBits[i] = (uint32_t)1 << digitEnablePins[i];
|
||||
//DPRINT(i); DPRINT(": "); DPRINTLN( digitEnableBits[i],BIN);
|
||||
}
|
||||
|
||||
DPRINTLN("---- Generated Character / Pins table -----");
|
||||
for (int i=0;i<MAXCHARS;i++) {
|
||||
out = 0;
|
||||
//DPRINT(i); DPRINT(": ");
|
||||
//DPRINT(charDefinition[i],BIN); //DPRINT(" = ");
|
||||
//j runs 0..7 through charDefinition's bits from MSB(a, bit7) to LSB(DP, bit0); since
|
||||
//segmentEnablePins[] is stored in the same a..DP order, j directly indexes it (no reversal
|
||||
//needed here, unlike the SN75518 driver which stores segments in the opposite bit order).
|
||||
for (int j=0;j<=7;j++) //from a to g
|
||||
if ((charDefinition[i] & (uint32_t)1<<(7-j)) != 0) {
|
||||
out = out | segmentEnableBits[j]; //DPRINT("1");
|
||||
}
|
||||
//else DPRINT("0");
|
||||
//DPRINT(" >> ");
|
||||
|
||||
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
|
||||
@@ -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;j<sizeof(segmentEnablePins);j++) {
|
||||
driverSetupStr += String(segmentEnablePins[j]) + ",";
|
||||
}
|
||||
driverSetupStr += "<br>SN75518 digitEnablePins:";
|
||||
for (int j=0;j<sizeof(digitEnablePins);j++) {
|
||||
driverSetupStr += String(digitEnablePins[j]) + ",";
|
||||
}
|
||||
driverSetupStr += String("<br>");
|
||||
|
||||
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<sizeof(asciiConvert)-1) { //-1 excludes the string's NUL terminator (see asciiConvert[] above)
|
||||
dispChar = asciiConvert[dispChar] - 32;
|
||||
} else {
|
||||
dispChar = 0; //unmapped/out-of-range code -> 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<count>) 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<MAXSEGMENTS;i++) {
|
||||
segmentEnableBits[i] = uint32_t(1)<<(segmentEnablePins[i]-1);
|
||||
//DPRINT(i); DPRINT(": "); DPRINTLN(segmentEnableBits[i],BIN);
|
||||
}
|
||||
#ifdef SEGMENT8
|
||||
//Progressive top-to-bottom segment groups for the 7-segment wipe animation: each mask ORs in
|
||||
//one more "row" of segments (a -> +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<<segmentEnablePins[0]); //a
|
||||
animationMaskBits[1] = animationMaskBits[0] | uint32_t(1)<<segmentEnablePins[1] | uint32_t(1<<segmentEnablePins[5]); //bf
|
||||
animationMaskBits[2] = animationMaskBits[1] | uint32_t(1)<<segmentEnablePins[6]; //g
|
||||
animationMaskBits[3] = animationMaskBits[2] | uint32_t(1)<<segmentEnablePins[4] | uint32_t(1<<segmentEnablePins[2]); //ec
|
||||
animationMaskBits[4] = animationMaskBits[3] | uint32_t(1)<<segmentEnablePins[3]; //d
|
||||
#else
|
||||
//Dutsrpnmkhgfedcba
|
||||
//DP=16,u=15,t=14,s=13,r=12,p=11,n=10,m=9,k=8,h=7,g=6,f=5,e=4,d=3,c=2,b=1,a=0}
|
||||
//Same top-to-bottom progressive wipe idea, but for the 16-segment layout (grouped by the
|
||||
//segments' approximate vertical position within the character cell rather than a..g letters).
|
||||
//NOTE: unlike the segmentEnablePins[]/charTable[] bit-order handling elsewhere in this file,
|
||||
//these use segmentEnablePins[N] directly (no "-1") - i.e. these masks are built with 0-based
|
||||
//bit positions offset by one from the rest of the table (a latent inconsistency to be aware
|
||||
//of if segmentEnablePins[] pin numbers are ever renumbered/changed).
|
||||
animationMaskBits[0] = uint32_t(1<<segmentEnablePins[0]) | uint32_t(1<<segmentEnablePins[1]); //ab
|
||||
animationMaskBits[1] = animationMaskBits[0] | uint32_t(1<<segmentEnablePins[2]) | uint32_t(1<<segmentEnablePins[8]) |
|
||||
uint32_t(1<<segmentEnablePins[9]) | uint32_t(1<<segmentEnablePins[10]) | uint32_t(1<<segmentEnablePins[7]); //hkmnc
|
||||
animationMaskBits[2] = animationMaskBits[1] | uint32_t(1<<segmentEnablePins[15]) | uint32_t(1<<segmentEnablePins[11]); //up
|
||||
animationMaskBits[3] = animationMaskBits[2] | uint32_t(1<<segmentEnablePins[6]) | uint32_t(1<<segmentEnablePins[14]) |
|
||||
uint32_t(1<<segmentEnablePins[13]) |uint32_t(1<<segmentEnablePins[12]) |uint32_t(1<<segmentEnablePins[3]); //gtsrd
|
||||
animationMaskBits[4] = animationMaskBits[3] | uint32_t(1<<segmentEnablePins[5]) | uint32_t(1<<segmentEnablePins[4]); //fe
|
||||
#endif
|
||||
DPRINTLN("--- Generating animation mask bitmap:");
|
||||
for (int i=0;i<5;i++) {
|
||||
animationMaskBits[i] = ~animationMaskBits[i]; //invert bits: AND-ing a char's bitmap with this clears (blanks) the "not yet revealed" segments, keeping everything else unaffected
|
||||
//DPRINTLN(animationMaskBits[i],BIN);
|
||||
}
|
||||
DPRINT("--- Generating digit pins bitmap:"); DPRINTLN(maxDigits);
|
||||
for (int i=0;i<maxDigits;i++) {
|
||||
digitEnableBits[i] = uint32_t(1 << (digitEnablePins[i]-1));
|
||||
//DPRINT(i); DPRINT(": "); DPRINTLN( digitEnableBits[i],BIN);
|
||||
}
|
||||
|
||||
DPRINT("--- Generated Character / Pins table:"); DPRINTLN(MAXCHARS);
|
||||
for (int i=0;i<MAXCHARS;i++) {
|
||||
out = 0;
|
||||
//DPRINT(i); DPRINT(": ");
|
||||
//DPRINT(charDefinition[i],BIN); //DPRINT(" = ");
|
||||
//Bit j of charDefinition[i] (0=a in the "Dutsrpnmkhgfedcba" order) maps to
|
||||
//segmentEnableBits[MAXSEGMENTS-1-j] - i.e. REVERSED relative to segmentEnablePins[]'s own
|
||||
//index order, because charDefinition[] here is written MSB-high-letter-first while
|
||||
//segmentEnablePins[] is written a-first; see the file header/table comments above.
|
||||
for (int j=0;j<MAXSEGMENTS;j++) //from a to g
|
||||
if ((uint32_t)(charDefinition[i] & (uint32_t)1<<j) != 0) {
|
||||
out = out | segmentEnableBits[MAXSEGMENTS-j-1]; //DPRINT("1");
|
||||
}
|
||||
//else {DPRINT("0");}
|
||||
//DPRINT(" >> ");
|
||||
|
||||
charTable[i] = out;
|
||||
//DPRINTLN(charTable[i],BIN);
|
||||
} //end for
|
||||
}
|
||||
|
||||
|
||||
void clearTubes() {
|
||||
digitalWrite(PIN_STROBE,HIGH);
|
||||
}
|
||||
|
||||
void writeDisplaySingle() {}
|
||||
#endif
|
||||
@@ -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<<i)); //register shifts bits on upstroke of clock pin
|
||||
digitalWrite(clockPin, 1);
|
||||
digitalWrite(dataPin, 0);
|
||||
}
|
||||
}
|
||||
|
||||
void inline shiftOutReverse(byte myDataOut) {
|
||||
// This shifts 8 bits out LSB first, on the rising edge of the clock, clock idles low
|
||||
// Used for the pixel/column DATA byte only - the opposite bit order from
|
||||
// shiftOutForward() is required here because the data byte travels through the same
|
||||
// shift-register chain but ends up latched onto a different (mirrored) set of output
|
||||
// pins than the select bytes; using the wrong helper here would mirror the glyph.
|
||||
for (int i=0; i<8; i++) {
|
||||
digitalWrite(clockPin, 0);
|
||||
digitalWrite(dataPin, myDataOut & (1<<i)); //register shifts bits on upstroke of clock pin
|
||||
digitalWrite(clockPin, 1);
|
||||
digitalWrite(dataPin, 0);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
#define PWMrefresh 8000l // NOTE: defined but not referenced anywhere in this file - dead macro (leftover from an earlier PWM implementation?)
|
||||
|
||||
// Required interface stub only: the hardware timer ISR that drives every other driver's
|
||||
// multiplex refresh is intentionally left EMPTY for VQC10. The real refresh (writeDisplay2()
|
||||
// below) needs delayMicroseconds() calls per row for PWM brightness dimming, which is far too
|
||||
// slow/blocking to run safely inside a timer ISR context. Instead, writeDisplay2() is called
|
||||
// cooperatively from the main loop() and from Fdelay() in the core file. Consistent with this,
|
||||
// setup_pins() above never calls startTimer(), so this ISR is in fact never even attached for
|
||||
// this driver - it exists purely to satisfy the common driver interface (extern declaration in
|
||||
// the core file, and the VQC10 special-case in changeDigit()'s animation code).
|
||||
void ICACHE_RAM_ATTR writeDisplay() {}
|
||||
|
||||
|
||||
int PWM_min = 0;
|
||||
int PWM_max = 800; // row on-time ceiling in microseconds, at full brightness (see PWMtimeBrightness below)
|
||||
|
||||
// Blocking, cooperative multiplex refresh for the whole 8x5x7 display - one full call
|
||||
// scans every row once. Called repeatedly from loop()/Fdelay() in the core file (NOT from
|
||||
// an ISR - see the note above writeDisplay()). Brightness is implemented as simple PWM: each
|
||||
// row is held "on" for PWMtimeBrightness microseconds out of a PWM_max budget, then forced
|
||||
// off for the remainder, so total per-row time is constant regardless of brightness (keeps
|
||||
// the row scan rate/refresh flicker frequency stable while only the mark/space ratio changes).
|
||||
void writeDisplay2() {
|
||||
static int PWMtimeBrightness;
|
||||
//static uint32_t refresh = 0;
|
||||
//static uint32_t runtim = 0;
|
||||
//static uint32_t laststart = 0;
|
||||
boolean offState;
|
||||
int brightness;
|
||||
int iRow,loopMax;
|
||||
|
||||
|
||||
//refresh = millis()-laststart;
|
||||
//laststart = millis();
|
||||
//runtim = laststart;
|
||||
|
||||
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); // 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<NUMDIGITS-1 this silently read dat[i+1][0] (the next digit's row-0
|
||||
// data, due to row-major layout) instead of a real row-7 pattern, and for the last digit
|
||||
// (i==NUMDIGITS-1) it read out of the whole `dat` array into whatever memory followed it
|
||||
// (oldDat[0][0]). It also drove a nonexistent 8th row-select line via shiftOutForward(~(1<<iRow))
|
||||
// below. Net effect was an extra, garbage 8th "row" scanned on every refresh - flicker/ghosting
|
||||
// risk and a stolen slice of the row-time PWM budget.
|
||||
loopMax = NUMROWS;
|
||||
do {
|
||||
// NOTE: the "iRow == maxDigits" term here can never be true (maxDigits==NUMDIGITS==8, but iRow now
|
||||
// only ever reaches up to NUMROWS-1==6 before the loop exits via `while(iRow<loopMax)`) - it looks
|
||||
// like it was meant to add an extra "sync/blank" row at the end of each sweep, but as written it's
|
||||
// dead weight in the OR. Left as-is (not an out-of-bounds/indexing issue, just an inert condition).
|
||||
offState = ( (brightness == 0) || (!radarON) || (iRow == maxDigits));
|
||||
|
||||
for (int i=0;i<NUMDIGITS;i++) { //fill one line with dots
|
||||
// Two-phase shift+latch per digit: first phase asserts this digit's select line
|
||||
// together with its row data; the second phase re-latches the SAME data but with
|
||||
// ALL digit-select lines forced back off (0xFF = all deselected, active-low select).
|
||||
// This immediately blanks the just-lit digit again before the loop moves on to the
|
||||
// next one, which prevents the still-settling row data from bleeding/ghosting onto
|
||||
// the previously selected digit while the next digit's select+data is being shifted
|
||||
// in (VFD/tube grids can retain enough charge to glow briefly if left selected while
|
||||
// data changes underneath them).
|
||||
shiftOutForward(0xFF); //deselect Z
|
||||
shiftOutForward(~(1<<i)); //select digit
|
||||
shiftOutReverse(offState ? 0 : dat[i][iRow]); // set data line
|
||||
digitalWrite(latchPin, HIGH); //Write shift register to latch
|
||||
digitalWrite(latchPin, LOW);
|
||||
asm volatile ("nop");
|
||||
|
||||
//Finish latching
|
||||
shiftOutForward(0xFF); //deselect Z
|
||||
shiftOutForward(0xFF); //All digit select lines are HIGH
|
||||
shiftOutReverse(offState ? 0 : dat[i][iRow]); //keep data line
|
||||
digitalWrite(latchPin, HIGH); //Write shift register to latch
|
||||
digitalWrite(latchPin, LOW);
|
||||
asm volatile ("nop");
|
||||
}
|
||||
|
||||
// All 8 digits now hold row iRow's column data latched into their driver ICs (but with
|
||||
// digit-select left off from the blanking step above). Now pulse on the single row-select
|
||||
// line for this row - this is what actually makes the row iRow of every digit light up
|
||||
// simultaneously - for a PWM-timed on-period, implementing brightness control.
|
||||
shiftOutForward(~(1<<iRow)); //enable row
|
||||
shiftOutForward(0xFF); //All digit select lines are HIGH
|
||||
shiftOutForward(0); //anything
|
||||
digitalWrite(latchPin, HIGH); //Write shift register to latch
|
||||
digitalWrite(latchPin, LOW);
|
||||
if (!offState)
|
||||
delayMicroseconds(PWMtimeBrightness);
|
||||
else
|
||||
delayMicroseconds(PWM_max-PWMtimeBrightness);
|
||||
|
||||
iRow++;
|
||||
} while(iRow<loopMax);
|
||||
|
||||
//runtim = millis()-runtim;
|
||||
//DPRINT(refresh); DPRINT("/"); DPRINTLN(runtim);
|
||||
yield(); // hand control back to the RTOS/WiFi stack between refresh passes - important since this whole function runs blocking on the main loop, not in an ISR
|
||||
}
|
||||
|
||||
|
||||
// Looks up character c's glyph in the column-major _font[] table (5 bytes per glyph, one
|
||||
// byte per column, each byte's low 7 bits = one dot per row) and converts it into the
|
||||
// row-major representation (row[NUMROWS], one byte per row, one bit per column) that the
|
||||
// row-scanned hardware needs. c is a plain ASCII code (0x00-0xFF), already translated from
|
||||
// the core firmware's digit-buffer code via asciiConvert[] by the caller.
|
||||
void show(byte c){
|
||||
byte loccol[NUMCOLS];
|
||||
for (byte i=0; i < NUMCOLS; i++)
|
||||
loccol[i] = _font[c*NUMCOLS+i];
|
||||
transposePattern(loccol,row);
|
||||
}
|
||||
|
||||
// Transposes a column-major 5x7 glyph (cols[NUMCOLS], one bit per row within each column
|
||||
// byte) into a row-major bitmap (rows[NUMROWS], one bit per column within each row byte).
|
||||
// Each output row byte ends up with its NUMCOLS=5 column bits packed into bits [2..6]
|
||||
// (via the final "<< (7-NUMCOLS)" shift), bit0/bit1/bit7 left as 0 - this alignment must
|
||||
// match how those bits are wired through the 74HC595 chain to the physical column drivers.
|
||||
// When _upsidedown is set, both the row scan direction (cmask shifted right instead of
|
||||
// left, starting from bit6 instead of bit0) and the column read order are reversed, giving
|
||||
// a full 180-degree flip for tubes mounted upside down (see _upsidedown declaration above -
|
||||
// currently unused/dead in this project, always false).
|
||||
void transposePattern(byte cols[], volatile byte rows[]){
|
||||
byte cmask;
|
||||
cmask = (_upsidedown ? 0x40 : 1);
|
||||
for (byte r=0; r < NUMROWS; r++) {
|
||||
rows[r] = 0;
|
||||
for (byte c=0; c < NUMCOLS; c++) {
|
||||
if (cmask&cols[(_upsidedown ? (NUMCOLS-c-1) : c)]) rows[r] |= 1;
|
||||
rows[r] = rows[r] << 1;
|
||||
}
|
||||
if (_upsidedown) cmask = cmask >> 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<<i);
|
||||
DPRINT(b ? '1' : '0');
|
||||
}
|
||||
}
|
||||
|
||||
// Debug helper: dumps the current dat[][] buffer (the exact bitmap about to be scanned out
|
||||
// by writeDisplay2()) to Serial as ASCII art, for bring-up/troubleshooting. Not called in
|
||||
// normal operation (see the commented-out call at the end of writeDisplaySingle() below).
|
||||
void printBitmap() { //print bitmap for testing
|
||||
for (int j=0;j<NUMROWS;j++) {
|
||||
for (int i=0;i<NUMDIGITS;i++) {
|
||||
bPrint(dat[i][j]); Serial.print(" ");
|
||||
}
|
||||
DPRINTLN(" ");
|
||||
}
|
||||
DPRINTLN("__________________________________");
|
||||
}
|
||||
|
||||
|
||||
// No-op: unlike several other drivers, VQC10 has no dedicated "all outputs off" hardware
|
||||
// shortcut here - blanking a digit position is instead achieved through the normal buffer
|
||||
// path by writing digit-buffer code 10 (BLANK) into digit[]/newDigit[], which asciiConvert[]
|
||||
// maps to a space character (empty glyph). The core .ino no longer calls clearTubes() from
|
||||
// anywhere at all (its former caller, disableDisplay(), has been removed entirely as dead code),
|
||||
// so this stub is purely for interface compliance at this point.
|
||||
void clearTubes() {}
|
||||
|
||||
// Blocking, one-shot render: rebuilds oldDat[]/newDat[] (bitmaps for the outgoing and
|
||||
// incoming character of every digit position) from the shared digit[]/newDigit[] buffers,
|
||||
// then blends them into dat[] according to each position's animMask[] wipe-animation
|
||||
// progress. dat[] is what writeDisplay2() actually scans out to the hardware - this
|
||||
// function does not touch the hardware itself.
|
||||
void writeDisplaySingle() {
|
||||
char dispChar;
|
||||
// Physical-to-logical digit position remapping: the 8 cells are wired/driven as two
|
||||
// groups of 4 in reverse order (3,2,1,0 then 7,6,5,4), consistent with two physical
|
||||
// VQC10 modules (see CLOCK_71 "2x VQC10 clock") each internally mirrored/chained back to
|
||||
// front relative to the logical left-to-right digit[]/newDigit[] buffer order.
|
||||
byte tubeShift[] = {3,2,1,0,7,6,5,4};
|
||||
byte a;
|
||||
for (int i=0;i<NUMDIGITS;i++) { //generate new line -- NOTE: comment says "new" but this loop actually builds oldDat[] (the OUTGOING/current character) from digit[]; see the next loop for newDat[]/newDigit[]. Comment/variable naming appears swapped from actual old/new roles, though the logic itself is correct (oldDat<-digit[], newDat<-newDigit[] matches the core file's old/new buffer convention).
|
||||
dispChar = digit[tubeShift[i]];
|
||||
if (dispChar<sizeof(asciiConvert)) dispChar = asciiConvert[(byte)dispChar]; // translate digit-buffer code to ASCII via asciiConvert[]; note dispChar is signed char compared against unsigned sizeof() - harmless here since digit-buffer codes are always small non-negative values, but a latent signed/unsigned mismatch
|
||||
show(dispChar);
|
||||
for (int k=0;k<NUMROWS;k++) {
|
||||
oldDat[i][k] = row[k];
|
||||
}
|
||||
if (digitDP[tubeShift[i]]) {
|
||||
show(DPchar); // overlay the decimal-point/separator dot glyph
|
||||
for (int k=0;k<NUMROWS;k++) {
|
||||
oldDat[i][k] |= row[k]; // OR the DP dot pattern onto the character pattern so both render together in the same cell
|
||||
}
|
||||
}
|
||||
}
|
||||
for (int i=0;i<NUMDIGITS;i++) { //generate old line -- NOTE: comment says "old" but this loop builds newDat[] (the INCOMING/target character) from newDigit[]; see note on the previous loop.
|
||||
dispChar = newDigit[tubeShift[i]];
|
||||
if (dispChar<sizeof(asciiConvert)) dispChar = asciiConvert[(byte)dispChar];
|
||||
show(dispChar);
|
||||
for (int k=0;k<NUMROWS;k++) {
|
||||
newDat[i][k] = row[k];
|
||||
}
|
||||
if (digitDP[tubeShift[i]]) {
|
||||
show(DPchar);
|
||||
for (int k=0;k<NUMROWS;k++) {
|
||||
newDat[i][k] |= row[k];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Vertical wipe/scroll transition: blend oldDat[] and newDat[] into dat[] based on
|
||||
// animMask[] (set by the core file's changeDigit() animation state machine, incrementing
|
||||
// roughly 1..19 over the transition - see the VQC10 special-case in that function). a=0
|
||||
// means no animation in progress yet (show old character unchanged); as a increases
|
||||
// (halved and capped to NUMROWS-1=7 dot-rows) the display effectively scrolls the old
|
||||
// character's rows upward out of view while the new character's rows scroll in from the
|
||||
// bottom, row-by-row, producing a smooth vertical wipe rather than an instant character
|
||||
// swap (this is the driver-specific animation timing referenced by the VQC10 special-case
|
||||
// in the core file's changeDigit()).
|
||||
for (int i=0;i<NUMDIGITS;i++) { //combine animation
|
||||
a = animMask[tubeShift[i]]/2; if (a>7) a=7;
|
||||
for (int k=0;k<NUMROWS;k++) {
|
||||
if (a==0) dat[i][k] = oldDat[i][k];
|
||||
else { //Animation!!!
|
||||
if ((a+k)<NUMROWS) {
|
||||
dat[i][k] = oldDat[i][k+a]; // still within old character's remaining (not yet scrolled out) rows
|
||||
}
|
||||
else {
|
||||
dat[i][k] = newDat[i][(k+a)-NUMROWS]; // wrapped past the bottom: pull in rows of the new character instead
|
||||
}
|
||||
} //endif Animation!!
|
||||
} //end for i
|
||||
} //end for
|
||||
//printBitmap();
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,242 @@
|
||||
// ============================================================================
|
||||
// ZM1500 Clock driver by UNFI
|
||||
// ----------------------------------------------------------------------------
|
||||
// Hardware: 4x 74HC595N serial-in/parallel-out shift registers (32 bits total)
|
||||
// feeding a bank of optocouplers ("opto PCB" wiring), which in turn switch the
|
||||
// actual ZM1500 (12-position) display element grid/segments. Data is bit-banged
|
||||
// out over a plain 3-wire interface (dataPin/clkPin/latchPin - classic 74HC595
|
||||
// SPI-like protocol: shift bits in on clkPin rising edges while dataPin holds
|
||||
// the current bit, then pulse latchPin to move the whole shift-register content
|
||||
// to the (glitch-free) output latches in one go).
|
||||
//
|
||||
// This is the driver currently active in clocks.h (CLOCK_72 defines
|
||||
// ZM1500_ESP32 - see clocks.h). The 32-bit value sent out each refresh packs
|
||||
// the 8 segment-enable bits (from segmentEnablePins[], bits 0-7) and the
|
||||
// 12 possible digit/tube-select bits (from digitEnablePins[], each shifted up
|
||||
// by +8, so bits 8-19) into one word, of which only the low 20 bits are
|
||||
// actually clocked out (see the 20-iteration loop in writeDisplay() below) -
|
||||
// this only supports the single "opto PCB" pin layout (no alternate
|
||||
// "transistor PCB" wiring mode), unlike an earlier work-in-progress revision
|
||||
// of this file that attempted to support both via a SEGMENTS_FIRST toggle.
|
||||
// That revision was abandoned half-finished and has been removed; this is the
|
||||
// last known-working version, restored from a backup (ZM1500 .sav).
|
||||
// ============================================================================
|
||||
|
||||
#ifdef ZM1500_ESP32
|
||||
|
||||
char tubeDriver[] = "ZM1500_ESP32";
|
||||
#if defined(ESP32)
|
||||
#else
|
||||
#error "Only ESP32 Board is supported!"
|
||||
#endif
|
||||
|
||||
// NOTE: despite the "msec" wording in the original comment, these are raw ESP32 hardware-timer
|
||||
// tick counts passed straight into timerAlarmWrite() below, not real milliseconds - the actual
|
||||
// tick period depends on the timer's prescaler (set up once, elsewhere, when the timer object is
|
||||
// created in the core .ino's startTimer()). Treat these as "bigger number = longer phase", not as
|
||||
// an absolute time unit.
|
||||
int DRAM_ATTR PWMrefresh=11000; //msec, Multiplex time period. Greater value => 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<MAXSEGMENTS;i++) {
|
||||
segmentEnableBits[i] = uint32_t(1<<segmentEnablePins[i]);
|
||||
DPRINT(i); DPRINT(": "); DPRINTLN(segmentEnableBits[i],BIN);
|
||||
}
|
||||
animationMaskBits[0] = uint32_t(1<<segmentEnablePins[0]); //a
|
||||
animationMaskBits[1] = animationMaskBits[0] | uint32_t(1<<segmentEnablePins[1]) | uint32_t(1<<segmentEnablePins[5]); //bf
|
||||
animationMaskBits[2] = animationMaskBits[1] | uint32_t(1<<segmentEnablePins[6]); //g
|
||||
animationMaskBits[3] = animationMaskBits[2] | uint32_t(1<<segmentEnablePins[4]) | uint32_t(1<<segmentEnablePins[2]); //ec
|
||||
animationMaskBits[4] = animationMaskBits[3] | uint32_t(1<<segmentEnablePins[3]); //d
|
||||
DPRINTLN("--- Animation mask ---");
|
||||
for (int i=0;i<5;i++) {
|
||||
animationMaskBits[i] = ~animationMaskBits[i]; //invert bits
|
||||
DPRINTLN(animationMaskBits[i],HEX);
|
||||
}
|
||||
|
||||
DPRINTLN("---- Generated Character / Pins table -----");
|
||||
for (int i=0;i<MAXCHARS;i++) {
|
||||
out = 0;
|
||||
DPRINT(i); DPRINT(": ");
|
||||
DPRINT(charDefinition[i],BIN); DPRINT(" = ");
|
||||
for (int j=0;j<=7;j++) //from a to g
|
||||
if ((charDefinition[i] & 1<<(7-j)) != 0) {
|
||||
out = out | segmentEnableBits[j]; //DPRINT("1");
|
||||
}
|
||||
//else DPRINT("0");
|
||||
//DPRINT(" >> ");
|
||||
|
||||
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<maxDigits;i++) {
|
||||
digitEnableBits[i] = uint32_t(1 << uint32_t(digitEnablePins[i]+8));
|
||||
DPRINT(i); DPRINT(": "); DPRINT(digitEnableBits[i],HEX); DPRINTLN(" ");
|
||||
}
|
||||
|
||||
// Debug-only sanity dump: shows what writeDisplay() would send for each tube if it always
|
||||
// displayed its own character-table index (not the real runtime digit[] value) - lets a developer
|
||||
// eyeball the generated bitmaps against the datasheet/wiring before trusting the live display.
|
||||
DPRINTLN("--- DEMO ---");
|
||||
for (int i=0;i<maxDigits;i++) {
|
||||
uint32_t val = uint32_t(charTable[i]); //the full bitmap to send to the shift-register chain
|
||||
val = val | digitEnableBits[i];
|
||||
DPRINT(i); DPRINT(": "); DPRINTLN(val,HEX);
|
||||
}
|
||||
}
|
||||
|
||||
// Empty no-op: nothing in the core .ino calls clearTubes() any more (its only former caller,
|
||||
// disableDisplay(), has been removed entirely as dead code), so this is purely for interface
|
||||
// compliance. If an active blank is ever needed, this would have to bit-bang an all-zero frame
|
||||
// out to the shift-register chain instead of just returning.
|
||||
void IRAM_ATTR clearTubes() {}
|
||||
#endif
|
||||
File diff suppressed because it is too large.
Load diff
@@ -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 <OneWire.h>
|
||||
#include <DallasTemperature.h>
|
||||
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
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 318 B |
@@ -0,0 +1,427 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Uniclock 3.0</title>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<link rel="stylesheet" href="site.css">
|
||||
<link rel="shortcut icon" type="image/x-icon" href="favicon.ico" sizes="16x16">
|
||||
<script src="jquery_360.js"></script>
|
||||
<script src="page.js"></script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="info-popup">
|
||||
<div id="popup-box" class="max-width">
|
||||
<div id="popup-close-btn">X</div>
|
||||
<h2>Random Input</h2>
|
||||
<div class="content">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.</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="menu-holder">
|
||||
<div class="floating-menu">
|
||||
<div class="max-width">
|
||||
<table id="menu">
|
||||
<tr></tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="content max-width">
|
||||
<div class="form">
|
||||
<div page="main">
|
||||
<div class="header">
|
||||
<br>
|
||||
<div class="control-holder">
|
||||
<label for="version">Version</label>
|
||||
<span id="versionHeader" class="info medium" pagemenu="settings"></span>
|
||||
</div>
|
||||
<div class="control-holder">
|
||||
<label>Current Time</label>
|
||||
<span id="currentTime" class="info medium"></span>
|
||||
</div>
|
||||
<div class="control-holder temperature-holder">
|
||||
<label>Temperature</label>
|
||||
<span id="temperature" class="info medium"></span>
|
||||
</div>
|
||||
<div class="control-holder temperature-holder2">
|
||||
<label>Temperature-2</label>
|
||||
<span id="temperature2" class="info medium"></span>
|
||||
</div>
|
||||
<div class="control-holder humidity-holder">
|
||||
<label>Humidity</label>
|
||||
<span id="humidity" class="info medium"></span>
|
||||
</div>
|
||||
<div class="control-holder humidity-holder2">
|
||||
<label>Humidity-2</label>
|
||||
<span id="humidity2" class="info medium"></span>
|
||||
</div>
|
||||
<div class="control-holder pressure-holder">
|
||||
<label>Pressure</label>
|
||||
<span id="pressure" class="info medium"></span>
|
||||
</div>
|
||||
<div class="control-holder lux-holder">
|
||||
<label>Illuminance</label>
|
||||
<span id="lux" class="info medium"></span>
|
||||
</div>
|
||||
<div class="control-holder">
|
||||
<label>WiFi RSSI dB</label>
|
||||
<span id="rssi" class="info medium"></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="control-holder">
|
||||
<label>Alarm</label>
|
||||
<input id="alarmEnable" type="checkbox" class="switch">
|
||||
<span class="switcher-text left non-selectable" for="alarmEnable">off</span>
|
||||
<span class="switcher non-selectable" for="alarmEnable"></span>
|
||||
<span class="switcher-text right non-selectable" for="alarmEnable">on</span>
|
||||
</div>
|
||||
|
||||
<div class="control-holder">
|
||||
<label for="alarmTime">Alarm Time</label>
|
||||
<div class="multiple-input">
|
||||
<select id="alarmTimeHours" class="number-select two-digit" min="0" max="23"></select>
|
||||
<span>:</span>
|
||||
<select id="alarmTimeMinutes" class="number-select two-digit" min="0" max="59" step="1"></select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="control-holder">
|
||||
<label for="alarmPeriod">Alarm End (sec)</label>
|
||||
<input type="range" min="1" max="240" class="slider" id="alarmPeriod">
|
||||
</div>
|
||||
<div class="control-holder radar-holder">
|
||||
<label>Radar</label>
|
||||
<input id="enableRadar" type="checkbox" class="switch">
|
||||
<span class="switcher-text left non-selectable" for="enableRadar">off</span>
|
||||
<span class="switcher non-selectable" for="enableRadar"></span>
|
||||
<span class="switcher-text right non-selectable" for="enableRadar">on</span>
|
||||
</div>
|
||||
<div class="control-holder radar-holder">
|
||||
<label>Radar Timeout (min)</label>
|
||||
<input type="range" min="1" max="60" class="slider" id="radarTimeout">
|
||||
</div>
|
||||
<div class="control-holder lux-holder">
|
||||
<label>AutoDimmer</label>
|
||||
<input id="enableAutoDim" type="checkbox" class="switch">
|
||||
<span class="switcher-text left non-selectable" for="enableAutoDim">off</span>
|
||||
<span class="switcher non-selectable" for="enableAutoDim"></span>
|
||||
<span class="switcher-text right non-selectable" for="enableAutoDim">on</span>
|
||||
</div>
|
||||
</div>
|
||||
<!-- display settings -->
|
||||
<div page="display" class="tube-holder">
|
||||
<br>
|
||||
<div class="control-holder">
|
||||
<label>Manual Override</label>
|
||||
<input id="manualOverride" type="checkbox" class="switch two-side">
|
||||
<span class="switcher-text left non-selectable" for="manualOverride">DAY</span>
|
||||
<span class="switcher non-selectable" for="manualOverride"></span>
|
||||
<span class="switcher-text right non-selectable" for="manualOverride">NIGHT</span>
|
||||
</div>
|
||||
|
||||
<div class="control-holder wordclock-holder">
|
||||
<label>Display Mode</label>
|
||||
<input id="set12_24" type="checkbox" class="switch two-side">
|
||||
<span class="switcher-text left non-selectable" for="set12_24">12h</span>
|
||||
<span class="switcher non-selectable" for="set12_24"></span>
|
||||
<span class="switcher-text right non-selectable" for="set12_24">24h</span>
|
||||
</div>
|
||||
|
||||
<div class="control-holder wordclock-holder">
|
||||
<label>Zero Handling</label>
|
||||
<input id="showZero" type="checkbox" class="switch">
|
||||
<span class="switcher-text left non-selectable" for="showZero">Hide</span>
|
||||
<span class="switcher non-selectable" for="showZero"></span>
|
||||
<span class="switcher-text right non-selectable" for="showZero">Show</span>
|
||||
</div>
|
||||
|
||||
<div class="control-holder wordclock-holder">
|
||||
<label>Blinking Dots</label>
|
||||
<input id="enableBlink" type="checkbox" class="switch">
|
||||
<span class="switcher-text left non-selectable" for="enableBlink">off</span>
|
||||
<span class="switcher non-selectable" for="enableBlink"></span>
|
||||
<span class="switcher-text right non-selectable" for="enableBlink">on</span>
|
||||
</div>
|
||||
<div class="control-holder wordclock-holder">
|
||||
<label>Double Blink</label>
|
||||
<input id="enableDoubleBlink" type="checkbox" class="switch">
|
||||
<span class="switcher-text left non-selectable" for="enableDoubleBlink">off</span>
|
||||
<span class="switcher non-selectable" for="enableDoubleBlink"></span>
|
||||
<span class="switcher-text right non-selectable" for="enableDoubleBlink">on</span>
|
||||
</div>
|
||||
<div class="control-holder">
|
||||
<label>Time Zone</label>
|
||||
<select id="utc_offset" class="number-select" min="-12" max="12" prefix="(UTC " suffix=":00)"></select>
|
||||
</div>
|
||||
|
||||
<div class="control-holder">
|
||||
<label title="Daylight saving time">DST</label>
|
||||
<input id="enableDST" type="checkbox" class="switch">
|
||||
<span class="switcher-text left non-selectable" for="enableDST">off</span>
|
||||
<span class="switcher non-selectable" for="enableDST"></span>
|
||||
<span class="switcher-text right non-selectable" for="enableDST">on</span>
|
||||
</div>
|
||||
|
||||
<div class="control-holder">
|
||||
<label for="animMode">Animation Mode</label>
|
||||
<!-- <select id="animMode" class="number-select" min="0" max="6" step="1" prefix="Animation_" suffix=""></select> -->
|
||||
<select id="animMode">
|
||||
<option value="0">Off</option>
|
||||
<option value="1">1 Count Up</option>
|
||||
<option value="2">2 Count Down</option>
|
||||
<option value="3">3 Shift Right</option>
|
||||
<option value="4">4 Shift Left</option>
|
||||
<option value="5">5 Transition</option>
|
||||
<option value="6">Random 1-5</option>
|
||||
<option value="7">Random 1-4</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="control-holder wordclock-holder">
|
||||
<label for="interval">Cathode Protect</label>
|
||||
<input type="range" min="0" max="60" class="slider" id="interval">
|
||||
</div>
|
||||
|
||||
|
||||
<div class="control-holder">
|
||||
<label>Auto Day/Night</label>
|
||||
<input id="enableAutoShutoff" type="checkbox" class="switch">
|
||||
<span class="switcher-text left non-selectable" for="enableAutoShutoff">off</span>
|
||||
<span class="switcher non-selectable" for="enableAutoShutoff"></span>
|
||||
<span class="switcher-text right non-selectable" for="enableAutoShutoff">on</span>
|
||||
</div>
|
||||
|
||||
<div class="control-holder">
|
||||
<label>Day Brightness</label>
|
||||
<input type="range" min="1" class="slider" id="dayBright">
|
||||
</div>
|
||||
<div class="control-holder">
|
||||
<label>Night Brightness</label>
|
||||
<input type="range" min="0" class="slider" id="nightBright">
|
||||
</div>
|
||||
|
||||
<div class="control-holder">
|
||||
<label>Day</label>
|
||||
<div class="multiple-input">
|
||||
<select id="dayTimeHours" class="number-select two-digit" min="0" max="23"></select>
|
||||
<span>:</span>
|
||||
<select id="dayTimeMinutes" class="number-select two-digit" min="0" max="59" step="5"></select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="control-holder">
|
||||
<label>Night</label>
|
||||
<div class="multiple-input">
|
||||
<select id="nightTimeHours" class="number-select two-digit" min="0" max="23"></select>
|
||||
<span>:</span>
|
||||
<select id="nightTimeMinutes" class="number-select two-digit" min="0" max="59" step="5"></select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="control-holder wordclock-holder">
|
||||
<label>Display Time/Date</label>
|
||||
<input id="enableTimeDisplay" type="checkbox" class="switch">
|
||||
<span class="switcher-text left non-selectable" for="enableTimeDisplay">off</span>
|
||||
<span class="switcher non-selectable" for="enableTimeDisplay"></span>
|
||||
<span class="switcher-text right non-selectable" for="enableTimeDisplay">on</span>
|
||||
</div>
|
||||
<div class="control-holder">
|
||||
<label for="dateMode">Date Format</label>
|
||||
<select id="dateMode">
|
||||
<option value="0">dd/mm/yyyy</option>
|
||||
<option value="1">mm/dd/yyyy</option>
|
||||
<option value="2">yyyy/mm/dd</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="control-holder wordclock-holder">
|
||||
<label>Date Repeat (min)</label>
|
||||
<input type="range" min="0" max="10" class="slider" id="dateRepeatMin">
|
||||
</div>
|
||||
<div class="control-holder temperature-holder">
|
||||
<label>Temp/H. Repeat (min)</label>
|
||||
<input type="range" min="0" max="10" class="slider" id="tempRepeatMin">
|
||||
</div>
|
||||
<div class="control-holder temperature-holder">
|
||||
<label>Temperature Format</label>
|
||||
<input id="tempCF" type="checkbox" class="switch two-side">
|
||||
<span class="switcher-text left non-selectable" for="tempCF">C</span>
|
||||
<span class="switcher non-selectable" for="tempCF"></span>
|
||||
<span class="switcher-text right non-selectable" for="tempCF">F</span>
|
||||
</div>
|
||||
<div class="control-holder wordclock-holder">
|
||||
<label>Date seconds</label>
|
||||
<div class="multiple-input">
|
||||
<select id="dateStart" class="number-select two-digit" min="0" max="59"></select>
|
||||
<span>-</span>
|
||||
<select id="dateEnd" class="number-select two-digit" min="0" max="59" step="1"></select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="control-holder temperature-holder">
|
||||
<label>Temperature seconds</label>
|
||||
<div class="multiple-input">
|
||||
<select id="tempStart" class="number-select two-digit" min="0" max="59"></select>
|
||||
<span>-</span>
|
||||
<select id="tempEnd" class="number-select two-digit" min="0" max="59" step="1"></select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="control-holder humidity-holder">
|
||||
<label>Humidity seconds</label>
|
||||
<div class="multiple-input">
|
||||
<select id="humidStart" class="number-select two-digit" min="0" max="59"></select>
|
||||
<span>-</span>
|
||||
<select id="humidEnd" class="number-select two-digit" min="0" max="59" step="1"></select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- RGB settings -->
|
||||
<div page="rgb" class="tube-holder">
|
||||
<br>
|
||||
<div class="control-holder rgb-holder">
|
||||
<label for="rgbEffect">RGB Effect</label>
|
||||
<select id="rgbEffect">
|
||||
<option value="0">OFF</option>
|
||||
<option value="1">Fixed Color</option>
|
||||
<option value="2">Rainbow Flow</option>
|
||||
<option value="3">Rainbow Stepper</option>
|
||||
<option value="4">Color Dimmer</option>
|
||||
<option value="5">Color Changer</option>
|
||||
<option value="6">Color FlowChanger</option>
|
||||
<option value="7">Color FlowChanger Random-1</option>
|
||||
<option value="8">Color FlowChanger Random-2</option>
|
||||
<option value="9">Color FlowChanger Random-3</option>
|
||||
<option value="10">Knight Rider's KITT</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="control-holder rgb-holder">
|
||||
<label>RGB Brightness</label>
|
||||
<input type="range" min="8" max="255" class="slider" id="rgbBrightness">
|
||||
</div>
|
||||
<div class="control-holder">
|
||||
<label>Fixed Color</label>
|
||||
<div id="color-select-holder">
|
||||
<div>
|
||||
<input type="range" min="1" max="255" class="slider" id="rgbFixColor">
|
||||
</div>
|
||||
<div>
|
||||
<div id="selectedColor"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="control-holder rgb-holder">
|
||||
<label>Direction</label>
|
||||
<input id="rgbDir" type="checkbox" class="switch two-side">
|
||||
<span class="switcher-text left non-selectable" for="rgbDir">left</span>
|
||||
<span class="switcher non-selectable" for="rgbDir"></span>
|
||||
<span class="switcher-text right non-selectable" for="rgbDir">right</span>
|
||||
</div>
|
||||
|
||||
<div class="control-holder">
|
||||
<label for="rgbSpeed">Animation Speed</label>
|
||||
<input type="range" min="1" max="255" class="slider" id="rgbSpeed">
|
||||
</div>
|
||||
</div>
|
||||
<!-- WiFi settings -->
|
||||
<div page=wifi>
|
||||
<br>
|
||||
<div class="control-holder">
|
||||
<label>WiFi SSID</label>
|
||||
<input type="text" id="wifiSsid">
|
||||
</div>
|
||||
<div class="control-holder">
|
||||
<label>WiFi Password</label>
|
||||
<input type="text" id="wifiPsw">
|
||||
</div>
|
||||
<div class="control-holder">
|
||||
<label>AP SSID</label>
|
||||
<input type="text" id="ApSsid">
|
||||
</div>
|
||||
<div class="control-holder">
|
||||
<label>AP Password</label>
|
||||
<input type="text" id="ApPsw">
|
||||
</div>
|
||||
<div class="control-holder">
|
||||
<label>WiFi Mode</label>
|
||||
<input id="wifiMode" type="checkbox" class="switch">
|
||||
<span class="switcher-text left non-selectable" for="wifiMode">AP</span>
|
||||
<span class="switcher non-selectable" for="wifiMode"></span>
|
||||
<span class="switcher-text right non-selectable" for="wifiMode">Client</span>
|
||||
</div>
|
||||
<div class="control-holder">
|
||||
<label>Ntp Server</label>
|
||||
<input type="text" id="NtpServer">
|
||||
</div>
|
||||
<div class="control-holder mqtt-holder">
|
||||
<label>MQTT Enable</label>
|
||||
<input id="mqttEnable" type="checkbox" class="switch">
|
||||
<span class="switcher-text left non-selectable" for="mqttEnable">off</span>
|
||||
<span class="switcher non-selectable" for="mqttEnable"></span>
|
||||
<span class="switcher-text right non-selectable" for="mqttEnable">on</span>
|
||||
</div>
|
||||
<div class="control-holder mqtt-holder">
|
||||
<label>MQTT Address</label>
|
||||
<input type="text" id="mqttBrokerAddr">
|
||||
</div>
|
||||
<div class="control-holder mqtt-holder">
|
||||
<label>MQTT User</label>
|
||||
<input type="text" id="mqttBrokerUser">
|
||||
</div>
|
||||
<div class="control-holder mqtt-holder">
|
||||
<label>MQTT Password</label>
|
||||
<input type="text" id="mqttBrokerPsw">
|
||||
</div>
|
||||
<div class="control-holder mqtt-holder">
|
||||
<label>MQTT Refresh (sec)</label>
|
||||
<input type="range" min="10" max="100" class="slider" id="mqttBrokerRefresh">
|
||||
</div>
|
||||
</div>
|
||||
<div page="settings" hidden="true">
|
||||
<br><br>
|
||||
<!-- Sensor corrections -->
|
||||
<div class="control-holder temperature-holder">
|
||||
<label>Temperature#1</label>
|
||||
<input type="range" min="-5" max="5" step="0.1" class="slider" id="corrT0">
|
||||
</div>
|
||||
<div class="control-holder temperature-holder2">
|
||||
<label>Temperature#2</label>
|
||||
<input type="range" min="-5" max="5" step="0.1" class="slider" id="corrT1">
|
||||
</div>
|
||||
<div class="control-holder humidity-holder">
|
||||
<label>Humidity#1</label>
|
||||
<input type="range" min="-25" max="25" step="0.1" class="slider" id="corrH0">
|
||||
</div>
|
||||
<div class="control-holder humidity-holder2">
|
||||
<label>Humidity#2</label>
|
||||
<input type="range" min="-25" max="25" step="0.1" class="slider" id="corrH1">
|
||||
</div>
|
||||
<div class="control-holder wordclock-holder">
|
||||
<label>CathodeProtect (minutes)</label>
|
||||
<input type="range" min="1" max="60" step="1" class="slider" id="cathProtMin">
|
||||
</div>
|
||||
<div class="control-holder">
|
||||
<label>Firmware Server</label>
|
||||
<input type="text" id="firmware">
|
||||
</div>
|
||||
<br><br><br>
|
||||
<div class="control-holder">
|
||||
<button id="cath-prot-btn" onclick="areYouSure('cathodeProtect')">Cathode Protect</button>
|
||||
<button id="fw-update-btn" onclick="areYouSure('firmwareupdate')">Firmware Update</button>
|
||||
<button id="restart-btn" onclick="areYouSure('reset')">Restart</button>
|
||||
<button id="factory-reset-btn" onclick="areYouSure('factoryreset')">Factory Reset</button>
|
||||
</div>
|
||||
<div id="clock-details">
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="separator"></div>
|
||||
<div class="footer">
|
||||
© 2020-22 Peter Gautier<span>-</span><a href="mailto: gautier.p62@gmail.com">gautier.p62@gmail.com</a><br>
|
||||
<div id="version" class="info" pagemenu="settings"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -0,0 +1,2 @@
|
||||
/*! jQuery v3.5.1 | (c) JS Foundation and other contributors | jquery.org/license */
|
||||
!function(e,t){"use strict";"object"==typeof module&&"object"==typeof module.exports?module.exports=e.document?t(e,!0):function(e){if(!e.document)throw new Error("jQuery requires a window with a document");return t(e)}:t(e)}("undefined"!=typeof window?window:this,function(C,e){"use strict";var t=[],r=Object.getPrototypeOf,s=t.slice,g=t.flat?function(e){return t.flat.call(e)}:function(e){return t.concat.apply([],e)},u=t.push,i=t.indexOf,n={},o=n.toString,v=n.hasOwnProperty,a=v.toString,l=a.call(Object),y={},m=function(e){return"function"==typeof e&&"number"!=typeof e.nodeType},x=function(e){return null!=e&&e===e.window},E=C.document,c={type:!0,src:!0,nonce:!0,noModule:!0};function b(e,t,n){var r,i,o=(n=n||E).createElement("script");if(o.text=e,t)for(r in c)(i=t[r]||t.getAttribute&&t.getAttribute(r))&&o.setAttribute(r,i);n.head.appendChild(o).parentNode.removeChild(o)}function w(e){return null==e?e+"":"object"==typeof e||"function"==typeof e?n[o.call(e)]||"object":typeof e}var f="3.5.1",S=function(e,t){return new S.fn.init(e,t)};function p(e){var t=!!e&&"length"in e&&e.length,n=w(e);return!m(e)&&!x(e)&&("array"===n||0===t||"number"==typeof t&&0<t&&t-1 in e)}S.fn=S.prototype={jquery:f,constructor:S,length:0,toArray:function(){return s.call(this)},get:function(e){return null==e?s.call(this):e<0?this[e+this.length]:this[e]},pushStack:function(e){var t=S.merge(this.constructor(),e);return t.prevObject=this,t},each:function(e){return S.each(this,e)},map:function(n){return this.pushStack(S.map(this,function(e,t){return n.call(e,t,e)}))},slice:function(){return this.pushStack(s.apply(this,arguments))},first:function(){return this.eq(0)},last:function(){return this.eq(-1)},even:function(){return this.pushStack(S.grep(this,function(e,t){return(t+1)%2}))},odd:function(){return this.pushStack(S.grep(this,function(e,t){return t%2}))},eq:function(e){var t=this.length,n=+e+(e<0?t:0);return this.pushStack(0<=n&&n<t?[this[n]]:[])},end:function(){return this.prevObject||this.constructor()},push:u,sort:t.sort,splice:t.splice},S.extend=S.fn.extend=function(){var e,t,n,r,i,o,a=arguments[0]||{},s=1,u=arguments.length,l=!1;for("boolean"==typeof a&&(l=a,a=arguments[s]||{},s++),"object"==typeof a||m(a)||(a={}),s===u&&(a=this,s--);s<u;s++)if(null!=(e=arguments[s]))for(t in e)r=e[t],"__proto__"!==t&&a!==r&&(l&&r&&(S.isPlainObject(r)||(i=Array.isArray(r)))?(n=a[t],o=i&&!Array.isArray(n)?[]:i||S.isPlainObject(n)?n:{},i=!1,a[t]=S.extend(l,o,r)):void 0!==r&&(a[t]=r));return a},S.extend({expando:"jQuery"+(f+Math.random()).replace(/\D/g,""),isReady:!0,error:function(e){throw new Error(e)},noop:function(){},isPlainObject:function(e){var t,n;return!(!e||"[object Object]"!==o.call(e))&&(!(t=r(e))||"function"==typeof(n=v.call(t,"constructor")&&t.constructor)&&a.call(n)===l)},isEmptyObject:function(e){var t;for(t in e)return!1;return!0},globalEval:function(e,t,n){b(e,{nonce:t&&t.nonce},n)},each:function(e,t){var n,r=0;if(p(e)){for(n=e.length;r<n;r++)if(!1===t.call(e[r],r,e[r]))break}else for(r in e)if(!1===t.call(e[r],r,e[r]))break;return e},makeArray:function(e,t){var n=t||[];return null!=e&&(p(Object(e))?S.merge(n,"string"==typeof e?[e]:e):u.call(n,e)),n},inArray:function(e,t,n){return null==t?-1:i.call(t,e,n)},merge:function(e,t){for(var n=+t.length,r=0,i=e.length;r<n;r++)e[i++]=t[r];return e.length=i,e},grep:function(e,t,n){for(var r=[],i=0,o=e.length,a=!n;i<o;i++)!t(e[i],i)!==a&&r.push(e[i]);return r},map:function(e,t,n){var r,i,o=0,a=[];if(p(e))for(r=e.length;o<r;o++)null!=(i=t(e[o],o,n))&&a.push(i);else for(o in e)null!=(i=t(e[o],o,n))&&a.push(i);return g(a)},guid:1,support:y}),"function"==typeof Symbol&&(S.fn[Symbol.iterator]=t[Symbol.iterator]),S.each("Boolean Number String Function Array Date RegExp Object Error Symbol".split(" "),function(e,t){n["[object "+t+"]"]=t.toLowerCase()});var d=function(n){var e,d,b,o,i,h,f,g,w,u,l,T,C,a,E,v,s,c,y,S="sizzle"+1*new Date,p=n.document,k=0,r=0,m=ue(),x=ue(),A=ue(),N=ue(),D=function(e,t){return e===t&&(l=!0),0},j={}.hasOwnProperty,t=[],q=t.pop,L=t.push,H=t.push,O=t.slice,P=function(e,t){for(var n=0,r=e.length;n<r;n++)if(e[n]===t)return n;return-1},R="checked|selected|async|autofocus|autoplay|controls|defer|disabled|hidden|ismap|loop|multiple|open|readonly|required|scoped",M="[\\x20\\t\\r\\n\\f]",I="(?:\\\\[\\da-fA-F]{1,6}"+M+"?|\\\\[^\\r\\n\\f]|[\\w-]|[^\0-\\x7f])+",W="\\["+M+"*("+I+")(?:"+M+"*([*^$|!~]?=)"+M+"*(?:'((?:\\\\.|[^\\\\'])*)'|\"((?:\\\\.|[^\\\\\"])*)\"|("+I+"))|)"+M+"*\\]",F=":("+I+")(?:\\((('((?:\\\\.|[^\\\\'])*)'|\"((?:\\\\.|[^\\\\\"])*)\")|((?:\\\\.|[^\\\\()[\\]]|"+W+")*)|.*)\\)|)",B=new RegExp(M+"+","g"),$=new RegExp("^"+M+"+|((?:^|[^\\\\])(?:\\\\.)*)"+M+"+$","g"),_=new RegExp("^"+M+"*,"+M+"*"),z=new RegExp("^"+M+"*([>+~]|"+M+")"+M+"*"),U=new RegExp(M+"|>"),X=new RegExp(F),V=new RegExp("^"+I+"$"),G={ID:new RegExp("^#("+I+")"),CLASS:new RegExp("^\\.("+I+")"),TAG:new RegExp("^("+I+"|[*])"),ATTR:new RegExp("^"+W),PSEUDO:new RegExp("^"+F),CHILD:new RegExp("^:(only|first|last|nth|nth-lLine truncated
|
||||
@@ -0,0 +1,2 @@
|
||||
/*! jQuery v3.6.0 | (c) OpenJS Foundation and other contributors | jquery.org/license */
|
||||
!function(e,t){"use strict";"object"==typeof module&&"object"==typeof module.exports?module.exports=e.document?t(e,!0):function(e){if(!e.document)throw new Error("jQuery requires a window with a document");return t(e)}:t(e)}("undefined"!=typeof window?window:this,function(C,e){"use strict";var t=[],r=Object.getPrototypeOf,s=t.slice,g=t.flat?function(e){return t.flat.call(e)}:function(e){return t.concat.apply([],e)},u=t.push,i=t.indexOf,n={},o=n.toString,v=n.hasOwnProperty,a=v.toString,l=a.call(Object),y={},m=function(e){return"function"==typeof e&&"number"!=typeof e.nodeType&&"function"!=typeof e.item},x=function(e){return null!=e&&e===e.window},E=C.document,c={type:!0,src:!0,nonce:!0,noModule:!0};function b(e,t,n){var r,i,o=(n=n||E).createElement("script");if(o.text=e,t)for(r in c)(i=t[r]||t.getAttribute&&t.getAttribute(r))&&o.setAttribute(r,i);n.head.appendChild(o).parentNode.removeChild(o)}function w(e){return null==e?e+"":"object"==typeof e||"function"==typeof e?n[o.call(e)]||"object":typeof e}var f="3.6.0",S=function(e,t){return new S.fn.init(e,t)};function p(e){var t=!!e&&"length"in e&&e.length,n=w(e);return!m(e)&&!x(e)&&("array"===n||0===t||"number"==typeof t&&0<t&&t-1 in e)}S.fn=S.prototype={jquery:f,constructor:S,length:0,toArray:function(){return s.call(this)},get:function(e){return null==e?s.call(this):e<0?this[e+this.length]:this[e]},pushStack:function(e){var t=S.merge(this.constructor(),e);return t.prevObject=this,t},each:function(e){return S.each(this,e)},map:function(n){return this.pushStack(S.map(this,function(e,t){return n.call(e,t,e)}))},slice:function(){return this.pushStack(s.apply(this,arguments))},first:function(){return this.eq(0)},last:function(){return this.eq(-1)},even:function(){return this.pushStack(S.grep(this,function(e,t){return(t+1)%2}))},odd:function(){return this.pushStack(S.grep(this,function(e,t){return t%2}))},eq:function(e){var t=this.length,n=+e+(e<0?t:0);return this.pushStack(0<=n&&n<t?[this[n]]:[])},end:function(){return this.prevObject||this.constructor()},push:u,sort:t.sort,splice:t.splice},S.extend=S.fn.extend=function(){var e,t,n,r,i,o,a=arguments[0]||{},s=1,u=arguments.length,l=!1;for("boolean"==typeof a&&(l=a,a=arguments[s]||{},s++),"object"==typeof a||m(a)||(a={}),s===u&&(a=this,s--);s<u;s++)if(null!=(e=arguments[s]))for(t in e)r=e[t],"__proto__"!==t&&a!==r&&(l&&r&&(S.isPlainObject(r)||(i=Array.isArray(r)))?(n=a[t],o=i&&!Array.isArray(n)?[]:i||S.isPlainObject(n)?n:{},i=!1,a[t]=S.extend(l,o,r)):void 0!==r&&(a[t]=r));return a},S.extend({expando:"jQuery"+(f+Math.random()).replace(/\D/g,""),isReady:!0,error:function(e){throw new Error(e)},noop:function(){},isPlainObject:function(e){var t,n;return!(!e||"[object Object]"!==o.call(e))&&(!(t=r(e))||"function"==typeof(n=v.call(t,"constructor")&&t.constructor)&&a.call(n)===l)},isEmptyObject:function(e){var t;for(t in e)return!1;return!0},globalEval:function(e,t,n){b(e,{nonce:t&&t.nonce},n)},each:function(e,t){var n,r=0;if(p(e)){for(n=e.length;r<n;r++)if(!1===t.call(e[r],r,e[r]))break}else for(r in e)if(!1===t.call(e[r],r,e[r]))break;return e},makeArray:function(e,t){var n=t||[];return null!=e&&(p(Object(e))?S.merge(n,"string"==typeof e?[e]:e):u.call(n,e)),n},inArray:function(e,t,n){return null==t?-1:i.call(t,e,n)},merge:function(e,t){for(var n=+t.length,r=0,i=e.length;r<n;r++)e[i++]=t[r];return e.length=i,e},grep:function(e,t,n){for(var r=[],i=0,o=e.length,a=!n;i<o;i++)!t(e[i],i)!==a&&r.push(e[i]);return r},map:function(e,t,n){var r,i,o=0,a=[];if(p(e))for(r=e.length;o<r;o++)null!=(i=t(e[o],o,n))&&a.push(i);else for(o in e)null!=(i=t(e[o],o,n))&&a.push(i);return g(a)},guid:1,support:y}),"function"==typeof Symbol&&(S.fn[Symbol.iterator]=t[Symbol.iterator]),S.each("Boolean Number String Function Array Date RegExp Object Error Symbol".split(" "),function(e,t){n["[object "+t+"]"]=t.toLowerCase()});var d=function(n){var e,d,b,o,i,h,f,g,w,u,l,T,C,a,E,v,s,c,y,S="sizzle"+1*new Date,p=n.document,k=0,r=0,m=ue(),x=ue(),A=ue(),N=ue(),j=function(e,t){return e===t&&(l=!0),0},D={}.hasOwnProperty,t=[],q=t.pop,L=t.push,H=t.push,O=t.slice,P=function(e,t){for(var n=0,r=e.length;n<r;n++)if(e[n]===t)return n;return-1},R="checked|selected|async|autofocus|autoplay|controls|defer|disabled|hidden|ismap|loop|multiple|open|readonly|required|scoped",M="[\\x20\\t\\r\\n\\f]",I="(?:\\\\[\\da-fA-F]{1,6}"+M+"?|\\\\[^\\r\\n\\f]|[\\w-]|[^\0-\\x7f])+",W="\\["+M+"*("+I+")(?:"+M+"*([*^$|!~]?=)"+M+"*(?:'((?:\\\\.|[^\\\\'])*)'|\"((?:\\\\.|[^\\\\\"])*)\"|("+I+"))|)"+M+"*\\]",F=":("+I+")(?:\\((('((?:\\\\.|[^\\\\'])*)'|\"((?:\\\\.|[^\\\\\"])*)\")|((?:\\\\.|[^\\\\()[\\]]|"+W+")*)|.*)\\)|)",B=new RegExp(M+"+","g"),$=new RegExp("^"+M+"+|((?:^|[^\\\\])(?:\\\\.)*)"+M+"+$","g"),_=new RegExp("^"+M+"*,"+M+"*"),z=new RegExp("^"+M+"*([>+~]|"+M+")"+M+"*"),U=new RegExp(M+"|>"),X=new RegExp(F),V=new RegExp("^"+I+"$"),G={ID:new RegExp("^#("+I+")"),CLASS:new RegExp("^\\.("+I+")"),TAG:new RegExp("^("+I+"|[*])"),ATTR:new RegExp("^"+W),PSEUDO:new RegExp("^"+F),CHILD:new RegExp("^Line truncated
|
||||
@@ -0,0 +1,616 @@
|
||||
var isTest = (location.hostname === "localhost" || location.hostname === "127.0.0.1" || location.href.indexOf("file://") != -1);
|
||||
|
||||
var controlInfos = {
|
||||
"version": '<a href="https://github.com/gpeter62/ESP_UniClock" target="_blank">https://github.com/gpeter62/ESP_UniClock</a>',
|
||||
"maxDigits": "Number of tubes",
|
||||
"maxBrightness": "MAX Brightness",
|
||||
"currentDate": "Date",
|
||||
"currentTime": "Time",
|
||||
"temperature": "Temperature sensor #1",
|
||||
"temperature2": "Temperature sensor #2",
|
||||
"humidity": "Humidity sensor #1",
|
||||
"humidity2": "Humidity sensor #2",
|
||||
"pressure": "Pressure sensor #1",
|
||||
"lux": "Lux sensor current value",
|
||||
"rssi": "WiFi RSSI level dB (connection quality)",
|
||||
"wifiMode": "WiFi Client or Access Point",
|
||||
"alarmEnable": "Switch ON/OFF alarm",
|
||||
"alarmTime": "Alarm time (hour/minute)",
|
||||
"alarmPeriod": "Alarm maximum length (sec)",
|
||||
|
||||
//Tube display settings
|
||||
"utc_offset": 'Timezone setting. <a href="https://en.wikipedia.org/wiki/List_of_time_zones_by_country" target="_blank"></a>https://en.wikipedia.org/wiki/List_of_time_zones_by_country',
|
||||
"enableDST": "Daylight Saving Time ON/OFF",
|
||||
"set12_24": "Clock display 12 / 24 hours",
|
||||
"showZero": "Show starting zero on hours",
|
||||
"enableBlink": "ON / OFF",
|
||||
"interval": "Cathode protect interval (minutes)",
|
||||
"enableAutoShutoff": "Enable automatic day/night switch",
|
||||
"dayTime": "Day starts (manual brightness control)",
|
||||
"nightTime": "Night starts (manual brightness control)",
|
||||
"dayBright": "Day brightness",
|
||||
"nightBright": "Night brightness",
|
||||
"manualOverride": "Manual Day/Night change",
|
||||
"animMode": "Tube animation mode: 0=OFF, 1-5: Animations 6-7: Random",
|
||||
"dateMode": "0:dd/mm/yyyy 1:mm/dd/yyyy 2:yyyy/mm/dd",
|
||||
"tempCF": "Celsius / Fahrenheit display",
|
||||
"enableTimeDisplay": "Enable time&date display (Use OFF for thermometers)" ,
|
||||
"dateStart": "Date display start: Date is shown between Start/End seconds",
|
||||
"dateEnd": "Date display end",
|
||||
"tempStart": "Temperature display time slice start",
|
||||
"tempEnd": "Temperature display end",
|
||||
"humidStart": "Humidity display time slice start",
|
||||
"humidEnd": "Humidity display end",
|
||||
"dateRepeatMin": "Date is shown in every xx minutes. 0 = Disable date display",
|
||||
"tempRepeatMin": "Temperature/humidity is shown in every xx minutes. 0 = Disable temp/humid display",
|
||||
"enableDoubleBlink": "Enable both blinking dots",
|
||||
"enableAutoDim": "Enable auto brightness control (if sensor is installed)",
|
||||
"enableRadar": "Enable Radar/Pir tube switch (if sensor is installed)",
|
||||
"radarTimeout": "If Radar is used, automatic switch off after this timeout (minutes)",
|
||||
|
||||
//RGB settings
|
||||
"rgbDir" : "RGB animation direction (left/right)",
|
||||
"rgbEffect": "RGB animation#",
|
||||
"rgbBrightness": "RGB leds brightness 0..255",
|
||||
"rgbFixColor": "RGB color, if FixColor Effect is used",
|
||||
"rgbSpeed": "RGB animation speed",
|
||||
|
||||
//Wifi settings
|
||||
"wifiSsid": "WiFi network SSID to connect",
|
||||
"wifiPsw": "WiFI password",
|
||||
"ApSsid": "AccessPoint name, if standalone mode is used",
|
||||
"ApPsw": "AP password",
|
||||
"NtpServer": "Timeserver name, using WiFi. (Default is: pool.ntp.org)",
|
||||
"mqttBrokerAddr": "MQTT server address, if MQTT used",
|
||||
"mqttBrokerUser": "MQTT user",
|
||||
"mqttBrokerPsw": "MQTT password",
|
||||
"mqttBrokerRefresh": "MQTT send data in every xx sec",
|
||||
"mqttEnable": "Enable/Disable MQTT refresh",
|
||||
"firmware": "http server + firmware name to download new firmware",
|
||||
"corrT0": "Temperature sensor#1 correction",
|
||||
"corrT1": "Temperature sensor#2 correction",
|
||||
"corrH0": "Humidity sensor#1 correction",
|
||||
"corrH1": "Humidity sensor#2 correction" ,
|
||||
"cathProtMin": "Cathode Protect procedure (minutes)",
|
||||
"FW": "firmware code",
|
||||
"tubeDriver": "Tube driver modul"
|
||||
};
|
||||
|
||||
//Example config that UI recieves
|
||||
var configuration = {
|
||||
//Main screen
|
||||
"version": "UniClock 3.0",
|
||||
"maxDigits": 6,
|
||||
"maxBrightness": 10,
|
||||
"currentDate": "2020.10.24",
|
||||
"currentTime": "15:01",
|
||||
"temperature": 22.1,
|
||||
"temperature2": 255,
|
||||
"humidity": 45,
|
||||
"humidity2": 255,
|
||||
"pressure": 255,
|
||||
"lux": 100,
|
||||
"rssi": 0,
|
||||
"wifiMode": true,
|
||||
"alarmEnable": false,
|
||||
"alarmTime": "6:30",
|
||||
"alarmPeriod": 15,
|
||||
"enableRadar": false,
|
||||
"radarTimeout": 5,
|
||||
|
||||
//Tube display settings
|
||||
"utc_offset": 1,
|
||||
"enableDST": true,
|
||||
"set12_24": true,
|
||||
"showZero": true,
|
||||
"enableBlink": true,
|
||||
"interval": 15,
|
||||
"enableAutoShutoff": true,
|
||||
"dayTime": "7:00",
|
||||
"nightTime": "22:00",
|
||||
"dayBright": 10,
|
||||
"nightBright": 3,
|
||||
"manualOverride": true,
|
||||
"animMode": 6,
|
||||
"dateMode": 2, // 0:dd/mm/yyyy 1:mm/dd/yyyy 2:yyyy/mm/dd
|
||||
"tempCF": false, //Temperature Celsius / Fahrenheit
|
||||
"enableTimeDisplay": true,
|
||||
"dateStart": 45,
|
||||
"dateEnd": 50,
|
||||
"tempStart": 35,
|
||||
"tempEnd": 40,
|
||||
"humidStart": 40,
|
||||
"humidEnd": 45,
|
||||
"dateRepeatMin": 3,
|
||||
"tempRepeatMin": 1,
|
||||
"enableDoubleBlink": true,
|
||||
"enableAutoDim": false,
|
||||
|
||||
//RGB settings
|
||||
"rgbDir" : true,
|
||||
"rgbEffect": 1,
|
||||
"rgbBrightness": 100,
|
||||
"rgbFixColor": 150,
|
||||
"rgbSpeed": 50,
|
||||
|
||||
//Wifi settings
|
||||
"wifiSsid": "mywifi",
|
||||
"wifiPsw": "mypsw",
|
||||
"ApSsid": "UniClock",
|
||||
"ApPsw": "uniclock",
|
||||
"NtpServer": "pool.ntp.org",
|
||||
"mqttBrokerAddr": "10.0.99.12",
|
||||
"mqttBrokerUser": "mqtt",
|
||||
"mqttBrokerPsw": "mqttPW",
|
||||
"mqttBrokerRefresh": 30,
|
||||
"mqttEnable": false,
|
||||
"firmware": "http://server/fw.bin",
|
||||
"corrT0": 0.0,
|
||||
"corrT1": 0.0,
|
||||
"corrH0": 0.0,
|
||||
"corrH1": 0.0,
|
||||
"cathProtMin": 5,
|
||||
"FW": "fw",
|
||||
"tubeDriver": "xxx"
|
||||
};
|
||||
|
||||
var isMouseDown = 0;
|
||||
//Runs, when HTML document is fully loaded
|
||||
$(document).ready(function(){
|
||||
$(document).on('mousedown touchstart',function(event) {
|
||||
++isMouseDown;
|
||||
});
|
||||
$(document).on('mouseup touchend',function(event) {
|
||||
--isMouseDown;
|
||||
});
|
||||
|
||||
if(isTest){
|
||||
Init();
|
||||
setCurrentInfos();
|
||||
}
|
||||
else{
|
||||
getConfiguration();
|
||||
//Set current infos, like time, humidity and temperature
|
||||
getCurrentInfos();
|
||||
}
|
||||
});
|
||||
|
||||
function setLocalPagination(){
|
||||
$('[page]').not('[hidden]').each(function(){
|
||||
var page = $(this).attr('page');
|
||||
$('#menu tr').append('<td><div class="menu-item" pageMenu="'+page+'">'+page+'</div></td>');
|
||||
});
|
||||
}
|
||||
|
||||
function getConfiguration(){
|
||||
$.get('/getConfiguration/').done(function(data){
|
||||
configuration = data;
|
||||
}).always(function(){
|
||||
Init();
|
||||
});
|
||||
}
|
||||
|
||||
function sendMsgToArduino(key,value) {
|
||||
if(isTest){
|
||||
console.log(key + " " + value);
|
||||
}
|
||||
else{
|
||||
$.post('/saveSetting/', {"key" : key ,"value" : value }).done(function(data){
|
||||
console.log(data);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
var COLORSATURATION = 255;
|
||||
var WHITE_INDEX = 192;
|
||||
function getColorFrom(WheelPos){
|
||||
if (WheelPos == WHITE_INDEX)
|
||||
return RgbColor(COLORSATURATION/2, COLORSATURATION/2, COLORSATURATION/2);
|
||||
|
||||
WheelPos = 255 - WheelPos;
|
||||
if(WheelPos < 85) {
|
||||
return RgbColor(255 - WheelPos * 3, 0, WheelPos * 3);
|
||||
} else if(WheelPos < 170) {
|
||||
WheelPos -= 85;
|
||||
return RgbColor(0, WheelPos * 3, 255 - WheelPos * 3);
|
||||
} else {
|
||||
WheelPos -= 170;
|
||||
return RgbColor(WheelPos * 3, 255 - WheelPos * 3, 0);
|
||||
}
|
||||
}
|
||||
|
||||
function RgbColor(r, g, b){
|
||||
return 'rgb('+r+','+g+','+b+')';
|
||||
}
|
||||
|
||||
function setPreviewColor(){
|
||||
var color = getColorFrom($('#rgbFixColor').val());
|
||||
$('#selectedColor').css('background-color',color);
|
||||
}
|
||||
|
||||
function setCurrentInfos(){
|
||||
$('#currentTime').html(getCurrentDate(configuration["currentDate"]) + " " + configuration["currentTime"]);
|
||||
$('#lux').html(configuration["lux"]);
|
||||
$('#rssi').html(configuration["rssi"]);
|
||||
$('#pressure').html(round(configuration["pressure"],1));
|
||||
$('#humidity').html(round(configuration["humidity"],1));
|
||||
$('#humidity2').html(round(configuration["humidity2"],1));
|
||||
$('#temperature').html(getTemperature(configuration["temperature"]));
|
||||
$('#temperature2').html(getTemperature(configuration["temperature2"]));
|
||||
$('#temperature, #temperature2').toggleClass('fahrenheit',configuration["tempCF"]);
|
||||
}
|
||||
|
||||
function round(value, decimals) {
|
||||
return parseFloat(value).toFixed(decimals);
|
||||
//return Number(Math.round(value+'e'+decimals)+'e-'+decimals);
|
||||
}
|
||||
|
||||
function getTemperature(temperatureInC){
|
||||
if(configuration["tempCF"]){ //Fahrenheit
|
||||
return round(temperatureInC * 1.8 + 32,1);
|
||||
}
|
||||
else{
|
||||
return round(temperatureInC,1);
|
||||
}
|
||||
}
|
||||
|
||||
function getControlInfo(index){
|
||||
return controlInfos[index];
|
||||
}
|
||||
|
||||
//Contains the most important initializes
|
||||
function Init(){
|
||||
|
||||
$('#dayBright, #nightBright').attr('max',configuration['maxBrightness']);
|
||||
document.title = configuration['version'];
|
||||
$('#versionHeader').html(configuration['version']);
|
||||
|
||||
setLocalPagination();
|
||||
$('[pagemenu]').on('click',function(){
|
||||
$('.menu-item.active').removeClass('active');
|
||||
$('[page].active').removeClass('active');
|
||||
$(this).addClass('active');
|
||||
$('[page="'+$(this).attr('pagemenu')+'"]').addClass('active');
|
||||
if($(this).attr('pagemenu') == 'settings'){
|
||||
getClockDetails();
|
||||
}
|
||||
});
|
||||
$('.menu-item').eq(0).trigger('click');
|
||||
|
||||
$('#rgbFixColor').on('input',function(){
|
||||
setPreviewColor();
|
||||
});
|
||||
|
||||
var sliderFadeOutTimer;
|
||||
function setFadeOutTimer(element){
|
||||
setTimeout(function(){
|
||||
if(isMouseDown > 0){
|
||||
clearTimeout(sliderFadeOutTimer);
|
||||
sliderFadeOutTimer = setFadeOutTimer(element);
|
||||
}
|
||||
else{
|
||||
$(element).addClass('fading').fadeOut('slow',function(){
|
||||
$(this).remove();
|
||||
});
|
||||
}
|
||||
},600);
|
||||
}
|
||||
|
||||
$('input[type="range"]').on('mousedown touchstart input',function(){
|
||||
clearTimeout(sliderFadeOutTimer);
|
||||
var id = $(this).attr('id');
|
||||
var parent = $(this).closest('.control-holder');
|
||||
$('[sliderfor]:not(.fading)').each(function(){
|
||||
if($(this).attr('sliderfor') != id && !$(this).hasClass('fading')){
|
||||
$(this).addClass('fading').fadeOut('slow',function(){
|
||||
$(this).remove();
|
||||
});
|
||||
}
|
||||
});
|
||||
if($(parent).children('.slider-info').length == 0){
|
||||
$(parent).append('<div class="slider-info" sliderfor="'+id+'"></div>');
|
||||
$(parent).children('.slider-info').fadeIn(300);
|
||||
}
|
||||
var _this = $(parent).children('.slider-info').text($(this).val());
|
||||
sliderFadeOutTimer = setFadeOutTimer(_this);
|
||||
});
|
||||
|
||||
$('.form label').on('click',function(){
|
||||
var id = '';
|
||||
if(!!$(this).attr('for')){
|
||||
id = $(this).attr('for');
|
||||
}
|
||||
else{
|
||||
id = $(this).closest('.control-holder').find('input, select, .info').eq(0).attr('id');
|
||||
}
|
||||
|
||||
var info = getControlInfo(id);
|
||||
if(!!info){
|
||||
showPopUp($(this).text(), info, 450, true);
|
||||
}
|
||||
});
|
||||
$('#popup-close-btn, #info-popup').on('click',function(){
|
||||
if($('#info-popup').hasClass('cantClose')){
|
||||
return;
|
||||
}
|
||||
else{
|
||||
$('#info-popup').fadeOut(450);
|
||||
}
|
||||
});
|
||||
$("#popup-box").click(function(event){
|
||||
event.stopPropagation();
|
||||
});
|
||||
|
||||
//binds custom switch functionality
|
||||
$('.switcher').on('click',function(){
|
||||
$('#'+$(this).attr('for')).prop('checked',!$('#'+$(this).attr('for')).is(":checked")).trigger('change');
|
||||
});
|
||||
|
||||
//binds custom switch text functionality
|
||||
$('.switcher-text').on('click',function(){
|
||||
$('#'+$(this).attr('for')).prop('checked',$(this).hasClass('right')).trigger('change');
|
||||
});
|
||||
|
||||
//fills autogenerated select drop downs
|
||||
/*
|
||||
Possible css class:
|
||||
two-digit - fills with two digit values if value lower than 10
|
||||
Possible attributes:
|
||||
min (mandatory) - auto generation's minimum value
|
||||
max (mandatory) - auto generation's maximum value
|
||||
step (optional) - auto generation's step, if not set, default is 1
|
||||
prefix (optional) - concatanate this string before value
|
||||
suffix (optional) - concatanate this string after value
|
||||
*/
|
||||
$('.number-select').each(function(){
|
||||
let from = $(this).attr('min')*1;
|
||||
let to = $(this).attr('max')*1;
|
||||
let step = !!$(this).attr('step') ? $(this).attr('step') : 1;
|
||||
|
||||
for(let i = from; i < to+1; i){
|
||||
let value = $(this).hasClass('two-digit') ? formatToTwoDigit(i) : i;
|
||||
let text_value = value;
|
||||
if($(this).attr('prefix') != null){
|
||||
text_value = $(this).attr('prefix') + text_value
|
||||
}
|
||||
if($(this).attr('suffix') != null){
|
||||
text_value += $(this).attr('suffix')
|
||||
}
|
||||
$(this).append('<option value="'+value+'">'+text_value+'</option>');
|
||||
i += step*1;
|
||||
}
|
||||
});
|
||||
|
||||
//fills the inputs with configuration values
|
||||
for(var index in configuration){
|
||||
let value = configuration[index];
|
||||
if(index == 'dayTime' || index == 'nightTime' || index == 'alarmTime'){
|
||||
value = value.split(':');
|
||||
$('#'+index+'Hours').val(formatToTwoDigit(value[0]));
|
||||
$('#'+index+'Minutes').val(formatToTwoDigit(value[1]));
|
||||
}
|
||||
else if(index == 'version'
|
||||
){
|
||||
$('#'+index).html(value);
|
||||
}
|
||||
else if(index == 'utc_offset' || index == 'maxBrightness' ||
|
||||
index == 'dayBright' || index == 'nightBright' ||
|
||||
index == 'animMode' || index == 'rgbBrightness' ||
|
||||
index == 'rgbFixColor' || index == 'rgbSpeed' ||
|
||||
index == 'rgbEffect' || index == 'interval' ||
|
||||
index == 'alarmPeriod' || index == 'radarTimeout' ||
|
||||
index == 'dateRepeatMin' || index == 'dateMode' ||
|
||||
index == 'dateStart' || index == 'dateEnd' ||
|
||||
index == 'timeStart' || index == 'timeEnd' ||
|
||||
index == 'tempStart' || index == 'tempEnd' ||
|
||||
index == 'humidStart' || index == 'humidEnd' ||
|
||||
index == 'mqttBrokerRefresh' || index == 'tempRepeatMin' ||
|
||||
index == 'wifiSsid' || index == 'wifiPsw' ||
|
||||
index == 'ApSsid' || index == 'ApPsw' ||
|
||||
index == 'mqttBrokerAddr' || index == 'NtpServer' ||
|
||||
index == 'mqttBrokerUser' || index =='mqttBrokerPsw' ||
|
||||
index == 'firmware' || index == 'corrT0' || index == 'corrT1' ||
|
||||
index == 'corrH0' || index == 'corrH1' || index == 'cathProtMin'
|
||||
){
|
||||
$('#'+index).val(value);
|
||||
}
|
||||
else if((index == 'enableDST' || index == 'set12_24' ||
|
||||
index == 'showZero' || index == 'enableBlink' ||
|
||||
index == 'enableAutoShutoff' || index == 'alarmEnable' ||
|
||||
index == 'rgbDir' || index == 'manualOverride' ||
|
||||
index == 'enableAutoDim' || index == 'enableRadar' ||
|
||||
index == 'enableDoubleBlink' || index == 'enableTimeDisplay' ||
|
||||
index == 'mqttEnable' || index == 'tempCF' || index == "wifiMode"
|
||||
) && !!value
|
||||
){
|
||||
$('#'+index).prop('checked',true);
|
||||
}
|
||||
else if(index == "maxDigits"){
|
||||
//TODO
|
||||
}
|
||||
}
|
||||
$('.radar-holder').toggleClass('hidden',configuration['radarTimeout'] == 0);
|
||||
$('.mqtt-holder').toggleClass('hidden',configuration['mqttBrokerRefresh'] == 0);
|
||||
$('.lux-holder').toggleClass('hidden',configuration['lux'] == 255);
|
||||
$('.pressure-holder').toggleClass('hidden',configuration['pressure'] == 255);
|
||||
$('.humidity-holder').toggleClass('hidden',configuration['humidity'] == 255);
|
||||
$('.humidity-holder2').toggleClass('hidden',configuration['humidity2'] == 255);
|
||||
$('.temperature-holder').toggleClass('hidden',configuration['temperature'] == 255);
|
||||
$('.temperature-holder2').toggleClass('hidden',configuration['temperature2'] == 255);
|
||||
$('.rgb-holder').toggleClass('hidden',configuration['rgbEffect'] == 255);
|
||||
$('.tube-holder').toggleClass('hidden',configuration['tubeDriver'] == "DUMMY");
|
||||
$('.wordclock-holder').toggleClass('hidden',configuration['tubeDriver'] == "WORDCLOCK");
|
||||
$('.sensors-holder').toggleClass('hidden',configuration['temperature'] == 255);
|
||||
setPreviewColor();
|
||||
|
||||
//sets a possible good timezone, if not already set
|
||||
//if(!configuration['utc_offset']){
|
||||
// //Intl.DateTimeFormat().resolvedOptions().timeZone <- I am in this Zone
|
||||
// var tryToFindCurrentZone = -(new Date().getTimezoneOffset() / 60);
|
||||
// $('#utc_offset').val(tryToFindCurrentZone);
|
||||
//}
|
||||
|
||||
setTimeout(function(){
|
||||
$('input, select').on('change',function(){
|
||||
var value = '';
|
||||
if($(this).attr('id').indexOf("Hours") == -1 || $(this).attr('id').indexOf("Minutes") == -1){
|
||||
if($(this).attr('type') == 'checkbox'){
|
||||
value = $(this).is(':checked');
|
||||
}
|
||||
else{
|
||||
value = $(this).val();
|
||||
}
|
||||
sendMsgToArduino($(this).attr('id'), value);
|
||||
}
|
||||
else{
|
||||
var key = $(this).attr('id').replace('Hours','').replace('Minutes','');
|
||||
value = $('#'+key+'Hours').val() + ":" + $('#'+key+'Minutes').val();
|
||||
sendMsgToArduino(key, value);
|
||||
}
|
||||
});
|
||||
$('#dateMode').on('change',function(){
|
||||
configuration["dateMode"] = $(this).val();
|
||||
setCurrentInfos();
|
||||
});
|
||||
$('#tempCF').on('change',function(){
|
||||
configuration["tempCF"] = !configuration["tempCF"];
|
||||
setCurrentInfos();
|
||||
});
|
||||
},200);
|
||||
}
|
||||
|
||||
function showPopUp(header, content, timeout, canClose){
|
||||
$('#popup-box h2').text(header);
|
||||
$('#popup-box .content').html(content);
|
||||
$('#info-popup').toggleClass('cantClose',!canClose);
|
||||
$('#info-popup').fadeIn(timeout);
|
||||
}
|
||||
|
||||
function areYouSure(fn){
|
||||
if (confirm("Are you sure to run " + fn + "?")) {
|
||||
if(fn == "reset"){
|
||||
callReset();
|
||||
}
|
||||
else if(fn == "factoryreset"){
|
||||
callFactoryReset();
|
||||
}
|
||||
else if(fn == "firmwareupdate"){
|
||||
callFirmwareUpdate();
|
||||
}
|
||||
else if(fn == "cathodeProtect"){
|
||||
callCathodeProtect();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function callReset(){
|
||||
if(isTest){return;}
|
||||
$.post('/reset/', {}).done(function(data){
|
||||
location.reload();
|
||||
}).always(function(){
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
function callFactoryReset(){
|
||||
if(isTest){return;}
|
||||
$.post('/factoryreset/', {}).done(function(data){
|
||||
location.reload();
|
||||
}).always(function(){
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
function callFirmwareUpdate(){
|
||||
if(isTest){return;}
|
||||
showPopUp("Firmware is updating", "Please do not turn off the clock", 300, false);
|
||||
$.post('/firmwareupdate/', {}).done(function(data){
|
||||
if(!!data.header){
|
||||
showPopUp(data.header, data.content, 300, data.canClose);
|
||||
}
|
||||
}).always(function(){
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
function callCathodeProtect(){
|
||||
if(isTest){return;}
|
||||
$.post('/cathodeProtect/', {}).done(function(data){
|
||||
location.reload();
|
||||
}).always(function(){
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
function getClockDetails(){
|
||||
if(isTest){return;}
|
||||
$.get('/getClockDetails/').done(function(data){
|
||||
$('#clock-details').html(data);
|
||||
}).always(function(){
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
function getCurrentInfos(){
|
||||
if(isTest){return;}
|
||||
$.get('/getCurrentInfos/').done(function(data){
|
||||
for(var i in data){
|
||||
configuration[i] = data[i];
|
||||
}
|
||||
if(!!data["popupMsg"] && data["popupMsg"].length > 0){
|
||||
showPopUp("Info", data["popupMsg"], 300, true);
|
||||
}
|
||||
setCurrentInfos();
|
||||
setTimeout(getCurrentInfos,20000); //refreshes time every 20 second by calling itself
|
||||
}).always(function(){
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
function getCurrentDate(date){
|
||||
var splittedDate = date.split('.');
|
||||
var yyyy = splittedDate[0];
|
||||
var mm = splittedDate[1];
|
||||
var dd = splittedDate[2];
|
||||
|
||||
if(configuration["dateMode"] == 0){
|
||||
return dd+"/"+mm+"/"+yyyy;
|
||||
}
|
||||
else if(configuration["dateMode"] == 1){
|
||||
return mm+"/"+dd+"/"+yyyy;
|
||||
}
|
||||
else if(configuration["dateMode"] == 2){
|
||||
return yyyy+"/"+mm+"/"+dd;
|
||||
}
|
||||
else{
|
||||
return yyyy+"/"+mm+"/"+dd; //if dateMode?? // 0:dd/mm/yyyy 1:mm/dd/yyyy 2:yyyy/mm/dd
|
||||
}
|
||||
}
|
||||
|
||||
//Gets the current time from browser and gives back as readable format
|
||||
function getCurrentTime(){
|
||||
var today = new Date();
|
||||
var dd = formatToTwoDigit(today.getDate());
|
||||
var mm = formatToTwoDigit(today.getMonth()+1);
|
||||
var yyyy = today.getFullYear();
|
||||
var hour = formatToTwoDigit(today.getHours());
|
||||
var minute = formatToTwoDigit(today.getMinutes());
|
||||
|
||||
if(configuration["dateMode"] == 0){
|
||||
return dd+"-"+mm+"-"+yyyy+" "+hour+":"+minute;
|
||||
}
|
||||
else if(configuration["dateMode"] == 1){
|
||||
return mm+"-"+dd+"-"+yyyy+" "+hour+":"+minute;
|
||||
}
|
||||
else if(configuration["dateMode"] == 2){
|
||||
return yyyy+"-"+mm+"-"+dd+" "+hour+":"+minute;
|
||||
}
|
||||
else{
|
||||
return yyyy+"-"+mm+"-"+dd+" "+hour+":"+minute; //if dateMode?? // 0:dd/mm/yyyy 1:mm/dd/yyyy 2:yyyy/mm/dd
|
||||
}
|
||||
}
|
||||
|
||||
//if number is lower than 10, adds a zero
|
||||
function formatToTwoDigit(number){
|
||||
return ("0" + number).slice(-2);
|
||||
}
|
||||
@@ -0,0 +1,479 @@
|
||||
*, *:before, *:after{
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.non-selectable{
|
||||
-webkit-touch-callout: none; /* iOS Safari */
|
||||
-webkit-user-select: none; /* Safari */
|
||||
-khtml-user-select: none; /* Konqueror HTML */
|
||||
-moz-user-select: none; /* Old versions of Firefox */
|
||||
-ms-user-select: none; /* Internet Explorer/Edge */
|
||||
user-select: none; /* Non-prefixed version, currently
|
||||
supported by Chrome, Edge, Opera and Firefox */
|
||||
}
|
||||
html {
|
||||
-webkit-font-smoothing: antialiased;
|
||||
background-image: radial-gradient(#222, #111);
|
||||
min-height: 100%;
|
||||
background-attachment: fixed;
|
||||
color: #c35;
|
||||
}
|
||||
.hidden{
|
||||
display: none !important;
|
||||
}
|
||||
a, a:visited{
|
||||
color: #c35;
|
||||
text-decoration: none;
|
||||
}
|
||||
a:hover{
|
||||
text-decoration: underline;
|
||||
color: #777;
|
||||
}
|
||||
body {
|
||||
font-family: "Helvetica Neue", Helvetica, Arial, "Liberation Sans", FreeSans, sans-serif;
|
||||
margin: 0;
|
||||
}
|
||||
.form{
|
||||
overflow: auto;
|
||||
}
|
||||
.max-width{
|
||||
max-width: 800px;
|
||||
margin: auto;
|
||||
}
|
||||
.row{
|
||||
clear: both;
|
||||
overflow: auto;
|
||||
}
|
||||
.row + .row{
|
||||
margin-top: 15px;
|
||||
}
|
||||
.control-holder{
|
||||
width: 33.333333%;
|
||||
display: block;
|
||||
float: left;
|
||||
padding: 5px;
|
||||
min-height: 80px;
|
||||
position: relative;
|
||||
}
|
||||
.header{
|
||||
overflow: auto;
|
||||
}
|
||||
.form label {
|
||||
cursor: help;
|
||||
width: 100%;
|
||||
display: inline-block;
|
||||
font-weight: bold;
|
||||
padding-top: 0;
|
||||
padding-left: 0;
|
||||
letter-spacing: 0.025em;
|
||||
font-size: 20px;
|
||||
vertical-align: top;
|
||||
line-height: 30px;
|
||||
position: relative;
|
||||
text-shadow: 1px 1px 0 black;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
input, select, button{
|
||||
margin: 0;
|
||||
font: inherit;
|
||||
line-height: normal;
|
||||
width: 100%;
|
||||
max-width: 250px;
|
||||
background: #222222;
|
||||
color: #eee;
|
||||
position: relative;
|
||||
padding: 5px;
|
||||
min-height: 30px;
|
||||
outline: none;
|
||||
line-height: normal;
|
||||
border-radius: 3px;
|
||||
border: 1px solid #666;
|
||||
transition: 0.15s background;
|
||||
}
|
||||
input:not([type="text"]):not(:disabled),select:not(:disabled),button:not(:disabled){
|
||||
cursor: pointer;
|
||||
}
|
||||
input:hover, select:hover, button:hover{
|
||||
background: #333;
|
||||
}
|
||||
input:focus, select:focus, button:focus{
|
||||
background: #4E4E4E;
|
||||
border-color: #888;
|
||||
}
|
||||
input:active, select:active, button:active{
|
||||
background: #5E5E5E;
|
||||
border-color: #aaa;
|
||||
}
|
||||
button{
|
||||
border: 2px solid #c35;
|
||||
color: #c35;
|
||||
text-transform: uppercase;
|
||||
font-weight: bold;
|
||||
}
|
||||
button:hover{
|
||||
color: #ddd;
|
||||
}
|
||||
.multiple-input span{
|
||||
color: white;
|
||||
font-weight: bold;
|
||||
}
|
||||
.two-digit, .small{
|
||||
max-width: 60px;
|
||||
}
|
||||
[pagemenu="settings"]:hover{
|
||||
cursor: pointer;
|
||||
text-decoration: underline;
|
||||
}
|
||||
[type="checkbox"].switch{
|
||||
position: fixed;
|
||||
left: -300px;
|
||||
top: -300px;
|
||||
}
|
||||
[type="checkbox"].switch ~ .switcher{
|
||||
width: 60px;
|
||||
display: inline-block;
|
||||
vertical-align: top;
|
||||
height: 30px;
|
||||
padding: 2px;
|
||||
background: #222222;
|
||||
border: 1px solid #666;
|
||||
transition: 0.15s background;
|
||||
border-radius: 30px;
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
}
|
||||
[type="checkbox"].switch:focus ~ .switcher{
|
||||
background: #5E5E5E;
|
||||
border-color: #aaa;
|
||||
}
|
||||
[type="checkbox"].switch ~ .switcher:after{
|
||||
display: inline-block;
|
||||
content: '';
|
||||
vertical-align: top;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
position: absolute;
|
||||
top: 4px;
|
||||
left: 4px;
|
||||
border-radius: 50%;
|
||||
background: rgba(255,255,255,0.4);
|
||||
transition: 0.3s all;
|
||||
}
|
||||
[type="checkbox"].switch:checked ~ .switcher:after{
|
||||
left: 34px;
|
||||
}
|
||||
[type="checkbox"].switch:not(.two-side):checked ~ .switcher:after{
|
||||
background: rgba(255,255,255,0.7);
|
||||
}
|
||||
[type="checkbox"].switch:not(.two-side):checked ~ .switcher{
|
||||
background: #37bfb9;
|
||||
background-color: #c35;
|
||||
}
|
||||
[page]{
|
||||
display: none;
|
||||
}
|
||||
[page].active{
|
||||
display: block;
|
||||
}
|
||||
#menu-holder{
|
||||
padding-bottom: 40px;
|
||||
}
|
||||
#menu-holder > .floating-menu{
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
z-index: 110;
|
||||
background: #161616;
|
||||
}
|
||||
#menu{
|
||||
width: 100%;
|
||||
}
|
||||
#menu td{
|
||||
padding: 0 18px;
|
||||
}
|
||||
#menu td .menu-item{
|
||||
cursor: pointer;
|
||||
border-bottom: 2px solid gray;
|
||||
text-align: center;
|
||||
text-transform: uppercase;
|
||||
font-weight: bold;
|
||||
line-height: 30px;
|
||||
background-color: transparent;
|
||||
transition: 0.3s border, 0.3s background, 0.3s color;
|
||||
}
|
||||
#menu td .menu-item:hover{
|
||||
background-color: #4E4E4E;
|
||||
border-color: #ddd;
|
||||
}
|
||||
#menu td .menu-item.active{
|
||||
color: #ddd;
|
||||
border-color: #c35;
|
||||
}
|
||||
.switcher-text{
|
||||
text-transform: uppercase;
|
||||
font-weight: bold;
|
||||
line-height: 30px;
|
||||
cursor: pointer;
|
||||
transition: 0.3s color;
|
||||
text-shadow: 1px 1px 0 black;
|
||||
}
|
||||
.switcher-text:hover{
|
||||
color: #c35 !important;
|
||||
}
|
||||
.switcher-text.left{
|
||||
margin-right: 10px;
|
||||
}
|
||||
.switcher-text.right{
|
||||
margin-left: 10px;
|
||||
color: #555;
|
||||
}
|
||||
[type="checkbox"].switch:checked ~ .switcher-text.left{
|
||||
color: #555;
|
||||
}
|
||||
[type="checkbox"].switch:checked ~ .switcher-text.right{
|
||||
color: inherit;
|
||||
}
|
||||
.footer{
|
||||
margin: auto;
|
||||
display: block;
|
||||
text-align: center;
|
||||
text-shadow: 1px 1px 0 black;
|
||||
}
|
||||
.footer span{
|
||||
margin: 0 10px;
|
||||
font-size: 26px;
|
||||
color: #555;
|
||||
}
|
||||
.separator{
|
||||
border-top: 1px dotted #555;
|
||||
margin-top: 20px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.info{
|
||||
color: #555;
|
||||
padding: 10px 0;
|
||||
font-style: italic;
|
||||
display: inline-block;
|
||||
text-shadow: 1px 1px 0 black;
|
||||
}
|
||||
.info.medium{
|
||||
color: #888;
|
||||
font-weight: bold;
|
||||
font-size: 25px;
|
||||
line-height: 32px;
|
||||
}
|
||||
.info.big{
|
||||
color: #888;
|
||||
font-weight: bold;
|
||||
font-size: 32px;
|
||||
line-height: 32px;
|
||||
}
|
||||
.info:after{
|
||||
color: #666;
|
||||
display: inline-block;
|
||||
padding-left: 5px;
|
||||
font-size: 28px;
|
||||
}
|
||||
#temperature:after{
|
||||
content: ' °C';
|
||||
}
|
||||
#temperature.fahrenheit:after{
|
||||
content: ' °F';
|
||||
}
|
||||
#temperature2:after{
|
||||
content: ' °C';
|
||||
}
|
||||
#temperature2.fahrenheit:after{
|
||||
content: ' °F';
|
||||
}
|
||||
#humidity:after{
|
||||
content: '%';
|
||||
}
|
||||
#pressure:after{
|
||||
content: ' hPa';
|
||||
}
|
||||
#lux:after{
|
||||
content: ' lux';
|
||||
}
|
||||
#color-select-holder > div{
|
||||
float: left;
|
||||
}
|
||||
#color-select-holder > div:first-child{
|
||||
width: calc(100% - 40px);
|
||||
}
|
||||
#color-select-holder > div:last-child{
|
||||
width: 40px;
|
||||
}
|
||||
|
||||
#info-popup{
|
||||
display: none;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
z-index: 9999;
|
||||
background: rgba(0,0,0,0.6);
|
||||
}
|
||||
|
||||
#popup-box{
|
||||
padding: 12px;
|
||||
border: 2px solid;
|
||||
margin: auto;
|
||||
width: 100%;
|
||||
background: #161616;
|
||||
margin-top: 50px;
|
||||
border-radius: 12px;
|
||||
position: relative;
|
||||
}
|
||||
#popup-box h2{
|
||||
margin-top: 0;
|
||||
color: #ddd;
|
||||
}
|
||||
#popup-close-btn{
|
||||
position: absolute;
|
||||
left: 100%;
|
||||
top: 0;
|
||||
background: #161616;
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
border-radius: 50%;
|
||||
border: 2px solid;
|
||||
line-height: 30px;
|
||||
text-align: center;
|
||||
font-weight: bold;
|
||||
cursor: pointer;
|
||||
margin-left: -15px;
|
||||
margin-top: -15px;
|
||||
transition: 0.3s background, 0.3s color;
|
||||
}
|
||||
#popup-close-btn:hover{
|
||||
color: #ddd;
|
||||
}
|
||||
#info-popup.cantClose #popup-close-btn{
|
||||
display: none;
|
||||
}
|
||||
|
||||
#selectedColor{
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
background-color: white;
|
||||
border-radius: 50%;
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
.slider-info{
|
||||
display: none;
|
||||
position: absolute;
|
||||
padding: 3px;
|
||||
font-weight: bold;
|
||||
font-size: 24px;
|
||||
background: #161616;
|
||||
border: 2px solid #c35;
|
||||
z-index: 3;
|
||||
border-radius: 50%;
|
||||
text-align: center;
|
||||
color: #ddd;
|
||||
min-width: 50px;
|
||||
top: 0;
|
||||
right: 10px;
|
||||
}
|
||||
|
||||
/* slider control */
|
||||
|
||||
.slidecontainer {
|
||||
width: 100%; /* Width of the outside container */
|
||||
}
|
||||
|
||||
/* The slider itself */
|
||||
.slider {
|
||||
margin-top: 9px;
|
||||
height: 10px;
|
||||
min-height: auto;
|
||||
-webkit-appearance: none; /* Override default CSS styles */
|
||||
appearance: none;
|
||||
outline: none; /* Remove outline */
|
||||
opacity: 0.7; /* Set transparency (for mouse-over effects on hover) */
|
||||
-webkit-transition: .2s; /* 0.2 seconds transition on hover */
|
||||
transition: opacity .2s;
|
||||
}
|
||||
|
||||
/* Mouse-over effects */
|
||||
.slider:hover {
|
||||
opacity: 1; /* Fully shown on mouse-over */
|
||||
}
|
||||
|
||||
/* The slider handle (use -webkit- (Chrome, Opera, Safari, Edge) and -moz- (Firefox) to override default look) */
|
||||
.slider::-webkit-slider-thumb {
|
||||
-webkit-appearance: none; /* Override default look */
|
||||
appearance: none;
|
||||
width: 20px; /* Set a specific slider handle width */
|
||||
height: 20px; /* Slider handle height */
|
||||
background: #c35;
|
||||
cursor: pointer; /* Cursor on hover */
|
||||
border-radius: 50%;
|
||||
border: 2px solid #ddd;
|
||||
}
|
||||
|
||||
.slider::-moz-range-thumb {
|
||||
width: 20px; /* Set a specific slider handle width */
|
||||
height: 20px; /* Slider handle height */
|
||||
background: #c35;
|
||||
cursor: pointer; /* Cursor on hover */
|
||||
border-radius: 50%;
|
||||
border: 2px solid #ddd;
|
||||
}
|
||||
|
||||
/* slider control end */
|
||||
|
||||
|
||||
/* width */
|
||||
::-webkit-scrollbar {
|
||||
width: 10px;
|
||||
}
|
||||
|
||||
/* Track */
|
||||
::-webkit-scrollbar-track {
|
||||
background: #333;
|
||||
}
|
||||
|
||||
/* Handle */
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: #c35;
|
||||
}
|
||||
|
||||
/* Handle on hover */
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: #d43d5f;
|
||||
}
|
||||
@media screen and (max-width: 600px){
|
||||
.control-holder{
|
||||
width: 50%;
|
||||
}
|
||||
.form label{
|
||||
font-size: 15px;
|
||||
}
|
||||
.switcher-text{
|
||||
font-size: 14px;
|
||||
}
|
||||
.info{
|
||||
padding: 5px 0;
|
||||
}
|
||||
.info:after{
|
||||
font-size: 16px;
|
||||
}
|
||||
.info.big{
|
||||
font-size: 20px;
|
||||
}
|
||||
.info.medium{
|
||||
font-size: 15px;
|
||||
}
|
||||
}
|
||||
@media screen and (max-width: 360px){
|
||||
body{
|
||||
text-align: center;
|
||||
}
|
||||
.control-holder{
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
#ifdef DUMMY
|
||||
|
||||
//Dummy clock driver for sensor box / mqtt sensors
|
||||
|
||||
char tubeDriver[] = "DUMMY";
|
||||
int maxDigits = 6;
|
||||
|
||||
void setup_pins() {
|
||||
DPRINTLN("Dummy display: no pins are used");
|
||||
}
|
||||
|
||||
#if defined(ESP32)
|
||||
void IRAM_ATTR writeDisplay() {}
|
||||
#else
|
||||
void ICACHE_RAM_ATTR writeDisplay(){}
|
||||
#endif
|
||||
|
||||
void clearTubes() {}
|
||||
void writeDisplaySingle() {}
|
||||
#endif
|
||||
@@ -0,0 +1,303 @@
|
||||
// =============================================================================
|
||||
// font5x7.h - 5x7 dot-matrix bitmap font table
|
||||
// =============================================================================
|
||||
//
|
||||
// WHAT THIS IS
|
||||
// A 256-glyph bitmap font (derived from the classic Adafruit GLCD 5x7 font,
|
||||
// "glcdfont.c") used to render single characters on 5-column x 7-row LED
|
||||
// matrix / dot-matrix displays. Several glyphs have been hand-modified from
|
||||
// the original Adafruit set (see "old:" comments below for the replaced
|
||||
// values), and the upper half of the table (0x80-0xFF) has been customized
|
||||
// with Hungarian/German accented letters (a, ä, ö, u, Ä, Ö, U), Greek
|
||||
// letters, and math/misc symbols in place of the original CP437 glyphs.
|
||||
//
|
||||
// DATA LAYOUT
|
||||
// _font[] is a flat byte array with exactly 5 bytes per character:
|
||||
// _font[c*5 + col] for col = 0..4 (NUMCOLS in wordclock.ino)
|
||||
// Each byte is one COLUMN of the glyph (column-major, not row-major).
|
||||
// Within a byte, bit 0 (LSB) is the TOP pixel of that column and bit 6 is
|
||||
// the BOTTOM pixel (7 rows tall); bit 7 is unused/always 0. Columns are
|
||||
// ordered left-to-right within each 5-byte block.
|
||||
// The table holds 256 entries indexed 0x00-0xFF, i.e. it is indexed
|
||||
// directly by (unsigned) character code - there is no separate character
|
||||
// map or lookup table.
|
||||
//
|
||||
// WHO USES IT
|
||||
// wordclock.ino is the only consumer: it does #include "font5x7.h" and
|
||||
// reads the array in show(byte c) as
|
||||
// loccol[i] = _font[c*NUMCOLS + i];
|
||||
// then calls transposePattern() to convert the column-major glyph into the
|
||||
// row-major bit pattern actually clocked out to the display hardware
|
||||
// (honoring the _upsidedown orientation flag). No other .ino in this
|
||||
// project references _font[] or includes this header.
|
||||
//
|
||||
// CHARACTER RANGE
|
||||
// 0x00-0x1F : custom symbols (smileys, suits, arrows, euro sign, etc.),
|
||||
// not standard control characters.
|
||||
// 0x20-0x7E : standard printable ASCII (space through ~).
|
||||
// 0x7F-0xFF : custom/extended glyphs - accented Hungarian/German letters,
|
||||
// Greek letters, and math symbols (see inline comments).
|
||||
// =============================================================================
|
||||
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
|
||||
};
|
||||
@@ -0,0 +1,151 @@
|
||||
// ============================================================================
|
||||
// gps.ino - GPS-based time synchronization
|
||||
// ----------------------------------------------------------------------------
|
||||
// Purpose : for STANDALONE clocks that have no WiFi/internet connectivity
|
||||
// (see "//#define USE_GPS //use for standalone clock, without wifi
|
||||
// internet access" in the parameter reference block at the top of
|
||||
// ESP32_UniClock2.ino). Where WiFi IS available, the core program
|
||||
// normally gets time from an NTP timeserver instead (see
|
||||
// updateTimefromTimeserver() in ESP32_UniClock2.ino); GPS and WiFi
|
||||
// time sync are not mutually exclusive, but GPS exists specifically
|
||||
// to cover the no-internet case.
|
||||
// Hardware: any generic NMEA-0183 serial GPS receiver module (e.g. Ublox
|
||||
// NEO-6M/NEO-8M or similar). Only the module's TX output needs to
|
||||
// be wired to the MCU's RX pin - we only ever READ NMEA sentences
|
||||
// from the module, we never send configuration commands to it, so
|
||||
// TX/TXPin on the MCU side is intentionally unused (-1).
|
||||
// Library : TinyGPS++ (TinyGPSPlus) - see libraries/TinyGPSPlus-1.0.2.zip.
|
||||
// It incrementally parses NMEA sentences (GPRMC/GPGGA/... checksum
|
||||
// validated) character by character via gps.encode(), and exposes
|
||||
// decoded fields (time, date, location, etc.) with isValid()/
|
||||
// isUpdated() freshness flags.
|
||||
// Wiring : ESP8266 -> SoftwareSerial on GPIO3 (RX only, bit-banged, no HW
|
||||
// UART needed). GPIO3 is also the chip's native
|
||||
// RXD0/USB-serial pin, so the GPS wire must be
|
||||
// disconnected while flashing firmware.
|
||||
// ESP32 -> HardwareSerial(1) on GPIO25 (RX only).
|
||||
// Used by : CLOCK_1 (ESP8266, IN-16 Nixie tubes) and CLOCK_13 (ESP8266,
|
||||
// IV-16 Numitron tubes) in clocks.h have USE_GPS active by
|
||||
// default. CLOCK_50, CLOCK_71, CLOCK_80/81/82 and CLOCK_99 carry
|
||||
// a commented-out "//#define USE_GPS" as an optional switch.
|
||||
// Time : GPS always provides time in UTC. This file converts to local
|
||||
// time itself using a flat prm.utc_offset/enableDST arithmetic
|
||||
// offset (see getGPS() below) - unlike the WiFi/NTP path in
|
||||
// ESP32_UniClock2.ino, which uses the Timezone library (myTZ/
|
||||
// myDST/mySTD) to resolve the exact calendar-based DST transition
|
||||
// date. See the note near the setTime() calls below.
|
||||
// ============================================================================
|
||||
#ifdef USE_GPS
|
||||
|
||||
#include <TinyGPS++.h> // Tiny GPS Plus Library
|
||||
|
||||
#if defined(ESP8266)
|
||||
#include "SoftwareSerial.h" // Software Serial Library so we can use other Pins for communication with the GPS module
|
||||
static int RXPin = 3; //RX pin - remove wire, when uploading program to 8266
|
||||
static int TXPin = -1; //not used
|
||||
SoftwareSerial ss(RXPin, TXPin);
|
||||
#else
|
||||
#include <HardwareSerial.h> //on ESP32 no SoftwareSerial is needed
|
||||
static int RXPin = 25; //RX pin
|
||||
static int TXPin = -1; //not used (we only read the GPS, never write to it, so TX is not wired/used)
|
||||
HardwareSerial ss(1);
|
||||
#endif
|
||||
|
||||
// The serial connection to the GPS device
|
||||
static uint32_t GPSBaud = 9600; // Ublox GPS default Baud Rate is 9600 (NMEA default for most modules; if a module was reconfigured to a different baud rate, this must be changed to match, otherwise all sentences will fail checksum validation)
|
||||
TinyGPSPlus gps; // Create an Instance of the TinyGPS++ object called gps
|
||||
|
||||
void setupGPS() {
|
||||
delay(1500); //give the GPS module time to power up / boot before we start talking to it
|
||||
DPRINTLN("Starting GPS...");
|
||||
if (RXPin>=0) regPin(RXPin,"RXPin"); //bookkeeping only (for the pin-usage diagnostics/webpage), doesn't affect GPS operation
|
||||
if (TXPin>=0) regPin(TXPin,"TXPin"); //TXPin is -1 (unused) on both platforms, so this branch never actually runs
|
||||
#if defined(ESP8266)
|
||||
ss.begin(GPSBaud);
|
||||
//ss.begin(GPSBaud, SWSERIAL_8N1, RXPin, TXPin, false);
|
||||
#else
|
||||
ss.begin(GPSBaud, SERIAL_8N1, RXPin); // ESP32 HardwareSerial::begin(baud, config, rxPin, txPin=-1) - txPin left at its default (-1/unused) since we don't transmit to the GPS
|
||||
#endif
|
||||
delay(200);
|
||||
GPSexist = true; // POSSIBLE BUG / design note: this is set unconditionally right after opening the serial port, regardless of whether a GPS module is actually attached, powered, or has ever produced a valid NMEA sentence. calcTime() uses GPSexist to decide whether to call getGPS() every cycle; if no module is present this flag stays true forever and getGPS() will just keep returning false harmlessly, but GPSexist does NOT mean "GPS is present/working", only "USE_GPS was compiled in and setupGPS() ran". Compare with how RTCexist is determined elsewhere (presumably via a presence check) for a stricter pattern.
|
||||
smartDelay(500); //feed the parser for 500ms so gps.encode() has a chance to see the first sentences before we try to read anything from it
|
||||
getGPS();
|
||||
}
|
||||
|
||||
boolean getGPS(){
|
||||
static unsigned long lastRun = 0;
|
||||
boolean res = false;
|
||||
|
||||
// Throttle: only attempt a refresh once every 10s. lastRun==0 lets the very first call
|
||||
// (from setupGPS()) through immediately without waiting for the first 10s window.
|
||||
if ((lastRun!=0) && (millis() - lastRun) < 10000) //refresh rate
|
||||
return(res);
|
||||
|
||||
lastRun = millis();
|
||||
smartDelay(500); //block for 500ms while feeding gps.encode(), giving the parser a chance to complete a fresh RMC/GGA/ZDA sentence since the last call
|
||||
// NOTE: this checks gps.time.isValid()/isUpdated() (sentence was parsed correctly and is new since last read),
|
||||
// but deliberately does NOT require gps.location.isValid() or any fix-quality/satellite-count check.
|
||||
// Many GPS chipsets can decode time/date from the almanac/ephemeris before achieving a full position fix,
|
||||
// so this allows time sync slightly earlier than a location fix would. It does mean the reported time could
|
||||
// in theory be used even with a very weak/no fix, as long as the receiver has parsed a valid time field.
|
||||
// gps.date.isUpdated() is intentionally/incidentally not checked here - time and date normally arrive
|
||||
// together in the same NMEA sentence (e.g. RMC), so checking only time.isUpdated() is normally sufficient.
|
||||
if (gps.time.isValid() && gps.date.isValid() && gps.time.isUpdated()) {
|
||||
if (gps.date.year()>2020) { //sanity check: many GPS modules report a bogus default date (e.g. from firmware build date or epoch start) until they have decoded a real almanac; reject anything before this fixed threshold. NOTE: this threshold is a fixed constant and will need to be bumped periodically as time passes.
|
||||
DPRINTLN("Time refreshed from GPS");
|
||||
setTime(gps.time.hour(),gps.time.minute(),gps.time.second(),gps.date.day(),gps.date.month(),gps.date.year()); //set the time (hr,min,sec,day,mnth,yr) -- GPS time is always UTC, so system time is UTC at this point
|
||||
// Convert UTC -> local time. NOTE: unlike updateTimefromTimeserver() in ESP32_UniClock2.ino (which builds a
|
||||
// Timezone object from myDST/mySTD TimeChangeRule's and lets the Timezone library pick the correct offset
|
||||
// for the current calendar date, i.e. it auto-detects whether "today" falls inside the DST period), this GPS
|
||||
// path just adds a flat prm.utc_offset hour, plus one more hour if prm.enableDST is currently true - it does
|
||||
// NOT look at gps.date to determine whether the DST period has actually started/ended this year. Since
|
||||
// enableDST is only ever changed manually (web UI / MQTT, see key=="enableDST" handling in
|
||||
// ESP32_UniClock2.ino) and is never auto-toggled by date, a standalone GPS-only clock (no WiFi to run the
|
||||
// calendar-aware Timezone conversion) will NOT automatically switch to/from summer time on the correct
|
||||
// calendar day unless the user (or some other mechanism) flips enableDST at the right moment.
|
||||
setTime(now()+(prm.utc_offset + (prm.enableDST ? 1 : 0)) * 3600);
|
||||
lastTimeUpdate = millis();
|
||||
}
|
||||
#ifdef DEBUG
|
||||
printGPS();
|
||||
#endif
|
||||
|
||||
res = true;
|
||||
} //endif valid date&time
|
||||
return (res);
|
||||
}
|
||||
|
||||
void printGPS() { // debug-only helper (only called when DEBUG is defined, see getGPS() above); values printed here are NOT freshness-checked, so location/satellites may be stale or invalid if no fix has ever been acquired
|
||||
DPRINT("Latitude : "); DPRINT(gps.location.lat(), 5); DPRINT(" Longitude : "); DPRINTLN(gps.location.lng(), 4);
|
||||
DPRINT("Satellites: "); DPRINT(gps.satellites.value()); DPRINT(" Elevation : "); DPRINT(gps.altitude.feet()); DPRINTLN("ft");
|
||||
DPRINT("GPS Time UTC : "); DPRINT(gps.time.hour()); // GPS time UTC
|
||||
DPRINT(":"); DPRINT(gps.time.minute()); // Minutes
|
||||
DPRINT(":"); DPRINTLN(gps.time.second()); // Seconds
|
||||
}
|
||||
|
||||
|
||||
// This custom version of delay() ensures the TinyGPS++ object keeps being "fed" bytes while we're blocked.
|
||||
// TinyGPSPlus has no internal buffering of its own: gps.encode() must be called for every single byte read from
|
||||
// the serial port, or that byte's contribution to the current NMEA sentence is lost. The GPS module streams NMEA
|
||||
// sentences continuously (typically once per second) regardless of whether anyone is listening, and the serial
|
||||
// RX FIFO (HW UART on ESP32, or the SoftwareSerial ring buffer on ESP8266) is small, so if this isn't called
|
||||
// often enough for long enough, incoming bytes get silently dropped/overwritten and whole sentences are lost -
|
||||
// which is exactly why Fdelay() in ESP32_UniClock2.ino calls smartDelay(1) on every iteration of its busy-loop
|
||||
// (see "feed GPS" there): it's not really about producing a 1ms delay, it's about draining ss.available() often
|
||||
// enough during any other blocking/animation work so the GPS UART buffer never has a chance to overflow.
|
||||
void smartDelay(unsigned long ms) {
|
||||
unsigned long start = millis();
|
||||
do {
|
||||
while (ss.available())
|
||||
gps.encode(ss.read());
|
||||
} while (millis() - start < ms);
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------------------
|
||||
|
||||
#else
|
||||
// USE_GPS not defined for this CLOCK_NN config: provide no-op stand-ins so the rest of the core program
|
||||
// (setup()/calcTime() in ESP32_UniClock2.ino) can call setupGPS()/getGPS() unconditionally without more #ifdef's.
|
||||
void setupGPS() {GPSexist = false;}
|
||||
boolean getGPS() {return(false);}
|
||||
#endif
|
||||
@@ -0,0 +1,306 @@
|
||||
#ifdef USE_MQTT
|
||||
//=========================================================================================================================
|
||||
// mqtt.ino - Home Assistant / MQTT integration (compiled only if USE_MQTT is defined for the active CLOCK_NN in clocks.h)
|
||||
//
|
||||
// Library: ArduinoHA (https://github.com/dawidchyrzynski/arduino-home-assistant), which wraps PubSubClient
|
||||
// (https://pubsubclient.knolleary.net/) and adds Home Assistant MQTT Discovery on top of it. Because ArduinoHA
|
||||
// is used, this file never hand-builds HA discovery JSON payloads itself - HADevice/HAMqtt/HASensor below
|
||||
// publish the discovery config topics automatically the first time mqtt.begin()/mqtt.loop() connects.
|
||||
//
|
||||
// MASTER / SLAVE sensor-sharing architecture (the distinctive part of this subsystem):
|
||||
// Every clock that defines USE_MQTT connects to the same broker (prm.mqttBrokerAddr/User/Psw, set on the web UI) and
|
||||
// PUBLISHES its own local sensors (temperature/humidity/pressure/lux/radar, whichever are physically present - see
|
||||
// HASensor objects below) under a "<MQTT_PREFIX>/sensor/<mac>/..." style topic, discovered via HA MQTT Discovery.
|
||||
// A clock that does NOT have its own sensor (or wants to reuse another clock's reading) can instead be built with
|
||||
// USE_MASTER_CLOCK, and SUBSCRIBE to another clock's already-published topics:
|
||||
// USE_MASTER_TEMP + MASTER_TEMPERATURE_TOPIC -> mirrors the master's temperature into local temperature[] array
|
||||
// USE_MASTER_HUMID + MASTER_HUMIDITY_TOPIC -> mirrors the master's humidity into local humid[] array
|
||||
// USE_MASTER_RADAR + MASTER_RADAR_TOPIC -> mirrors the master's PIR/radar presence state into mqttRadarON
|
||||
// This lets one "master" clock's physical sensor (e.g. a dedicated sensor box, or a clock with a real radar/PIR
|
||||
// sensor wired to RADAR_PIN) feed several "slave" clocks that have no sensor of their own, purely over MQTT.
|
||||
// IMPORTANT: if a "slave" clock DOES also have its own local RADAR_PIN wired, the local pin always wins - the radar
|
||||
// topic subscription is skipped entirely in onMqttConnected() (see RADAR_PIN>=0 check there), and the core loop's
|
||||
// checkTubePowerOnOff() also prefers digitalRead(RADAR_PIN) over the MQTT-derived mqttRadarON when both exist.
|
||||
// As of the current clocks.h: CLOCK_99 ("Dummy clock, sensor box with MQTT connector") is a pure MASTER/publisher
|
||||
// (USE_MQTT on, USE_MASTER_CLOCK off, has its own RADAR_PIN). CLOCK_26 has USE_MQTT + USE_MASTER_CLOCK +
|
||||
// USE_MASTER_RADAR all on, so it both publishes its own temperature/humidity sensors AND subscribes to another
|
||||
// clock's radar topic. CLOCK_23/CLOCK_23A define the USE_MASTER_* macros too but currently have USE_MQTT commented
|
||||
// out in clocks.h, so for those configs this whole file is compiled out and the master settings are presently inert.
|
||||
//
|
||||
// Call pattern from the core .ino: setupMqtt() is called once from setup() when USE_MQTT is defined; mqttSend() is
|
||||
// called every loop() iteration (but only while WiFi is connected and prm.mqttEnable is set) - it internally rate-limits
|
||||
// actual publishes to once every prm.mqttBrokerRefresh seconds, while still pumping mqtt.loop() (which also drives
|
||||
// PubSubClient's/ArduinoHA's own reconnect handling) on every call.
|
||||
//_______________________________________________________________________________________________________________________
|
||||
//To use MQTT services the following settings should be defined in clocks.h
|
||||
//#define 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"
|
||||
//_______________________________________________________________________________________________________________________
|
||||
// (the block above is documentation only, shown here as a template of what to copy into a CLOCK_NN section of clocks.h -
|
||||
// none of these #define lines are active in this file; the real values come from clocks.h at compile time.)
|
||||
|
||||
//To subscribe to my sensors: You can check it with Chrome browser + MQTTlens addon) MAC_ADDRESS: for example: 3e090b3e090b
|
||||
// homeassistant/sensor/3e090b3e090b/temperature/value
|
||||
// homeassistant/sensor/3e090b3e090b/humidity/value
|
||||
// homeassistant/sensor/3e090b3e090b/pressure/value
|
||||
// homeassistant/sensor/3e090b3e090b/lux/value
|
||||
|
||||
// old solution, now moved to prm.mqttBrokerAddr, available on clock's web page
|
||||
//#define BROKER_ADDR IPAddress(192,168,1,241)
|
||||
//#define BROKER_USERNAME "" //moved to prm.mqttBrokerUser
|
||||
//#define BROKER_PASSWORD "" //moved to mqttBrokerPsw
|
||||
//_______________________________________________________________________________________________________________________
|
||||
// Credential handling: broker IP/user/password are NOT compile-time constants (see the commented-out BROKER_* macros
|
||||
// above, kept only as historical documentation) - they live in prm.mqttBrokerAddr/mqttBrokerUser/mqttBrokerPsw, which
|
||||
// are persisted settings editable from the clock's own web UI (and read here in setupMqtt()/mqttSend()). This lets a
|
||||
// user point any already-flashed clock at a different broker without recompiling.
|
||||
// SECURITY: factoryReset() in the core .ino seeds these with a hardcoded default
|
||||
// (strcpy(prm.mqttBrokerAddr,"10.10.0.202"); strcpy(prm.mqttBrokerUser,"mqtt"); strcpy(prm.mqttBrokerPsw,"mqtt")) -
|
||||
// i.e. a real broker IP and a trivial user/password baked into the firmware as the out-of-the-box default. Not this
|
||||
// file's code, but directly relevant here since it is exactly the credential pair mqttSend()/setupMqtt() use below.
|
||||
|
||||
#include <ArduinoHA.h> //https://github.com/dawidchyrzynski/arduino-home-assistant
|
||||
//https://pubsubclient.knolleary.net/ https://github.com/knolleary/pubsubclient
|
||||
|
||||
#if !defined(MQTT_PREFIX)
|
||||
#define MQTT_PREFIX "UniClock32" //used both as the HA discovery prefix and as the first path segment of this device's own published topics
|
||||
#endif
|
||||
|
||||
byte mac[6]; //this device's WiFi MAC, used as a per-device unique ID/topic segment (see setupMqtt())
|
||||
char masterTemperatureTopic[80] = ""; //resolved from MASTER_TEMPERATURE_TOPIC (clocks.h) if USE_MASTER_CLOCK/USE_MASTER_TEMP is used - topic this clock SUBSCRIBES to
|
||||
char masterHumidityTopic[80] = ""; //resolved from MASTER_HUMIDITY_TOPIC (clocks.h) if USE_MASTER_CLOCK/USE_MASTER_HUMID is used - topic this clock SUBSCRIBES to
|
||||
char masterRadarTopic[80] = ""; //resolved from MASTER_RADAR_TOPIC (clocks.h) if USE_MASTER_CLOCK/USE_MASTER_RADAR is used - topic this clock SUBSCRIBES to
|
||||
|
||||
// Index into the shared temperature[]/humid[] arrays (declared in the core .ino, up to 6 slots) reserved for the
|
||||
// MASTER clock's mirrored reading, so the rest of the firmware (display rotation, web UI, etc.) can treat the
|
||||
// MQTT-sourced master reading exactly like one more local sensor. Assigned in setupMqtt() as "next free slot"
|
||||
// (useTemp/useHumid BEFORE incrementing) - despite the doc comment above saying "this will be the sensor#0", it is
|
||||
// only actually sensor#0 if this clock has no other local temperature/humidity sensor already counted.
|
||||
byte MASTERtempPtr=0;
|
||||
byte MASTERhumidPtr=0;
|
||||
|
||||
WiFiClient client;
|
||||
HADevice device;
|
||||
HAMqtt mqtt(client, device); //ArduinoHA wrapper around PubSubClient; mqtt.loop() also drives PubSubClient's own reconnect/keepalive handling
|
||||
//Sensors of local clock, to send data (each one auto-publishes its HA MQTT Discovery config the first time it connects):
|
||||
HASensor mqttTemp("temperature");
|
||||
HASensor mqttHumid("humidity");
|
||||
HASensor mqttPress("pressure");
|
||||
HASensor mqttLux("lux");
|
||||
HASensor mqttRadar("radar");
|
||||
|
||||
// This is the SLAVE side of the master/slave architecture: called by ArduinoHA/PubSubClient whenever a message
|
||||
// arrives on any topic this clock is subscribed to (see onMqttConnected() below for what gets subscribed - only
|
||||
// the MASTER_* topics selected via USE_MASTER_TEMP/USE_MASTER_HUMID/USE_MASTER_RADAR in clocks.h). The payload is
|
||||
// the raw value published by the MASTER clock's own mqttSend()/HASensor.setValue() (a plain ASCII number for
|
||||
// temperature/humidity, "0"/"1" for radar - matching HASensor's default text encoding).
|
||||
void onMqttMessage(const char* topic, const uint8_t* payload, uint16_t length) {
|
||||
float tempTMP,humidTMP;
|
||||
char tmp[100];
|
||||
// This callback is called when message from MQTT broker is received.
|
||||
// Please note that you should always verify if the message's topic is the one you expect.
|
||||
// For example: if (memcmp(topic, "myCustomTopic") == 0) { ... }
|
||||
|
||||
DPRINT("New message on topic: "); DPRINT(topic);
|
||||
|
||||
// POSSIBLE BUG: `length` (the broker-reported payload size) is copied into the fixed 100-byte `tmp` buffer with
|
||||
// no bounds check - a payload of 100 bytes or more (e.g. a misconfigured/malicious publisher on the same topic)
|
||||
// overflows `tmp` on the stack. Also, memcpy() does NOT null-terminate `tmp`: it is used afterwards as a C string
|
||||
// (DPRINTLN(tmp), atof(tmp), tmp[0]) with whatever byte happens to follow the copied data in memory, which is
|
||||
// undefined behavior if that byte isn't already '\0'. A `if (length >= sizeof(tmp)) length = sizeof(tmp)-1;`
|
||||
// guard plus `tmp[length] = '\0';` after the memcpy would fix both issues.
|
||||
memcpy(tmp,payload,length);
|
||||
DPRINT(" Data: "); DPRINTLN(tmp);
|
||||
// Topic match: compares up to sizeof(masterTemperatureTopic) (80, the fixed buffer size) bytes rather than the
|
||||
// actual topic length. This still behaves like strcmp() in practice because strncmp() stops as soon as it hits
|
||||
// the '\0' terminator on either side, but it is fragile - if MASTER_TEMPERATURE_TOPIC in clocks.h were ever 80
|
||||
// characters or longer, strncpy() in setupMqtt() would not null-terminate masterTemperatureTopic, and this
|
||||
// comparison could silently misbehave. Using strcmp(topic, masterTemperatureTopic) would be more robust.
|
||||
if ((strlen(masterTemperatureTopic)>0) && (strncmp(topic,masterTemperatureTopic,sizeof(masterTemperatureTopic)) ==0)) {
|
||||
tempTMP = atof(tmp);
|
||||
temperature[MASTERtempPtr] = tempTMP; //overwrite the reserved "master" slot in the shared temperature[] array (see MASTERtempPtr above)
|
||||
DPRINT("MASTER clock's temperature:"); DPRINTLN(tempTMP);
|
||||
}
|
||||
if ((strlen(masterHumidityTopic)>0) && (strncmp(topic,masterHumidityTopic,sizeof(masterHumidityTopic)) ==0)) {
|
||||
humidTMP = atof(tmp);
|
||||
DPRINT("MASTER clock's humidity:"); DPRINTLN(humidTMP);
|
||||
humid[MASTERhumidPtr] = humidTMP; //overwrite the reserved "master" slot in the shared humid[] array (see MASTERhumidPtr above)
|
||||
}
|
||||
if ((strlen(masterRadarTopic)>0) && (strncmp(topic,masterRadarTopic,sizeof(masterRadarTopic)) ==0)) {
|
||||
// Master clock's radar/PIR presence state, mirrored into mqttRadarON (and radarON) so the rest of the
|
||||
// firmware (night animation, tube power on/off, etc.) reacts to it exactly like a local sensor reading.
|
||||
// We only ever get here if this clock did NOT subscribe to the radar topic due to having its own local
|
||||
// RADAR_PIN (see the RADAR_PIN>=0 guard in onMqttConnected() below) - local hardware always wins.
|
||||
DPRINT("MASTER radar:"); DPRINTLN(tmp[0]);
|
||||
if (tmp[0] == '0') {
|
||||
mqttRadarON = false;
|
||||
radarON = mqttRadarON;
|
||||
}
|
||||
else {
|
||||
mqttRadarON = true;
|
||||
radarON = mqttRadarON;
|
||||
radarLastOn = millis();
|
||||
}
|
||||
DPRINT("mqttRadarON:"); DPRINTLN(mqttRadarON);
|
||||
|
||||
}
|
||||
//mqtt.publish("myPublishTopic", "hello");
|
||||
}
|
||||
|
||||
// Called by ArduinoHA once the broker connection (re)establishes - this is where the SLAVE side actually subscribes
|
||||
// to the MASTER clock's topics. Runs again on every reconnect (not just the first connect), so subscriptions survive
|
||||
// broker restarts/WiFi drops without any extra logic needed in mqttSend().
|
||||
void onMqttConnected() {
|
||||
DPRINTLN("Connected to the broker!");
|
||||
// You can subscribe to custom topic if you need
|
||||
#ifdef USE_MASTER_CLOCK
|
||||
DPRINTLN(F("MASTER_CLOCK enabled! Subscribe to following topics:"));
|
||||
#ifdef USE_MASTER_TEMP
|
||||
if (strlen(masterTemperatureTopic)==0) {
|
||||
DPRINTLN("Please define MASTER_TEMPERATURE_TOPIC in clocks.h'");
|
||||
}
|
||||
else {
|
||||
DPRINT(F("Master temperature topic:")); DPRINTLN(masterTemperatureTopic);
|
||||
mqtt.subscribe(masterTemperatureTopic);
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_MASTER_HUMID
|
||||
if (strlen(masterHumidityTopic)==0) {
|
||||
DPRINTLN("Please define MASTER_HUMIDITY_TOPIC in clocks.h'");
|
||||
}
|
||||
else {
|
||||
DPRINT(F("Master humidity topic:")); DPRINTLN(masterHumidityTopic);
|
||||
mqtt.subscribe(masterHumidityTopic);
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_MASTER_RADAR
|
||||
// WHY: if this clock has its own physical radar/PIR sensor wired to RADAR_PIN, that local reading always
|
||||
// takes priority (see checkTubePowerOnOff() in the core .ino, which prefers digitalRead(RADAR_PIN) over
|
||||
// mqttRadarON when both exist). So there is no point subscribing to the master's radar topic at all in that
|
||||
// case - onMqttMessage()'s radar branch would just be mirrored into a value nothing ever reads.
|
||||
if (RADAR_PIN>=0) {
|
||||
DPRINTLN(F("Local radar enabled! MQTT MASTER radar disabled!"));
|
||||
}
|
||||
else {
|
||||
if (strlen(masterRadarTopic)==0) {
|
||||
DPRINTLN("Please define MASTER_RADAR_TOPIC in clocks.h'");
|
||||
}
|
||||
else {
|
||||
DPRINT(F("Master radar topic:")); DPRINTLN(masterRadarTopic);
|
||||
mqtt.subscribe(masterRadarTopic);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
#endif
|
||||
}
|
||||
|
||||
void setupMqtt() {
|
||||
char mactmp[20];
|
||||
|
||||
WiFi.macAddress(mac);
|
||||
device.setUniqueId(mac, sizeof(mac)); //set device MAC, as unique ID
|
||||
sprintf(mactmp,"%02x%02x%02x%02x%02x%02x",mac[0],mac[1],mac[2],mac[3],mac[4],mac[5]);
|
||||
#if defined(MASTER_TEMPERATURE_TOPIC)
|
||||
strncpy(masterTemperatureTopic,MASTER_TEMPERATURE_TOPIC,sizeof(masterTemperatureTopic));
|
||||
#endif
|
||||
#if defined(MASTER_HUMIDITY_TOPIC)
|
||||
strncpy(masterHumidityTopic,MASTER_HUMIDITY_TOPIC,sizeof(masterHumidityTopic));
|
||||
#endif
|
||||
#if defined(MASTER_RADAR_TOPIC)
|
||||
strncpy(masterRadarTopic,MASTER_RADAR_TOPIC,sizeof(masterRadarTopic));
|
||||
#endif
|
||||
DPRINT("Starting MQTT CLient: "); DPRINT(MQTT_PREFIX); DPRINT("/sensor/"); DPRINTLN(mactmp);
|
||||
|
||||
mqtt.setDiscoveryPrefix(MQTT_PREFIX);
|
||||
device.setName(FW);
|
||||
device.setSoftwareVersion("3.0");
|
||||
if (useTemp>0) {
|
||||
mqttTemp.setUnitOfMeasurement("°C");
|
||||
mqttTemp.setDeviceClass("sensor");
|
||||
mqttTemp.setName("Inside temperature");
|
||||
}
|
||||
if (useHumid>0) {
|
||||
mqttHumid.setUnitOfMeasurement("%");
|
||||
mqttHumid.setDeviceClass("sensor");
|
||||
mqttHumid.setName("Inside humidity");
|
||||
}
|
||||
if (usePress>0) {
|
||||
mqttPress.setUnitOfMeasurement("hPa");
|
||||
mqttPress.setDeviceClass("sensor");
|
||||
mqttPress.setName("Air pressure");
|
||||
}
|
||||
if (useLux>0) {
|
||||
mqttLux.setUnitOfMeasurement("lux");
|
||||
mqttLux.setDeviceClass("sensor");
|
||||
mqttLux.setName("Inside lux");
|
||||
}
|
||||
#if RADAR_PIN>=0
|
||||
mqttRadar.setUnitOfMeasurement("on/off");
|
||||
mqttRadar.setDeviceClass("sensor");
|
||||
mqttRadar.setName("Radar sensor");
|
||||
#endif
|
||||
|
||||
mqtt.onMessage(onMqttMessage);
|
||||
mqtt.onConnected(onMqttConnected);
|
||||
|
||||
IPAddress tmp;
|
||||
tmp.fromString(String(prm.mqttBrokerAddr));
|
||||
if (strlen(prm.mqttBrokerUser)==0)
|
||||
mqtt.begin(tmp);
|
||||
else
|
||||
mqtt.begin(tmp, prm.mqttBrokerUser, prm.mqttBrokerPsw);
|
||||
|
||||
//mqtt.begin(BROKER_ADDR, BROKER_USERNAME, BROKER_PASSWORD);
|
||||
|
||||
device.enableLastWill();
|
||||
#ifdef USE_MASTER_CLOCK
|
||||
DPRINTLN("MQTT MASTER_CLOCK enabled!");
|
||||
DPRINT("MASTER_TEMPERATURE_TOPIC:"); DPRINTLN(masterTemperatureTopic);
|
||||
DPRINT("MASTER_HUMIDITY_TOPIC:"); DPRINTLN(masterHumidityTopic);
|
||||
DPRINT("MASTER_RADAR_TOPIC:"); DPRINTLN(masterRadarTopic);
|
||||
#ifdef USE_MASTER_TEMP
|
||||
DPRINTLN(" - MQTT MASTER_TEMP sensor added!");
|
||||
MASTERtempPtr = useTemp; //remember my ID-s
|
||||
useTemp++; //increase the number of sensors
|
||||
#endif
|
||||
#ifdef USE_MASTER_HUMID
|
||||
DPRINTLN(" - MQTT MASTER_HUMID sensor added!");
|
||||
MASTERhumidPtr = useHumid;
|
||||
useHumid++;
|
||||
#endif
|
||||
#endif
|
||||
}
|
||||
|
||||
void mqttSend() {
|
||||
static unsigned long lastRun = millis();
|
||||
//if (!mqtt.isConnected()) return;
|
||||
mqtt.loop();
|
||||
|
||||
if ((millis() - lastRun) < prm.mqttBrokerRefresh*1000) return;
|
||||
lastRun = millis();
|
||||
if (mqtt.isConnected()) DPRINT("MQTT Connected.");
|
||||
else {
|
||||
DPRINTLN("MQTT Failed to connect.");
|
||||
return;
|
||||
}
|
||||
DPRINT("MQTT send:");
|
||||
if (useTemp>0) {mqttTemp.setValue(temperature[0]+ prm.corrT0); DPRINT(temperature[0]+ prm.corrT0); DPRINT(", ");}
|
||||
if (useHumid>0) {mqttHumid.setValue(humid[0]+ prm.corrH0); DPRINT(humid[0]+ prm.corrH0); DPRINT(", ");}
|
||||
if (usePress>0) {mqttPress.setValue(pressur[0]); DPRINT(pressur[0]); DPRINT(", ");}
|
||||
if (useLux>0) {mqttLux.setValue(lx); DPRINT(lx); DPRINT(", ");}
|
||||
#if RADAR_PIN>=0
|
||||
mqttRadar.setValue(radarON); DPRINT(radarON);
|
||||
#endif
|
||||
DPRINTLN(" ");
|
||||
}
|
||||
|
||||
#else
|
||||
void mqttSend() {}
|
||||
#endif
|
||||
@@ -0,0 +1,260 @@
|
||||
// ============================================================================
|
||||
// MULE_V2 driver
|
||||
//
|
||||
// Drives the "Mule V2" ESP32 board: 6 x SA40 7-segment LED digit modules,
|
||||
// used by CLOCK_34 ("Mule V2 ESP32, board, 6 x SA40 LED Display", see
|
||||
// clocks.h around line 972). This is a BCD-multiplexed 7-segment LED clock,
|
||||
// NOT a Nixie/VFD tube driver, even though several comments/variable names
|
||||
// below (DP_PIN, "Nixie clock", tube terminology) are inherited boilerplate
|
||||
// copied from the tube-driver files this codebase is built from.
|
||||
//
|
||||
// Hardware / wiring protocol (see clocks.h CLOCK_34 block for actual pins):
|
||||
// - ABCDPins[4] -> CD4543 BCD-to-7-segment latch/decoder/driver IC.
|
||||
// 4 bits carry the digit VALUE (0-9) for whichever
|
||||
// digit position is currently selected; the 4543
|
||||
// decodes it to segments and drives the LED digit
|
||||
// directly (latching, so no extra data hold time is
|
||||
// required).
|
||||
// - digitEnablePins[3] -> CD4028 BCD-to-Decimal decoder ("A,B,C" inputs).
|
||||
// Only 3 bits are used (not per-digit enable lines!)
|
||||
// to binary-encode which ONE of the 6 SA40 digits
|
||||
// is active right now; the 4028's decoded output
|
||||
// lines 1-6 are wired to the 6 digit commons, output
|
||||
// 0 and 7 are intentionally left unconnected and are
|
||||
// used as a "blank all digits" state (see writeDisplay()).
|
||||
// - This is a classic BCD multiplex scheme: only one digit is illuminated
|
||||
// at any instant, cycling fast enough (via hardware timer ISR) that
|
||||
// persistence of vision makes all 6 digits appear lit simultaneously.
|
||||
// - Digit brightness is done by PWM-ing the "on" time of each digit within
|
||||
// its multiplex slot (see PWMrefresh/PWM_min/PWM_max and PWMtimeBrightness
|
||||
// below), not by varying LED current.
|
||||
//
|
||||
// Board also has optional peripherals declared in clocks.h but NOT touched
|
||||
// by this file directly: NeoPixel backlight (USE_NEOPIXEL/NEOPIXEL_PIN) and
|
||||
// a Dallas temperature sensor pin - both handled by the core .ino.
|
||||
// ============================================================================
|
||||
#ifdef MULE_V2
|
||||
char tubeDriver[] = "MULE_V2";
|
||||
|
||||
//define here the digit enable pins from 4 to 8
|
||||
|
||||
//byte digitEnablePins[] = {15,13,12,14}; //for example... But SET in clocks.h !!!
|
||||
//byte ABCDPins[4] = {2,4,5,0};
|
||||
//int DP_PIN = -1; // decimalPoint inside Nixie tube, set -1, if not used!
|
||||
|
||||
int maxDigits = 6; //sizeof(digitEnablePins);
|
||||
// NOTE: maxDigits (6) is the number of physical SA40 digits, NOT the size of
|
||||
// digitEnablePins (which is only 3 - see CLOCK_34 in clocks.h: {5,16,23}).
|
||||
// digitEnablePins holds the 3 CD4028 select lines, so its correct element
|
||||
// count is always 3 regardless of maxDigits. Do not assume they are equal
|
||||
// (setup_pins() below used to get this wrong - now fixed, see the note there).
|
||||
int DRAM_ATTR maxDig = maxDigits; //memory variable version
|
||||
// Local DRAM_ATTR copy of maxDigits, used inside the IRAM_ATTR ISR
|
||||
// (writeDisplay). Keeping a dedicated DRAM-resident copy guarantees the
|
||||
// value stays reachable from the ISR even in situations where flash-cache
|
||||
// backed data access would be unsafe (e.g. during flash/EEPROM writes).
|
||||
|
||||
//byte convert[] = {1,0,9,8,7,6,5,4,3,2}; //tube pin conversion, if needed (e.g. for a non-standard tube pin layout)
|
||||
int DRAM_ATTR PWMrefresh=11000; //msec, Multiplex time period. Greater value => slower multiplex frequency
|
||||
int DRAM_ATTR PWM_min = 1000;
|
||||
int DRAM_ATTR PWM_max = 10000;
|
||||
//int DRAM_ATTR PWMtiming[11] = {0,1000,2000,3000,4000,5000,6000,7000,8000,9000,10000};
|
||||
// PWMrefresh is the total time budget (in hardware-timer ticks) for one full
|
||||
// digit's multiplex slot: part of it is "digit ON" (length set by
|
||||
// PWMtimeBrightness, scaled between PWM_min/PWM_max according to
|
||||
// brightness/ambient light) and the remainder is "digit OFF" (blanking), see
|
||||
// case 2 in writeDisplay(). This is how per-digit brightness/dimming is
|
||||
// achieved without a dedicated PWM peripheral per digit.
|
||||
|
||||
#if defined(ESP32)
|
||||
#else
|
||||
#error "Only ESP32 board is supported by this driver!"
|
||||
#endif
|
||||
|
||||
void setup_pins() {
|
||||
char tmp[30];
|
||||
|
||||
DPRINTLN("Nixie clock - setup pins - Multiplex BCD mode (MULE_V2)...");
|
||||
// "Nixie clock" text is inherited boilerplate from the tube-driver
|
||||
// templates this file was cloned from; this board drives LED digits.
|
||||
DPRINTLN("BCD digitEnablePins: ");
|
||||
// FIXED (was "i<maxDigits", i.e. 0..5): digitEnablePins[] is declared in clocks.h with only 3
|
||||
// elements ({5,16,23} - the CD4028 3-bit select lines, see header comment above), not one entry
|
||||
// per digit, so looping to maxDigits(=6) read past the end of the array for i=3,4,5 and called
|
||||
// pinMode()/regPin() with whatever garbage byte followed it in memory. Every other place in this
|
||||
// file that iterates digitEnablePins (writeDisplay(), clearTubes()) already correctly uses
|
||||
// sizeof(digitEnablePins) (=3) instead of maxDigits - this loop now matches them.
|
||||
for (int i=0;i<(int)sizeof(digitEnablePins);i++) {
|
||||
pinMode(digitEnablePins[i], OUTPUT);
|
||||
sprintf(tmp,"digitEnPin[%d]",i);
|
||||
regPin(digitEnablePins[i],tmp);
|
||||
}
|
||||
DPRINTLN(" ");
|
||||
DPRINTLN("ABCDPins: ");
|
||||
// ABCDPins[4]: the 4 BCD data lines feeding the CD4543 7-segment decoder.
|
||||
for (int i=0;i<4;i++) {
|
||||
pinMode(ABCDPins[i], OUTPUT);
|
||||
sprintf(tmp,"Pin[%c]",char('A'+i));
|
||||
regPin(ABCDPins[i],tmp);
|
||||
}
|
||||
DPRINTLN(" ");
|
||||
// DP_PIN (decimal point) is not wired for the Mule V2 board (defaults to
|
||||
// -1 in clocks.h for CLOCK_34), so this whole block is compiled out for
|
||||
// this board; kept for source compatibility with boards that do use it.
|
||||
#if DP_PIN>=0
|
||||
pinMode(DP_PIN,OUTPUT);
|
||||
DPRINT("DP_PIN:"); DPRINTLN(DP_PIN);
|
||||
#endif
|
||||
startTimer(); // arms the hardware timer that drives writeDisplay() as an ISR
|
||||
}
|
||||
|
||||
void IRAM_ATTR writeDisplay(){ //void IRAM_ATTR writeDisplay(){
|
||||
// Timer-interrupt multiplex refresh handler. Runs once per "phase" of a
|
||||
// single digit's multiplex slot (see state machine below) and re-arms the
|
||||
// hardware timer itself with the next phase's duration - i.e. this is a
|
||||
// self-rescheduling one-shot timer, not a fixed-period tick. Because it is
|
||||
// IRAM_ATTR it (and everything it touches, hence the DRAM_ATTR statics) must
|
||||
// not depend on code/data that lives in flash, since it can fire while the
|
||||
// SPI flash cache is disabled.
|
||||
|
||||
static DRAM_ATTR byte pos = 0;
|
||||
static DRAM_ATTR byte state=0;
|
||||
static DRAM_ATTR int timer = PWMrefresh;
|
||||
static DRAM_ATTR int num,brightness;
|
||||
static DRAM_ATTR byte DPpos;
|
||||
static DRAM_ATTR int PWMtimeBrightness=PWM_min;
|
||||
|
||||
if (EEPROMsaving) { //stop refresh while an EEPROM write is in progress!
|
||||
return;
|
||||
}
|
||||
|
||||
portENTER_CRITICAL_ISR(&timerMux);
|
||||
noInterrupts();
|
||||
|
||||
intCounter++;
|
||||
brightness = displayON ? prm.dayBright : prm.nightBright;
|
||||
if (brightness>MAXBRIGHTNESS) brightness = MAXBRIGHTNESS; //only for safety
|
||||
|
||||
timer = PWMrefresh;
|
||||
|
||||
switch (state) { //state machine...
|
||||
// 4-phase cycle per digit position, advancing `pos` only in case 0:
|
||||
// 0: decide which digit to show next (advance pos, recompute
|
||||
// brightness once per full 6-digit sweep) and pick ON-phase timing
|
||||
// 1: (only reached when animMask[pos]>0, i.e. a digit-change animation
|
||||
// is in progress) show the NEW character for the remaining
|
||||
// fraction of the ON time, crossfading old->new digit by splitting
|
||||
// the ON slot into two sub-phases weighted by animMask (0..20)
|
||||
// 2: blank phase - digit output stays on the last-set value but the
|
||||
// remaining PWMrefresh budget elapses with the digit dark, giving
|
||||
// the dimming effect
|
||||
// 3: sentinel reached via case 2's fallthrough-by-timer only, forces
|
||||
// the blank branch below and resets state to 0 next call
|
||||
case 0:
|
||||
pos++; if (pos>maxDig-1) {
|
||||
pos = 0; //go to the first tube
|
||||
if (autoBrightness && displayON) {
|
||||
PWMtimeBrightness = max(PWM_min,PWM_max*lx/MAXIMUM_LUX); //ambient-light-driven auto brightness
|
||||
}
|
||||
else
|
||||
PWMtimeBrightness = max(PWM_min,PWM_max*brightness/MAXBRIGHTNESS); //manual day/night brightness setting
|
||||
}
|
||||
if (animMask[pos] > 0) { //Animation?
|
||||
num = oldDigit[pos]; //show old character
|
||||
timer = (PWMtimeBrightness * (20-animMask[pos]))/20;
|
||||
state = 1; //next state is: show newDigit
|
||||
}
|
||||
else {
|
||||
num = digit[pos]; //show active character
|
||||
timer = PWMtimeBrightness;
|
||||
state = 2; //next state is: BLANK display
|
||||
}
|
||||
break;
|
||||
case 1: //show new character, if animation
|
||||
num = newDigit[pos];
|
||||
timer = (PWMtimeBrightness * animMask[pos])/20;
|
||||
state = 2; //default next state is: BLANK display
|
||||
break;
|
||||
case 2: //blank display
|
||||
timer = PWMrefresh-PWMtimeBrightness;
|
||||
state = 3;
|
||||
break;
|
||||
} //end switch
|
||||
if (timer<500) timer = 500; //safety only... (avoid re-arming the HW timer with an unreasonably short/zero alarm)
|
||||
|
||||
// if ((pos>0) && (num<=9)) num = convert[num]; //tube character conversion, if needed... (maybe bad pin numbering)
|
||||
|
||||
// Blank the currently-selected digit when: display globally off/zero
|
||||
// brightness, we're in the "blank" phase (state==3), the character code
|
||||
// isn't a real digit (10=BLANK,11=dash,12=DP,13+=custom glyph - none of
|
||||
// these have a 7-segment BCD encoding this driver understands), or the
|
||||
// radar/PIR presence sensor says nobody is around.
|
||||
if ((brightness == 0) || (state == 3) || (num >9) || (!radarON)) { //blank digit
|
||||
state = 0;
|
||||
for (int i=0;i<sizeof(digitEnablePins);i++)
|
||||
digitalWrite(digitEnablePins[i],HIGH); //switch off anode
|
||||
// All 3 CD4028 select lines HIGH = binary 111 = decoder output 7,
|
||||
// which is intentionally NOT wired to any digit common (only outputs
|
||||
// 1-6 are used, see file header) - this is how "no digit selected"
|
||||
// is achieved without a separate enable/blank signal.
|
||||
#if DP_PIN >=0
|
||||
digitalWrite(DP_PIN,LOW);
|
||||
#endif
|
||||
}
|
||||
else {
|
||||
// Push the digit VALUE (0-9) as 4 bits onto the CD4543 (ABCDPins),
|
||||
// which latches and decodes it to 7-segment outputs for whichever
|
||||
// digit is currently selected below.
|
||||
for (int i=0;i<4;i++) {digitalWrite(ABCDPins[i],num & 1<<i); }
|
||||
// Binary-encode (pos+1) across the 3 CD4028 select lines to choose
|
||||
// which of the 6 SA40 digits is active (decoder outputs 1..6); "+1"
|
||||
// is because output 0 is reserved/unused (see header comment).
|
||||
for (int i=0;i<sizeof(digitEnablePins);i++) {digitalWrite(digitEnablePins[i],(pos+1) & 1<<i);} //switch on the new digit
|
||||
|
||||
#if DP_PIN >=0
|
||||
if (LEFTDECIMAL) DPpos = min(maxDig-1,pos+1); else DPpos = pos;
|
||||
if (digitDP[DPpos]) digitalWrite(DP_PIN,HIGH); //switch ON decimal point, if needed
|
||||
#endif
|
||||
}
|
||||
|
||||
// COLON_PIN / DECIMALPOINT_PIN are both -1 (unused) for the Mule V2 board
|
||||
// (CLOCK_34 in clocks.h does not define them), so these blocks compile out
|
||||
// entirely on this board; kept for source compatibility with other boards
|
||||
// that reuse this driver file.
|
||||
#if COLON_PIN >= 0
|
||||
if (num==10) digitalWrite(COLON_PIN,LOW); // Colon pin OFF
|
||||
else digitalWrite(COLON_PIN,colonBlinkState); // Blink colon pin
|
||||
#endif
|
||||
|
||||
#if DECIMALPOINT_PIN >=0
|
||||
if (num==10) {digitalWrite(DECIMALPOINT_PIN,LOW);}
|
||||
else {digitalWrite(DECIMALPOINT_PIN,decimalpointON); }
|
||||
#endif
|
||||
|
||||
portEXIT_CRITICAL_ISR(&timerMux);
|
||||
//ESP32timer = timerBegin(0, PRESCALER, true); //set prescaler, true = edge generated signal
|
||||
//timerAttachInterrupt(ESP32timer, &writeDisplay, true);
|
||||
timerAlarmWrite(ESP32timer, timer, true); // re-arm the one-shot HW timer for the next phase's duration (see state machine above)
|
||||
timerAlarmEnable(ESP32timer);
|
||||
interrupts();
|
||||
}
|
||||
|
||||
void clearTubes() {
|
||||
// Blank all digits immediately (outside the ISR/multiplex cycle), using
|
||||
// the same "select CD4028 output 7" trick as the blanking branch in
|
||||
// writeDisplay() above.
|
||||
for (int i=0;i<sizeof(digitEnablePins);i++) digitalWrite(digitEnablePins[i],HIGH);
|
||||
#if (DP_PIN>=0)
|
||||
digitalWrite(DP_PIN,LOW);
|
||||
#endif
|
||||
}
|
||||
|
||||
// Intentionally empty: unlike non-multiplexed drivers, this board's display
|
||||
// is refreshed continuously by the writeDisplay() timer ISR, which reads the
|
||||
// live digit[]/newDigit[]/animMask[] state on every phase. Callers elsewhere
|
||||
// in the core .ino invoke writeDisplaySingle() to force an immediate
|
||||
// blocking redraw after changing digit values (e.g. during animations); for
|
||||
// MULE_V2 no explicit action is needed since the next ISR tick (at most
|
||||
// PWM_min..PWMrefresh ticks away) will already pick up the new state.
|
||||
void writeDisplaySingle() {}
|
||||
#endif
|
||||
@@ -0,0 +1,194 @@
|
||||
// ============================================================================
|
||||
// Nixie driver: shared/multiplexed BCD-to-decimal decoder (SN74141 / 7441 / K155ID1)
|
||||
// ----------------------------------------------------------------------------
|
||||
// Hardware: ONE (or a few) 74141-family BCD driver chip(s) whose 4 BCD inputs
|
||||
// (ABCDPins[4]) are wired in parallel to ALL Nixie tubes, while each tube's
|
||||
// high-voltage anode is switched individually through a transistor connected
|
||||
// to digitEnablePins[]. Only one digitEnablePin is HIGH at any instant, so
|
||||
// only that tube's cathodes actually light up with the BCD value currently
|
||||
// present on ABCDPins - this is classic time-division multiplexing: the
|
||||
// digits are shown one tube at a time, fast enough (via the hardware timer
|
||||
// ISR below) that persistence of vision makes them look simultaneously lit.
|
||||
// This trades tube count for 74141-chip count (cheap: 1 decoder for N tubes)
|
||||
// at the cost of average brightness per tube (each tube is only ON 1/N of
|
||||
// the time) and of needing careful ISR timing to avoid visible flicker.
|
||||
//
|
||||
// Platform: ESP8266 ONLY (uses the 8266 hardware timer1 API: timer1_write /
|
||||
// ICACHE_RAM_ATTR). For the ESP32 port of the same multiplex strategy see
|
||||
// multiplex74141_ESP32.ino (uses timerAlarmWrite/IRAM_ATTR instead).
|
||||
//
|
||||
// Used by these clocks.h configs (search for "#define MULTIPLEX74141"):
|
||||
// CLOCK_1, CLOCK_2, CLOCK_20, CLOCK_21, CLOCK_22
|
||||
// digitEnablePins[], ABCDPins[4] and DP_PIN are actually defined per-CLOCK_NN
|
||||
// in clocks.h (the declarations commented out below are just examples of the
|
||||
// shape clocks.h must provide).
|
||||
// ============================================================================
|
||||
#ifdef MULTIPLEX74141 //Nixie driver for 8266 only!
|
||||
char tubeDriver[] = "MULTIPLEX74141";
|
||||
//define here the digit enable pins from 4 to 8
|
||||
//byte digitEnablePins[] = {15,13,12,14}; //for example... But SET in clocks.h !!!
|
||||
//byte ABCDPins[4] = {2,4,5,0};
|
||||
//#define DP_PIN -1 // decimalPoint inside Nixie tube, set -1, if not used!
|
||||
|
||||
#if defined(ESP8266)
|
||||
#else
|
||||
#error "Board is not supported! For ESP32 use MULTIPLEX74141_ESP32 !"
|
||||
#endif
|
||||
|
||||
int maxDigits = sizeof(digitEnablePins); //digitEnablePins is byte[], so sizeof == element count
|
||||
int maxDig = maxDigits; //memory variable version (kept as a separate copy so it can be read cheaply from the ISR)
|
||||
|
||||
//byte convert[] = {1,0,9,8,7,6,5,4,3,2}; //tube pin conversion, is needed (for example: bad tube pin layout)
|
||||
// NOTE: these "PWM..." values are NOT milliseconds despite the comment - they are raw
|
||||
// timer1 tick counts (timer1 on ESP8266 runs at TIM_DIV16, i.e. 5MHz/16 ~= 312.5kHz,
|
||||
// so 1 tick ~= 3.2us). They set how long (in ticks) each tube stays lit / blanked
|
||||
// within one full multiplex refresh cycle, which is what actually controls brightness.
|
||||
int PWMrefresh=11000; //msec, Multiplex time period. Greater value => slower multiplex frequency
|
||||
int PWM_min = 1000;
|
||||
int PWM_max = 10000;
|
||||
//int PWMtiming[11] = {0,1000,2000,3000,4000,5000,6000,7000,8000,9000,10000};
|
||||
|
||||
void setup_pins() {
|
||||
char tmp[30];
|
||||
|
||||
DPRINTLN("Nixie clock - setup pins - Multiplex 74141 mode...");
|
||||
DPRINTLN("digitEnablePins: ");
|
||||
for (int i=0;i<maxDigits;i++) {
|
||||
pinMode(digitEnablePins[i], OUTPUT);
|
||||
sprintf(tmp,"digitEnPin[%d]",i);
|
||||
regPin(digitEnablePins[i],tmp);
|
||||
}
|
||||
DPRINTLN(" ");
|
||||
|
||||
DPRINTLN("ABCDPins: ");
|
||||
for (int i=0;i<4;i++) {
|
||||
pinMode(ABCDPins[i], OUTPUT);
|
||||
sprintf(tmp,"Pin[%c]",char('A'+i));
|
||||
regPin(ABCDPins[i],tmp);
|
||||
}
|
||||
DPRINTLN(" ");
|
||||
#if DP_PIN>=0
|
||||
pinMode(DP_PIN,OUTPUT); regPin(DP_PIN,"DP_PIN");
|
||||
DPRINT("DP_PIN:"); DPRINTLN(DP_PIN);
|
||||
#endif
|
||||
startTimer();
|
||||
}
|
||||
|
||||
void ICACHE_RAM_ATTR writeDisplay(){ //https://circuits4you.com/2018/01/02/esp8266-timer-ticker-example/
|
||||
// ESP8266 timer1 is a SINGLE-SHOT (TIM_SINGLE) timer, so unlike the ESP32
|
||||
// version, it must be explicitly re-armed with timer1_write(timer) at the
|
||||
// END of every single call below, otherwise the ISR would never fire again.
|
||||
static byte pos = 0;
|
||||
static volatile byte state=0;
|
||||
static int timer = PWMrefresh;
|
||||
static int num,brightness;
|
||||
static byte DPpos;
|
||||
static int PWMtimeBrightness=PWM_min;
|
||||
|
||||
if (EEPROMsaving) { //stop refresh, while EEPROM write is in progress!
|
||||
digitalWrite(digitEnablePins[pos],LOW); //blank current tube so it doesn't stay lit "stuck" during the (possibly long) flash write
|
||||
timer1_write(PWMrefresh); //re-arm with a "slow idle" period; state machine resumes normally once EEPROMsaving clears
|
||||
return;
|
||||
}
|
||||
|
||||
intCounter++;
|
||||
brightness = displayON ? prm.dayBright : prm.nightBright;
|
||||
if (brightness>MAXBRIGHTNESS) brightness = MAXBRIGHTNESS; //only for safety
|
||||
|
||||
timer = PWMrefresh;
|
||||
|
||||
// 4-state per-tube state machine, re-entered once per ISR call:
|
||||
// state 0: advance to next tube position; if animating, show the OLD digit for a
|
||||
// fraction of the slot proportional to animMask[pos] (0..20 => wipe progress)
|
||||
// state 1: show the NEW digit for the remaining fraction of the slot (only reached
|
||||
// when animating)
|
||||
// state 2: blank the tube for the rest of the PWMrefresh period - this "off time"
|
||||
// is what implements brightness dimming (shorter ON / longer OFF = dimmer)
|
||||
// state 3: transient marker consumed immediately below to force digitalWrite LOW
|
||||
switch (state) { //state machine...
|
||||
case 0:
|
||||
pos++; if (pos>maxDig-1) {
|
||||
pos = 0; //go to the first tube
|
||||
// Brightness/lux is only re-sampled once per FULL refresh cycle (when wrapping
|
||||
// back to tube 0), not on every tube - this keeps brightness visually uniform
|
||||
// across all tubes within one cycle even if lx/brightness changes mid-cycle.
|
||||
if (autoBrightness && displayON) {
|
||||
PWMtimeBrightness = max(PWM_min,PWM_max*lx/MAXIMUM_LUX);
|
||||
}
|
||||
else
|
||||
PWMtimeBrightness = max(PWM_min,PWM_max*brightness/MAXBRIGHTNESS);
|
||||
}
|
||||
|
||||
if (animMask[pos] > 0) { //Animation?
|
||||
num = oldDigit[pos]; //show old character
|
||||
timer = (PWMtimeBrightness * (20-animMask[pos]))/20; //animMask range is 0..20; older digit gets the (20-mask) share of the ON time
|
||||
state = 1; //next state is: show newDigit
|
||||
}
|
||||
else {
|
||||
num = digit[pos]; //show active character
|
||||
timer = PWMtimeBrightness;
|
||||
state = 2; //next state is: BLANK display
|
||||
}
|
||||
break;
|
||||
case 1: //show new character, if animation
|
||||
num = newDigit[pos];
|
||||
timer = (PWMtimeBrightness * animMask[pos])/20; //new digit gets the mask/20 share - together with case 0 this "wipes" old->new within one tube's ON slot
|
||||
state = 2; //default next state is: BLANK display
|
||||
break;
|
||||
case 2: //blank display
|
||||
timer = PWMrefresh-PWMtimeBrightness; //remaining OFF portion of the cycle = dimming
|
||||
state = 3;
|
||||
break;
|
||||
} //end switch
|
||||
if (timer<500) timer = 500; //safety only... (a too-short timer1 reload can miss the interrupt entirely on 8266)
|
||||
|
||||
// if ((pos>0) && (num<=9)) num = convert[num]; //tube character conversion, if needed... (maybe bad pin numbering)
|
||||
|
||||
// num>9 (BLANK=10, MINUS=11, DP glyph=12, ...) intentionally falls through to "blank digit":
|
||||
// the 74141 itself also blanks all outputs for BCD inputs 10-15, so this check is really a
|
||||
// software-side shortcut that avoids driving ABCDPins at all for non-numeric glyphs.
|
||||
if ((brightness == 0) || (state == 3) || (num >9) || (!radarON)) { //blank digit
|
||||
state = 0;
|
||||
digitalWrite(digitEnablePins[pos],LOW); //switch off anode
|
||||
#if DP_PIN >=0
|
||||
digitalWrite(DP_PIN,LOW);
|
||||
#endif
|
||||
}
|
||||
else {
|
||||
for (int i=0;i<4;i++) {digitalWrite(ABCDPins[i],num & 1<<i); } //push BCD bits to the shared 74141 inputs (LSB first, bit i <-> ABCDPins[i])
|
||||
digitalWrite(digitEnablePins[pos],HIGH); //switch on the new digit
|
||||
#if DP_PIN >=0
|
||||
// Some tube types (Z574M) have their decimal-point cathode wired so it visually
|
||||
// sits to the LEFT of the printed digit; LEFTDECIMAL compensates by lighting the
|
||||
// DP together with the NEXT tube position instead of the current one.
|
||||
if (LEFTDECIMAL) DPpos = min(maxDig-1,pos+1); else DPpos = pos;
|
||||
if (digitDP[DPpos]) digitalWrite(DP_PIN,HIGH); //switch ON decimal point, if needed
|
||||
#endif
|
||||
}
|
||||
|
||||
#if COLON_PIN >= 0
|
||||
if (num==10) digitalWrite(COLON_PIN,LOW); // Colon pin OFF
|
||||
else digitalWrite(COLON_PIN,colonBlinkState); // Blink colon pin
|
||||
#endif
|
||||
|
||||
#if DECIMALPOINT_PIN >=0
|
||||
if (num==10) {digitalWrite(DECIMALPOINT_PIN,LOW);}
|
||||
else {digitalWrite(DECIMALPOINT_PIN,decimalpointON); }
|
||||
#endif
|
||||
|
||||
timer1_write(timer); //re-arm the single-shot timer1 for the next phase (see note at top of function)
|
||||
}
|
||||
|
||||
void clearTubes() {
|
||||
for (int i=0;i<maxDig;i++) digitalWrite(digitEnablePins[i],LOW); //drop every digit-enable line so no tube anode is active
|
||||
#if DP_PIN >= 0
|
||||
digitalWrite(DP_PIN,LOW);
|
||||
#endif
|
||||
}
|
||||
|
||||
// Not implemented for this driver: the multiplex ISR (writeDisplay) already refreshes
|
||||
// continuously on its own, so there's no separate "blocking single write" path here
|
||||
// (unlike the no_multiplex*.ino drivers, which are not otherwise refreshed by an ISR loop
|
||||
// in the same way and rely more on this being a real no-op too, kept only for interface parity).
|
||||
void writeDisplaySingle() {}
|
||||
#endif
|
||||
@@ -0,0 +1,215 @@
|
||||
// ============================================================================
|
||||
// Nixie driver: shared/multiplexed BCD-to-decimal decoder (SN74141 / 7441 / K155ID1)
|
||||
// ----------------------------------------------------------------------------
|
||||
// ESP32 port of multiplex74141.ino - identical hardware/multiplex strategy
|
||||
// (one shared BCD decoder driving all tubes, one digitEnablePins[] transistor
|
||||
// per tube selects which tube's cathodes are actually connected at any given
|
||||
// instant; see the header comment in multiplex74141.ino for the full write-up
|
||||
// of the wiring/timing rationale). The two files are kept separate rather
|
||||
// than merged behind #ifdefs because the ESP8266 and ESP32 hardware-timer
|
||||
// APIs are different enough (timer1_write single-shot vs. timerAlarmWrite
|
||||
// auto-reload, ICACHE_RAM_ATTR vs IRAM_ATTR/DRAM_ATTR placement, and the
|
||||
// dual-core critical-section requirements on ESP32) that sharing one file
|
||||
// would need almost as many #ifdefs as duplicating it.
|
||||
//
|
||||
// Platform: ESP32 ONLY.
|
||||
// Used by these clocks.h configs (search for "#define MULTIPLEX74141_ESP32"):
|
||||
// CLOCK_23, CLOCK_23A, CLOCK_23B, CLOCK_31, CLOCK_33, CLOCK_43, CLOCK_44, CLOCK_45
|
||||
// digitEnablePins[], ABCDPins[4] and DP_PIN are defined per-CLOCK_NN in clocks.h.
|
||||
// ============================================================================
|
||||
#ifdef MULTIPLEX74141_ESP32
|
||||
char tubeDriver[] = "MULTIPLEX74141_ESP32";
|
||||
|
||||
//define here the digit enable pins from 4 to 8
|
||||
|
||||
//byte digitEnablePins[] = {15,13,12,14}; //for example... But SET in clocks.h !!!
|
||||
//byte ABCDPins[4] = {2,4,5,0};
|
||||
//int DP_PIN = -1; // decimalPoint inside Nixie tube, set -1, if not used!
|
||||
|
||||
int maxDigits = sizeof(digitEnablePins); //digitEnablePins is byte[], so sizeof == element count
|
||||
int DRAM_ATTR maxDig = maxDigits; //memory variable version; DRAM_ATTR forces it into internal RAM (never flash-cached) so the IRAM ISR can safely read it
|
||||
|
||||
//byte convert[] = {1,0,9,8,7,6,5,4,3,2}; //tube pin conversion, is needed (for example: bad tube pin layout)
|
||||
// NOTE: despite the "msec" comment these are raw hardware-timer tick counts (see
|
||||
// PRESCALER in the core .ino / startTimer()), not real milliseconds - they only
|
||||
// control the ON/OFF ratio of each tube's multiplex slot (= brightness).
|
||||
int DRAM_ATTR PWMrefresh=11000; //msec, Multiplex time period. Greater value => slower multiplex frequency
|
||||
int DRAM_ATTR PWM_min = 1000;
|
||||
int DRAM_ATTR PWM_max = 10000;
|
||||
//int DRAM_ATTR PWMtiming[11] = {0,1000,2000,3000,4000,5000,6000,7000,8000,9000,10000};
|
||||
|
||||
#if defined(ESP32)
|
||||
#else
|
||||
#error "Only ESP32 board is supported by this driver!"
|
||||
#endif
|
||||
|
||||
void setup_pins() {
|
||||
char tmp[30];
|
||||
|
||||
DPRINTLN("Nixie clock - setup pins - Multiplex 74141 mode...");
|
||||
DPRINTLN("digitEnablePins: ");
|
||||
for (int i=0;i<maxDigits;i++) {
|
||||
pinMode(digitEnablePins[i], OUTPUT);
|
||||
sprintf(tmp,"digitEnPin[%d]",i);
|
||||
regPin(digitEnablePins[i],tmp);
|
||||
}
|
||||
DPRINTLN(" ");
|
||||
DPRINTLN("ABCDPins: ");
|
||||
for (int i=0;i<4;i++) {
|
||||
pinMode(ABCDPins[i], OUTPUT);
|
||||
sprintf(tmp,"Pin[%c]",char('A'+i));
|
||||
regPin(ABCDPins[i],tmp);
|
||||
}
|
||||
DPRINTLN(" ");
|
||||
#if DP_PIN>=0
|
||||
pinMode(DP_PIN,OUTPUT);
|
||||
// POSSIBLE BUG: no regPin(DP_PIN,"DP_PIN") call here, unlike the ESP8266 sibling
|
||||
// (multiplex74141.ino) and the other drivers in this family. DP_PIN will therefore
|
||||
// be silently missing from the "USED CLOCK PINS" diagnostic list even though it is
|
||||
// actively driven below in writeDisplay().
|
||||
DPRINT("DP_PIN:"); DPRINTLN(DP_PIN);
|
||||
#endif
|
||||
startTimer();
|
||||
}
|
||||
|
||||
void IRAM_ATTR writeDisplay(){ //void IRAM_ATTR writeDisplay(){
|
||||
// All static locals are DRAM_ATTR: code and read-only data placed by the linker in
|
||||
// flash can become briefly inaccessible while the ESP32 is reading/writing SPI flash
|
||||
// (e.g. Wi-Fi, EEPROM/OTA). An IRAM_ATTR ISR is safe to run during that time, but only
|
||||
// if every byte of data it touches - not just the function code - is forced into
|
||||
// internal DRAM instead of flash-backed memory. Hence DRAM_ATTR on every static here
|
||||
// and on the module-level PWM constants above.
|
||||
|
||||
static DRAM_ATTR byte pos = 0;
|
||||
static DRAM_ATTR byte state=0;
|
||||
static DRAM_ATTR int timer = PWMrefresh;
|
||||
static DRAM_ATTR int num,brightness;
|
||||
static DRAM_ATTR byte DPpos;
|
||||
static DRAM_ATTR int PWMtimeBrightness=PWM_min;
|
||||
|
||||
if (EEPROMsaving) { //stop refresh, while EEPROM write is in progress!
|
||||
// Note: unlike the ESP8266 sibling, this does NOT re-arm the hardware timer before
|
||||
// returning. That's fine here because startTimer() configured ESP32timer with
|
||||
// autoreload=true (see timerAlarmWrite(...,true) in startTimer()), so the timer
|
||||
// keeps firing on its own at whatever period was last programmed; only the display
|
||||
// content update is skipped while EEPROM is being written.
|
||||
return;
|
||||
}
|
||||
|
||||
// ESP32 is dual-core: portENTER/EXIT_CRITICAL_ISR takes a spinlock so the other core
|
||||
// (e.g. running Wi-Fi/loop() code) cannot preempt this ISR mid-update of shared state,
|
||||
// while noInterrupts()/interrupts() additionally blocks other interrupt sources on
|
||||
// THIS core. Both are used together defensively so the digitalWrite() sequence below
|
||||
// (which must stay atomic - partial BCD + digit-enable writes would flash a wrong
|
||||
// digit) can't be interleaved with anything else.
|
||||
portENTER_CRITICAL_ISR(&timerMux);
|
||||
noInterrupts();
|
||||
|
||||
intCounter++;
|
||||
brightness = displayON ? prm.dayBright : prm.nightBright;
|
||||
if (brightness>MAXBRIGHTNESS) brightness = MAXBRIGHTNESS; //only for safety
|
||||
|
||||
timer = PWMrefresh;
|
||||
|
||||
// Same 4-state per-tube multiplex/animation state machine as the ESP8266 version
|
||||
// (multiplex74141.ino) - see the detailed comments there. In short: state 0 advances
|
||||
// to the next tube and (if a wipe animation is active for it) shows the OLD digit for
|
||||
// part of the slot; state 1 shows the NEW digit for the rest; state 2 blanks the tube
|
||||
// for the remainder of the refresh period (this ON/OFF ratio is what implements
|
||||
// brightness); state 3 is a one-tick marker consumed immediately below.
|
||||
switch (state) { //state machine...
|
||||
case 0:
|
||||
pos++; if (pos>maxDig-1) {
|
||||
pos = 0; //go to the first tube
|
||||
// Brightness/lux is only resampled once per full refresh cycle (all tubes), not per-tube.
|
||||
if (autoBrightness && displayON) {
|
||||
PWMtimeBrightness = max(PWM_min,PWM_max*lx/MAXIMUM_LUX);
|
||||
}
|
||||
else
|
||||
PWMtimeBrightness = max(PWM_min,PWM_max*brightness/MAXBRIGHTNESS);
|
||||
}
|
||||
if (animMask[pos] > 0) { //Animation?
|
||||
num = oldDigit[pos]; //show old character
|
||||
timer = (PWMtimeBrightness * (20-animMask[pos]))/20; //older digit gets the (20-mask)/20 share of ON time
|
||||
state = 1; //next state is: show newDigit
|
||||
}
|
||||
else {
|
||||
num = digit[pos]; //show active character
|
||||
timer = PWMtimeBrightness;
|
||||
state = 2; //next state is: BLANK display
|
||||
}
|
||||
break;
|
||||
case 1: //show new character, if animation
|
||||
num = newDigit[pos];
|
||||
timer = (PWMtimeBrightness * animMask[pos])/20; //new digit gets the mask/20 share -> "wipe" transition within one tube's slot
|
||||
state = 2; //default next state is: BLANK display
|
||||
break;
|
||||
case 2: //blank display
|
||||
timer = PWMrefresh-PWMtimeBrightness;
|
||||
state = 3;
|
||||
break;
|
||||
} //end switch
|
||||
if (timer<500) timer = 500; //safety only... (too-short alarm periods risk missed/late reloads)
|
||||
|
||||
// if ((pos>0) && (num<=9)) num = convert[num]; //tube character conversion, if needed... (maybe bad pin numbering)
|
||||
|
||||
// num>9 (BLANK/MINUS/DP-glyph/... codes >=10) is treated as "blank" here purely as a
|
||||
// software shortcut; the 74141 hardware itself also outputs nothing for BCD 10-15.
|
||||
if ((brightness == 0) || (state == 3) || (num >9) || (!radarON)) { //blank digit
|
||||
state = 0;
|
||||
digitalWrite(digitEnablePins[pos],LOW); //switch off anode
|
||||
#if DP_PIN >=0
|
||||
digitalWrite(DP_PIN,LOW);
|
||||
#endif
|
||||
#if COLON_PIN >= 0
|
||||
digitalWrite(COLON_PIN,LOW); // Colon pin OFF
|
||||
#endif
|
||||
#if DECIMALPOINT_PIN >=0
|
||||
digitalWrite(DECIMALPOINT_PIN,LOW);
|
||||
#endif
|
||||
}
|
||||
else {
|
||||
for (int i=0;i<4;i++) {digitalWrite(ABCDPins[i],num & 1<<i); } //push BCD bits (bit i -> ABCDPins[i]) to the shared decoder
|
||||
digitalWrite(digitEnablePins[pos],HIGH); //switch on the new digit
|
||||
#if DP_PIN >=0
|
||||
// LEFTDECIMAL: some tube types have the DP cathode physically positioned to the
|
||||
// left of the digit glyph, so the DP for logical position `pos` must be lit
|
||||
// together with the NEXT tube's slot instead of this one's.
|
||||
if (LEFTDECIMAL) DPpos = min(maxDig-1,pos+1); else DPpos = pos;
|
||||
if (digitDP[DPpos]) digitalWrite(DP_PIN,HIGH); //switch ON decimal point, if needed
|
||||
else digitalWrite(DP_PIN,LOW);
|
||||
#endif
|
||||
#if COLON_PIN >= 0
|
||||
digitalWrite(COLON_PIN,colonBlinkState); // Blink colon pin
|
||||
#endif
|
||||
#if DECIMALPOINT_PIN >=0
|
||||
digitalWrite(DECIMALPOINT_PIN,decimalpointON);
|
||||
#endif
|
||||
}
|
||||
|
||||
|
||||
portEXIT_CRITICAL_ISR(&timerMux);
|
||||
//ESP32timer = timerBegin(0, PRESCALER, true); //set prescaler, true = edge generated signal
|
||||
//timerAttachInterrupt(ESP32timer, &writeDisplay, true);
|
||||
// Even though startTimer() configured the alarm as auto-reload, we still rewrite the
|
||||
// alarm value + re-enable every call: `timer` changes call-to-call (it encodes the
|
||||
// current state's ON/OFF duration), so this is how the variable-period software PWM
|
||||
// is actually implemented on top of a fixed-period hardware auto-reload timer.
|
||||
timerAlarmWrite(ESP32timer, timer, true);
|
||||
timerAlarmEnable(ESP32timer);
|
||||
interrupts();
|
||||
}
|
||||
|
||||
void clearTubes() {
|
||||
|
||||
for (int i=0;i<maxDig;i++) digitalWrite(digitEnablePins[i],LOW); //drop every digit-enable line so no tube anode remains active
|
||||
|
||||
#if (DP_PIN>=0)
|
||||
digitalWrite(DP_PIN,LOW);
|
||||
#endif
|
||||
}
|
||||
|
||||
// Not implemented for this driver: writeDisplay() (the timer ISR) already refreshes the
|
||||
// display continuously on its own; kept only for interface parity with drivers that need it.
|
||||
void writeDisplaySingle() {}
|
||||
#endif
|
||||
@@ -0,0 +1,203 @@
|
||||
// ============================================================================
|
||||
// Nixie driver: NON-multiplexed, one 74141 BCD decoder LATCHED per tube
|
||||
// ----------------------------------------------------------------------------
|
||||
// Hardware: 4 digit Nixie Clock, 2x 74HC595N shift register + 4x 74141
|
||||
// (or 6 tubes with more 74595/74141 chips, depending on clocks.h). Unlike the
|
||||
// multiplex74141*.ino drivers, there is NO time-division sharing of a single
|
||||
// decoder across tubes: EVERY tube has its own 74141, and all of them are fed
|
||||
// from a shared serial shift-register chain (dataPin/clkPin/latchPin, classic
|
||||
// 74HC595 bit-bang protocol - MSB-of-nibble first, see writeBits()). All tube
|
||||
// digits are pushed out serially and then latched (latchPin pulse) so that
|
||||
// ALL tubes display their digit SIMULTANEOUSLY and permanently (no flicker,
|
||||
// no per-tube ON/OFF multiplexing) - at the cost of needing one 74141 chip
|
||||
// per tube instead of one shared chip.
|
||||
// Because there is no per-tube ON slot to shorten/lengthen for dimming, the
|
||||
// SAME hardware-timer ISR mechanism as the multiplex drivers is reused here
|
||||
// only to fake brightness control: the whole shift-register chain is
|
||||
// periodically re-latched with either real digits or an all-blank pattern
|
||||
// (0xF nibbles) for a portion of each refresh cycle, so perceived brightness
|
||||
// is still a global ON-time/OFF-time duty cycle, just applied to all tubes at
|
||||
// once rather than tube-by-tube.
|
||||
//
|
||||
// Platform: ESP8266 ONLY. For the ESP32 port see no_multiplex_ESP32.ino.
|
||||
// Used by these clocks.h configs (search for "#define NO_MULTIPLEX74141"):
|
||||
// CLOCK_6, CLOCK_6A
|
||||
// ============================================================================
|
||||
|
||||
#ifdef NO_MULTIPLEX74141
|
||||
|
||||
char tubeDriver[] = "NO_MULTIPLEX74141";
|
||||
#if defined(ESP8266)
|
||||
#else
|
||||
#error "Only 8266 Board is supported!"
|
||||
#endif
|
||||
|
||||
//change it, if needed for the correct tube sequence
|
||||
//byte tubes[] = {3,2,1,0}; //4 tubes, old clock... in clocks.h
|
||||
//byte tubes[] = {5,4,3,2,1,0}; //6 tubes, reverse order in clocks.h
|
||||
// tubes[] maps PHYSICAL shift-register output position -> LOGICAL digit[] index,
|
||||
// so a clock's tube wiring order (which may not match the natural digit order)
|
||||
// can be corrected purely in clocks.h without touching this driver's logic.
|
||||
|
||||
int maxDigits = sizeof(tubes); //tubes is byte[], so sizeof == tube count
|
||||
// NOTE: despite the "msec" comment, these are raw timer1 tick counts (see startTimer():
|
||||
// TIM_DIV16 => ~312.5kHz => ~3.2us/tick), not real milliseconds.
|
||||
int PWMrefresh=12000; //msec, Multiplex time period. Greater value => slower multiplex frequency
|
||||
int PWM_min = 1000;
|
||||
int PWM_max = 12000;
|
||||
//int PWMtiming[11] = {1000,1000,2000,3000,4000,5000,6000,7000,8000,10000,12000};
|
||||
|
||||
//#define dataPin 14 //D5
|
||||
//#define latchPin 12 //D6
|
||||
//#define clkPin 13 //D7
|
||||
|
||||
// Not implemented for this driver: brightness/blanking is handled entirely inside the
|
||||
// timer ISR (writeDisplay) by periodically re-shifting an all-blank pattern, so there is
|
||||
// no separate blocking "write once" path. Kept only for interface parity.
|
||||
void writeDisplaySingle() { }
|
||||
|
||||
void setup_pins(){
|
||||
DPRINTLN("Nixie clock - setup pins NON-MULTIPLEX 4(or 6) x 74141");
|
||||
pinMode(dataPin, OUTPUT); regPin(dataPin,"dataPin");
|
||||
pinMode(latchPin,OUTPUT); regPin(latchPin,"latchPin");
|
||||
pinMode(clkPin,OUTPUT); regPin(clkPin,"clkPin");
|
||||
startTimer();
|
||||
}
|
||||
|
||||
// Bit-bangs one 4-bit BCD nibble (one tube's worth of data) into the 74HC595 shift
|
||||
// register chain, MSB-first. Runs inside the timer ISR (ICACHE_RAM_ATTR keeps it in IRAM
|
||||
// so it stays callable even while flash is busy), so it cannot use delayMicroseconds()
|
||||
// (which is not ISR-safe / not guaranteed available) - the "for t<10 nop" loops are a
|
||||
// crude but deterministic ~100ns busy-wait to satisfy the 74HC595's minimum clock
|
||||
// pulse-width/setup-time requirements.
|
||||
void ICACHE_RAM_ATTR writeBits(byte num) { //shift out 4 bits
|
||||
for (int i=3;i>=0;i--) {
|
||||
digitalWrite(dataPin,num & (1<<i));
|
||||
for (int t=0; t<10;t++) asm volatile ("nop"); //100nsec
|
||||
digitalWrite(clkPin,HIGH);
|
||||
for (int t=0; t<10;t++) asm volatile ("nop"); //100nsec
|
||||
digitalWrite(clkPin,LOW);
|
||||
for (int t=0; t<10;t++) asm volatile ("nop"); //100nsec
|
||||
}
|
||||
}
|
||||
|
||||
void ICACHE_RAM_ATTR writeDisplay(){ //https://circuits4you.com/2018/01/02/esp8266-timer-ticker-example/
|
||||
static volatile byte state=0;
|
||||
static int timer = PWMrefresh;
|
||||
static int brightness;
|
||||
static int PWMtimeBrightness=PWM_min;
|
||||
byte animM;
|
||||
|
||||
if (EEPROMsaving) { //stop refresh, while EEPROM write is in progress!
|
||||
timer1_write(PWMrefresh);
|
||||
return;
|
||||
}
|
||||
|
||||
// POSSIBLE BUG / design limitation: this collapses each tube's per-position animMask[i]
|
||||
// (0..20 wipe-progress value) into a SINGLE global `animM`, keeping only the last
|
||||
// nonzero one found while scanning left to right. Because ALL tubes are re-latched
|
||||
// together every ISR call (see the loop below), the actual ON/OFF *timing* of the wipe
|
||||
// transition (computed further down from `animM`) is necessarily shared by every tube,
|
||||
// even though the choice of WHICH digit to show per tube (old vs new, via animMask[i]==0)
|
||||
// is still made individually. So if two tubes are mid-wipe with different animMask
|
||||
// values at the same moment, only one of them will actually be timed correctly; unlike
|
||||
// the multiplex74141*.ino drivers (where each tube gets its own ISR pass and thus its
|
||||
// own independent timing), true per-tube staggered animation isn't achievable here.
|
||||
animM = 0;
|
||||
for (int i=0;i<maxDigits;i++) {
|
||||
if (animMask[i] > 0) animM = animMask[i]; //find, if any animation is wanted?
|
||||
}
|
||||
|
||||
brightness = displayON ? prm.dayBright : prm.nightBright;
|
||||
if (brightness>MAXBRIGHTNESS) brightness = MAXBRIGHTNESS; //only for safety
|
||||
|
||||
digitalWrite(latchPin, LOW); //keep 74141 outputs frozen on their OLD latched values while we shift in new data
|
||||
|
||||
if ((brightness ==0) || !radarON){
|
||||
for (int i=0;i<maxDigits;i++) writeBits(0xA); //blank display (0xA..0xF are all "invalid BCD" -> 74141 outputs nothing, i.e. blank)
|
||||
|
||||
#if DECIMALPOINT_PIN>=0
|
||||
digitalWrite(DECIMALPOINT_PIN,LOW);
|
||||
#endif
|
||||
#if COLON_PIN >= 0
|
||||
digitalWrite(COLON_PIN,LOW); // colon pin OFF
|
||||
#endif
|
||||
digitalWrite(latchPin, HIGH); //commit the all-blank pattern to the 74141 outputs
|
||||
timer1_write(100*PWMrefresh); //display fully off (radar/night-off) - just idle-poll slowly instead of running the PWM state machine
|
||||
return;
|
||||
}
|
||||
else {
|
||||
// Shift out one BCD nibble per tube, all in a single ISR pass (this is what makes
|
||||
// the driver "non-multiplexed": every tube gets its data on every call, rather than
|
||||
// only the currently-selected tube as in the multiplex74141*.ino drivers).
|
||||
for (int i=0;i<maxDigits;i++) {
|
||||
if (state==0) {
|
||||
if (animMask[i]==0) writeBits(digit[tubes[i]]);
|
||||
else writeBits(oldDigit[tubes[i]]);
|
||||
}
|
||||
else if (state==1) writeBits(newDigit[tubes[i]]);
|
||||
else writeBits(0xF); //state 2 (blank phase of the brightness duty cycle): 0xF is any non-BCD code, forces the 74141 output off
|
||||
} //end for
|
||||
|
||||
//for (int i=0;i<maxDigits;i++) writeBits(digit[tubes[i]]); //simple test, without animation
|
||||
|
||||
// 3-state duty-cycle machine (global, applies to the whole tube bank at once - see the
|
||||
// note above about animM): state 0 = show current/old digits for the brightness ON
|
||||
// time (optionally starting a wipe if animM>0); state 1 = show the "new" half of a wipe
|
||||
// for the ON time; state 2 = blank all tubes for the remaining OFF time (dimming).
|
||||
// When brightness is already MAXBRIGHTNESS, state 2 is skipped entirely (goes straight
|
||||
// back to state 0) since there's no need to ever blank at 100% duty cycle.
|
||||
switch (state) { //state machine...
|
||||
case 0:
|
||||
#if DECIMALPOINT_PIN>=0
|
||||
if (decimalpointON) digitalWrite(DECIMALPOINT_PIN,HIGH);
|
||||
#endif
|
||||
#if COLON_PIN>=0
|
||||
digitalWrite(COLON_PIN,colonBlinkState); // Blink colon pin
|
||||
#endif
|
||||
if (autoBrightness && displayON) {
|
||||
PWMtimeBrightness = max(PWM_min,PWM_max*lx/MAXIMUM_LUX);
|
||||
}
|
||||
else {
|
||||
PWMtimeBrightness = max(PWM_min,PWM_max*brightness/MAXBRIGHTNESS);
|
||||
}
|
||||
if (animM > 0) { //Animation?
|
||||
timer = (PWMtimeBrightness * (20-animM))/20;
|
||||
state = 1; //next state is: show newDigits
|
||||
}
|
||||
else {
|
||||
timer = PWMtimeBrightness;
|
||||
if (brightness>=MAXBRIGHTNESS) state = 0;
|
||||
else state = 2; //default next state is: BLANK display
|
||||
}
|
||||
break;
|
||||
case 1: //show new character, if animation
|
||||
timer = (PWMtimeBrightness * animM)/20;
|
||||
if (brightness>=MAXBRIGHTNESS) state = 0;
|
||||
else state = 2; //default next state is: BLANK display
|
||||
break;
|
||||
case 2: //blank display
|
||||
#if DECIMALPOINT_PIN>=0
|
||||
digitalWrite(DECIMALPOINT_PIN,LOW);
|
||||
#endif
|
||||
#if COLON_PIN>=0
|
||||
digitalWrite(COLON_PIN,LOW); // colon pin OFF
|
||||
#endif
|
||||
state = 0;
|
||||
timer = PWMrefresh-PWMtimeBrightness;
|
||||
break;
|
||||
} //end switch
|
||||
} //end else
|
||||
|
||||
digitalWrite(latchPin, HIGH); //commit the freshly shifted-in data to all 74141 outputs at once (this is the actual visible update instant)
|
||||
|
||||
if ((brightness == 0) || (!radarON)) {timer = PWM_max; state = 0;} //no time sharing is needed
|
||||
if (timer<500) timer = 500; //safety only...
|
||||
timer1_write(timer);
|
||||
}
|
||||
|
||||
// Not implemented for this driver: there's no persistent "enable" line to blank, since
|
||||
// blanking is achieved by re-shifting an all-off (0xA-0xF) pattern from within
|
||||
// writeDisplay() itself; a real clearTubes() would need to bit-bang a blank frame here too.
|
||||
void clearTubes() {}
|
||||
#endif
|
||||
@@ -0,0 +1,242 @@
|
||||
// ============================================================================
|
||||
// Nixie driver: NON-multiplexed, one 74141 BCD decoder LATCHED per tube (ESP32)
|
||||
// ----------------------------------------------------------------------------
|
||||
// Hardware: 4 (or 6) digit Nixie Clock, 2x 74HC595N shift register + 4x (or 6x)
|
||||
// 74141. Identical hardware/wiring strategy to no_multiplex74141.ino (every
|
||||
// tube has its own dedicated 74141, all fed from one shared 74HC595 shift-
|
||||
// register chain via dataPin/clkPin/latchPin; ALL tubes are shifted+latched
|
||||
// together every ISR call so they update SIMULTANEOUSLY - there is no
|
||||
// per-tube time-division multiplexing here, unlike the multiplex74141*.ino /
|
||||
// pcf_multiplex74141.ino drivers). Brightness is still faked with the same
|
||||
// trick: the whole chain is periodically re-latched with an all-blank pattern
|
||||
// (0xF nibbles) for part of each refresh cycle, turning a single shared
|
||||
// ON/OFF duty cycle into perceived dimming across all tubes at once.
|
||||
//
|
||||
// This file is the ESP32 port of no_multiplex74141.ino - kept as a separate
|
||||
// file (rather than merged behind #ifdefs) for the same reason as
|
||||
// multiplex74141_ESP32.ino vs multiplex74141.ino: the hardware-timer APIs
|
||||
// differ (timer1_write single-shot vs. timerAlarmWrite auto-reload,
|
||||
// ICACHE_RAM_ATTR vs IRAM_ATTR/DRAM_ATTR placement) and ESP32 is dual-core,
|
||||
// so the ISR here additionally needs portENTER/EXIT_CRITICAL_ISR(&timerMux)
|
||||
// around the shared-state update, which the single-core ESP8266 doesn't need.
|
||||
//
|
||||
// Platform: ESP32 ONLY. For the ESP8266 original see no_multiplex74141.ino.
|
||||
// Used by these clocks.h configs (search for "#define NO_MULTIPLEX_ESP32"):
|
||||
// CLOCK_35 (6x Z566M tubes, 3x74595/6x74141), CLOCK_36 (4x IN-1 tubes,
|
||||
// 4x74595/4x74141) - both currently commented out (inactive) in clocks.h.
|
||||
// dataPin/latchPin/clkPin and tubes[] are defined per-CLOCK_NN in clocks.h.
|
||||
// ============================================================================
|
||||
|
||||
#ifdef NO_MULTIPLEX_ESP32
|
||||
|
||||
char tubeDriver[] = "NO_MULTIPLEX_ESP32";
|
||||
#if defined(ESP32)
|
||||
#else
|
||||
#error "Only ESP32 Board is supported!"
|
||||
#endif
|
||||
|
||||
//change it, if needed for the correct tube sequence
|
||||
//byte tubes[] = {3,2,1,0}; //4 tubes, old clock... use in clocks.h
|
||||
//byte tubes[] = {5,4,3,2,1,0}; //6 tubes, reverse order use in clocks.h
|
||||
// tubes[] maps PHYSICAL shift-register output position -> LOGICAL digit[] index,
|
||||
// exactly as in no_multiplex74141.ino, so tube wiring order is corrected in
|
||||
// clocks.h alone without touching this driver's logic.
|
||||
|
||||
// NOTE: unlike multiplex74141_ESP32.ino (which keeps a separate DRAM_ATTR maxDig
|
||||
// copy of maxDigits specifically for the ISR to read), writeDisplay() below reads
|
||||
// this plain maxDigits directly. In practice this is harmless: ordinary non-const
|
||||
// globals like this one already live in .bss (real internal RAM), not in a
|
||||
// flash-cached region, so no DRAM_ATTR is strictly required here - but it is an
|
||||
// inconsistency with the pattern established elsewhere in this driver family, so
|
||||
// flagging it for consistency.
|
||||
int maxDigits = sizeof(tubes);
|
||||
// NOTE: PWMrefresh/PWM_min/PWM_max here are noticeably larger than the ESP8266
|
||||
// sibling's (12000/1000/12000) because the ESP32 hardware timer used by
|
||||
// startTimer() runs at a different effective tick rate (PRESCALER=15 against the
|
||||
// APB clock, see startTimer() in the core .ino) than the ESP8266's TIM_DIV16
|
||||
// timer1 - these were re-tuned for ESP32 rather than being real milliseconds
|
||||
// on either platform.
|
||||
int DRAM_ATTR PWMrefresh=25000; //msec, Multiplex time period. Greater value => slower multiplex frequency
|
||||
int DRAM_ATTR PWM_min = 5000;
|
||||
int DRAM_ATTR PWM_max = 25000;
|
||||
|
||||
//#define dataPin 14 //D5
|
||||
//#define latchPin 27 //12 //D6
|
||||
//#define clkPin 26 //13 //D7
|
||||
|
||||
// Not implemented for this driver: brightness/blanking is handled entirely inside
|
||||
// the timer ISR (writeDisplay) by periodically re-shifting an all-blank pattern, so
|
||||
// there is no separate blocking "write once" path. Kept only for interface parity.
|
||||
void IRAM_ATTR writeDisplaySingle() { }
|
||||
|
||||
void setup_pins(){
|
||||
DPRINTLN("Nixie clock - setup pins NON-MULTIPLEX 4(or 6) x 74141");
|
||||
pinMode(dataPin, OUTPUT); regPin(dataPin,"dataPin");
|
||||
pinMode(latchPin,OUTPUT); regPin(latchPin,"latchPin");
|
||||
pinMode(clkPin,OUTPUT); regPin(clkPin,"clkPin");
|
||||
startTimer();
|
||||
}
|
||||
|
||||
// Bit-bangs one 4-bit BCD nibble (one tube's worth of data) into the 74HC595 shift
|
||||
// register chain, MSB-first - same protocol/timing as no_multiplex74141.ino's
|
||||
// writeBits(). Uses ICACHE_RAM_ATTR (aliased to IRAM_ATTR for ESP32 builds, see
|
||||
// "#define ICACHE_RAM_ATTR IRAM_ATTR" in the core .ino) purely so this file stays
|
||||
// close to its ESP8266 sibling; functionally identical to marking it IRAM_ATTR
|
||||
// directly. Runs from inside the timer ISR, so it cannot use delayMicroseconds()
|
||||
// - the "for t<10 nop" loops are a crude but deterministic busy-wait instead, to
|
||||
// satisfy the 74HC595's minimum clock pulse-width/setup-time requirements.
|
||||
void ICACHE_RAM_ATTR writeBits(byte num) { //shift out 4 bits
|
||||
for (int i=3;i>=0;i--) {
|
||||
digitalWrite(dataPin,num & (1<<i));
|
||||
for (int t=0; t<10;t++) asm volatile ("nop"); //100nsec
|
||||
digitalWrite(clkPin,HIGH);
|
||||
for (int t=0; t<10;t++) asm volatile ("nop"); //100nsec
|
||||
digitalWrite(clkPin,LOW);
|
||||
for (int t=0; t<10;t++) asm volatile ("nop"); //100nsec
|
||||
}
|
||||
}
|
||||
|
||||
void IRAM_ATTR writeDisplay(){ //https://circuits4you.com/2018/01/02/esp8266-timer-ticker-example/
|
||||
// All static locals are DRAM_ATTR for the same reason as in multiplex74141_ESP32.ino:
|
||||
// an IRAM_ATTR ISR must be able to run even while flash is briefly inaccessible
|
||||
// (Wi-Fi / EEPROM-OTA activity), which requires every byte of data it touches -
|
||||
// not just the function's own code - to be forced into internal DRAM.
|
||||
static DRAM_ATTR volatile byte state=0;
|
||||
static DRAM_ATTR int timer = PWMrefresh;
|
||||
static DRAM_ATTR int brightness;
|
||||
static DRAM_ATTR int PWMtimeBrightness=PWM_min;
|
||||
byte animM;
|
||||
|
||||
if (EEPROMsaving) { //stop refresh, while EEPROM write is in progress!
|
||||
// Unlike the ESP8266 sibling (which re-arms timer1 at an idle rate before
|
||||
// returning), this simply returns without touching the timer. That's fine
|
||||
// because startTimer() configured ESP32timer as auto-reload (true flag), so
|
||||
// the alarm keeps firing on its own at whatever period was last programmed;
|
||||
// only the shift-register update is skipped while EEPROM is being written.
|
||||
return;
|
||||
}
|
||||
|
||||
// Dual-core ESP32: portENTER/EXIT_CRITICAL_ISR takes a spinlock so the other core
|
||||
// can't preempt this ISR mid-update, while noInterrupts()/interrupts() additionally
|
||||
// blocks other interrupt sources on THIS core - together they keep the
|
||||
// writeBits()+latchPin sequence below atomic (a partial shift-out latched early
|
||||
// would flash the wrong digits on every tube at once).
|
||||
portENTER_CRITICAL_ISR(&timerMux);
|
||||
noInterrupts();
|
||||
|
||||
// POSSIBLE BUG / design limitation (same as no_multiplex74141.ino): this collapses
|
||||
// every tube's per-position animMask[i] (0..20 wipe-progress) into a SINGLE global
|
||||
// animM, keeping only the last nonzero value found. Because every tube is re-shifted
|
||||
// and re-latched together in one ISR pass, the actual wipe TIMING computed from animM
|
||||
// further down is necessarily shared by all tubes; only the per-tube choice of old vs.
|
||||
// new digit (via animMask[i]==0 below) is still individual. So two tubes mid-wipe with
|
||||
// different animMask values at the same time can't both be timed correctly - true
|
||||
// per-tube staggered animation isn't achievable with this non-multiplexed design.
|
||||
animM = 0;
|
||||
for (int i=0;i<maxDigits;i++) {
|
||||
if (animMask[i] > 0) animM = animMask[i]; //find, if any animation is wanted?
|
||||
}
|
||||
|
||||
brightness = displayON ? prm.dayBright : prm.nightBright;
|
||||
if (brightness>MAXBRIGHTNESS) brightness = MAXBRIGHTNESS; //only for safety
|
||||
|
||||
digitalWrite(latchPin, LOW); //keep the 74141 outputs frozen on their OLD latched values while new data is shifted in
|
||||
|
||||
if ((brightness ==0) || !radarON) {
|
||||
for (int i=0;i<maxDigits;i++) writeBits(0xA); //blank display (0xA is an invalid BCD code -> every 74141 output stays off)
|
||||
|
||||
#if DECIMALPOINT_PIN>=0
|
||||
digitalWrite(DECIMALPOINT_PIN,LOW);
|
||||
#endif
|
||||
#if COLON_PIN >= 0
|
||||
digitalWrite(COLON_PIN,LOW); // colon pin OFF
|
||||
#endif
|
||||
digitalWrite(latchPin, HIGH); //commit the all-blank pattern to the 74141 outputs
|
||||
portEXIT_CRITICAL_ISR(&timerMux);
|
||||
timerAlarmWrite(ESP32timer, 100*PWMrefresh, true); //display fully off (radar/night-off) - just idle-poll slowly instead of running the PWM state machine
|
||||
timerAlarmEnable(ESP32timer);
|
||||
interrupts();
|
||||
return;
|
||||
}
|
||||
else {
|
||||
// Shift out one BCD nibble per tube, all in a single ISR pass - this is what makes
|
||||
// the driver "non-multiplexed": every tube gets fresh data on every call, rather
|
||||
// than only the currently-selected tube as in the multiplex74141*.ino drivers.
|
||||
for (int i=0;i<maxDigits;i++) {
|
||||
if (state==0) {
|
||||
if (animMask[i]==0) writeBits(digit[tubes[i]]);
|
||||
else writeBits(oldDigit[tubes[i]]);
|
||||
}
|
||||
else if (state==1) writeBits(newDigit[tubes[i]]);
|
||||
else writeBits(0xF); //state 2 (blank phase of the brightness duty cycle): 0xF is any non-BCD code, forces the 74141 output off
|
||||
} //end for
|
||||
|
||||
//for (int i=0;i<maxDigits;i++) writeBits(digit[tubes[i]]); //simple test, without animation
|
||||
|
||||
// 3-state duty-cycle machine (global, applies to the whole tube bank at once - see the
|
||||
// animM note above): state 0 = show current/old digits for the brightness ON time
|
||||
// (optionally starting a wipe if animM>0); state 1 = show the "new" half of a wipe for
|
||||
// the ON time; state 2 = blank all tubes for the remaining OFF time (dimming). At
|
||||
// MAXBRIGHTNESS, state 2 is skipped entirely (stays at/returns to state 0) since
|
||||
// there's no need to ever blank at 100% duty cycle.
|
||||
switch (state) { //state machine...
|
||||
case 0:
|
||||
#if DECIMALPOINT_PIN>=0
|
||||
if (decimalpointON) digitalWrite(DECIMALPOINT_PIN,HIGH);
|
||||
#endif
|
||||
#if COLON_PIN>=0
|
||||
digitalWrite(COLON_PIN,colonBlinkState); // Blink colon pin
|
||||
#endif
|
||||
if (autoBrightness && displayON) {
|
||||
PWMtimeBrightness = max(PWM_min,PWM_max*lx/MAXIMUM_LUX);
|
||||
}
|
||||
else {
|
||||
PWMtimeBrightness = max(PWM_min,PWM_max*brightness/MAXBRIGHTNESS);
|
||||
}
|
||||
if (animM > 0) { //Animation?
|
||||
timer = (PWMtimeBrightness * (20-animM))/20;
|
||||
state = 1; //next state is: show newDigits
|
||||
}
|
||||
else {
|
||||
timer = PWMtimeBrightness;
|
||||
if (brightness>=MAXBRIGHTNESS) state = 0;
|
||||
else state = 2; //default next state is: BLANK display
|
||||
}
|
||||
break;
|
||||
case 1: //show new character, if animation
|
||||
timer = (PWMtimeBrightness * animM)/20;
|
||||
if (brightness>=MAXBRIGHTNESS) state = 0;
|
||||
else state = 2; //default next state is: BLANK display
|
||||
break;
|
||||
case 2: //blank display
|
||||
#if DECIMALPOINT_PIN>=0
|
||||
digitalWrite(DECIMALPOINT_PIN,LOW);
|
||||
#endif
|
||||
#if COLON_PIN>=0
|
||||
digitalWrite(COLON_PIN,LOW); // colon pin OFF
|
||||
#endif
|
||||
state = 0;
|
||||
timer = PWMrefresh-PWMtimeBrightness;
|
||||
break;
|
||||
} //end switch
|
||||
} //end else
|
||||
|
||||
digitalWrite(latchPin, HIGH); //commit the freshly shifted-in data to all 74141 outputs at once (this is the actual visible update instant)
|
||||
|
||||
if ((brightness == 0) || (!radarON)) {timer = PWM_max; state = 0;} //no time sharing is needed
|
||||
if (timer<500) timer = 500; //safety only...
|
||||
portEXIT_CRITICAL_ISR(&timerMux);
|
||||
// Even though startTimer() set the alarm to auto-reload, the alarm value is still
|
||||
// rewritten on every call: `timer` changes call-to-call (it encodes the current
|
||||
// state's ON/OFF duration), so this is how the variable-period software PWM is
|
||||
// actually implemented on top of a fixed-period hardware auto-reload timer.
|
||||
timerAlarmWrite(ESP32timer, timer, true);
|
||||
timerAlarmEnable(ESP32timer);
|
||||
interrupts();
|
||||
}
|
||||
|
||||
// Not implemented for this driver: there's no persistent "enable" line to blank, since
|
||||
// blanking is achieved by re-shifting an all-off (0xA/0xF) pattern from within
|
||||
// writeDisplay() itself; a real clearTubes() would need to bit-bang a blank frame here too.
|
||||
void IRAM_ATTR clearTubes() {}
|
||||
#endif
|
||||
@@ -0,0 +1,318 @@
|
||||
// ============================================================================
|
||||
// Nixie driver: shared/multiplexed BCD-to-decimal decoder (SN74141), tube
|
||||
// selection via a PCF8574 I2C GPIO expander
|
||||
// ----------------------------------------------------------------------------
|
||||
// Hardware: same multiplex strategy as multiplex74141.ino (ONE shared 74141
|
||||
// decoder, BCD inputs wired in parallel to all tubes, time-division
|
||||
// multiplexed one tube at a time via a per-tube anode-enable line - see that
|
||||
// file's header for the full rationale). The difference here is where the
|
||||
// per-tube ENABLE lines come from: instead of driving each digitEnablePins[i]
|
||||
// straight off an ESP GPIO, up to 8 of them can instead live on the port pins
|
||||
// (P0..P7) of a PCF8574 I2C I/O expander chip, addressed with a hand-rolled
|
||||
// (bit-banged, NOT Wire-library) I2C implementation. This lets a clock with
|
||||
// more tubes than spare direct GPIOs (this file supports up to 6-8 tubes)
|
||||
// still be built with a single shared 74141 and only 2 extra ESP pins
|
||||
// (PCF_SDA_PIN/PCF_SCL_PIN) instead of one extra GPIO per tube.
|
||||
// digitEnablePins[i] values 0-99 address a direct ESP GPIO as usual; values
|
||||
// 100-107 address PCF8574 port P(value-100) instead (see sendBits() below) -
|
||||
// by convention "add 100 to the port number" when a tube's enable line is on
|
||||
// the PCF chip. The ABCDPins[4] (shared BCD lines) and DP_PIN always stay on
|
||||
// direct ESP GPIOs since there are only 4 of them regardless of tube count.
|
||||
//
|
||||
// The I2C bit-banging (not Wire.h) is deliberate: writeDisplay()/sendBits()/
|
||||
// shiftout() all run inside the hardware-timer ISR (ICACHE_RAM_ATTR), and the
|
||||
// Arduino Wire library is not safe/available to call from an ISR context, so
|
||||
// the SDA/SCL toggling is done by hand with busy-wait "delayMS()" nop loops
|
||||
// instead (see the protocol walkthrough further down).
|
||||
//
|
||||
// Platform: ESP8266 ONLY (this file has never been ported to ESP32; compare
|
||||
// with how multiplex74141.ino/multiplex74141_ESP32.ino form a matching pair -
|
||||
// no *_ESP32 sibling exists for this PCF-expander strategy).
|
||||
// Used by these clocks.h configs (search for "#define PCF_74141"):
|
||||
// CLOCK_7 (8266 UNFI PCB clock, 6x Z574M/Z573M tubes) - currently commented
|
||||
// out (inactive) in clocks.h, i.e. this driver is presently orphaned/unused
|
||||
// by the active build (CLOCK_72 is the config currently enabled).
|
||||
// Unlike the other files in this family, digitEnablePins[]/ABCDPins[]/DP_PIN/
|
||||
// I2C_ADDR/PCF_SDA_PIN/PCF_SCL_PIN are hardcoded directly in THIS file rather
|
||||
// than left commented-out for clocks.h to define per-config - clocks.h's
|
||||
// CLOCK_7 block only sets "#define PCF_74141" and does not redefine any of
|
||||
// these, so every clock built with this driver currently gets this exact
|
||||
// fixed 6-tube/PCF pinout with no per-config override, unlike every sibling
|
||||
// driver in this family.
|
||||
// ============================================================================
|
||||
#ifdef PCF_74141
|
||||
char tubeDriver[] = "PCF_74141";
|
||||
//PCF8574 I/O expander version, 4..8 tube Nixie Clock, driven by 74141
|
||||
//define here the digit enable pins from 4 to 8 tubes
|
||||
|
||||
//if a digitEnablePort is on PCF chip, add 100 to the port number! (for example 2-->102)
|
||||
|
||||
#define I2C_ADDR 0x20 //PCF8574 base 7-bit I2C address (A0/A1/A2 strapped to GND on this board)
|
||||
#define PCF_SDA_PIN 4
|
||||
#define PCF_SCL_PIN 5
|
||||
|
||||
//If a digitEnablePin is on pcf8574, add 100 to the pin number. (P0 = 100,... P7 = 107)
|
||||
byte digitEnablePins[] = {100,101,102,103,104,105}; //6 tube nixie driven by PCF - all 6 enable lines live on the PCF8574 here, none direct on ESP GPIOs
|
||||
byte ABCDPins[4] = {14,12,13,2}; //D5,D6,D7,D4 on 8266 - shared BCD lines, always direct ESP GPIOs regardless of tube count
|
||||
#define DP_PIN 16 // decimalPoint on 8266's D0
|
||||
|
||||
int maxDigits = sizeof(digitEnablePins);
|
||||
|
||||
//byte convert[] = {1,0,9,8,7,6,5,4,3,2}; //tube pin conversion, is needed (for example: bad tube pin layout)
|
||||
int PWMrefresh=15000; //msec, Multiplex time period. Greater value => slower multiplex frequency
|
||||
int PWM_min = 2000;
|
||||
int PWM_max = 14000;
|
||||
//int PWMtiming[11] = {0,2000,3000,4000,5000,6000,7000,8000,10000,12000,14000};
|
||||
|
||||
#if defined(ESP8266)
|
||||
#else
|
||||
#error "Only 8266 Board is supported!"
|
||||
#endif
|
||||
|
||||
// NOTE: named "delayMS" but the comment says "Delay microsec" - actually neither is
|
||||
// literally true: it's a plain busy-wait nop loop, roughly proportional to `d` at
|
||||
// whatever clock speed the 8266 runs at (not a calibrated microsecond/millisecond
|
||||
// delay). Used instead of delayMicroseconds()/delay() because this runs inside the
|
||||
// timer ISR (ICACHE_RAM_ATTR) where those aren't safe/appropriate to call.
|
||||
void ICACHE_RAM_ATTR delayMS(int d) { //Delay microsec
|
||||
for (int i=0;i<d*30;i++) {asm volatile ("nop"); }
|
||||
}
|
||||
|
||||
// Bit-bangs one 8-bit byte onto the I2C bus, MSB first, toggling PCF_SCL_PIN once per
|
||||
// bit while PCF_SDA_PIN carries the bit value (standard I2C: SDA must be stable
|
||||
// while SCL is HIGH). After the 8 data bits, pulls SDA LOW and pulses SCL once more
|
||||
// for the slave's ACK bit - but note the ACK is never actually READ back (the
|
||||
// pinMode(...,INPUT_PULLUP) sampling code is commented out just above): this
|
||||
// implementation blindly assumes the PCF8574 acknowledged and will happily continue
|
||||
// even if the chip is missing or the bus is stuck.
|
||||
void ICACHE_RAM_ATTR shiftout(byte in) {
|
||||
for (int i=0;i<8;i++) {
|
||||
digitalWrite(PCF_SDA_PIN,in & (1<<(7-i)));
|
||||
delayMS(1);
|
||||
digitalWrite(PCF_SCL_PIN,HIGH);
|
||||
//if (i==0) delayMS(8);
|
||||
delayMS(4);
|
||||
digitalWrite(PCF_SCL_PIN,LOW);
|
||||
}
|
||||
|
||||
//ACK is coming
|
||||
//pinMode(PCF_SDA_PIN,INPUT_PULLUP);
|
||||
digitalWrite(PCF_SDA_PIN,LOW); //LOW
|
||||
delayMS(12);
|
||||
digitalWrite(PCF_SCL_PIN,HIGH); //clock pulse
|
||||
delayMS(4);
|
||||
digitalWrite(PCF_SCL_PIN,LOW);
|
||||
//pinMode(PCF_SDA_PIN,OUTPUT);
|
||||
delayMS(4);
|
||||
}
|
||||
|
||||
// Sends one complete I2C write transaction (START + 7-bit address+W + 1 data byte +
|
||||
// STOP) to the PCF8574 at `address`, blocking (from the ISR's point of view) for the
|
||||
// whole bit-banged transfer. `val` is the full 8-bit output pattern for the PCF8574's
|
||||
// P0..P7 pins - since the PCF8574 has no separate "set one pin" command, every call
|
||||
// here replaces the ENTIRE port state at once (this matters for how digitEnablePins
|
||||
// on the PCF get turned on/off in writeDisplay() below: enabling a new tube's PCF
|
||||
// pin implicitly turns every other PCF-hosted tube off in the same write).
|
||||
void ICACHE_RAM_ATTR sendBits(byte address,byte val){
|
||||
//Serial.print(address,HEX); Serial.print("/"); Serial.println(val);
|
||||
|
||||
// The explicit startCondition()/stopCondition() helpers below are left commented out
|
||||
// and inlined by hand instead; the START here works because the bus is left idle
|
||||
// (SDA=HIGH, SCL=HIGH) by the previous call's STOP condition (or by setup_pins()),
|
||||
// so simply pulling SDA LOW while SCL is still HIGH IS the START condition.
|
||||
//void ICACHE_RAM_ATTR startCondition() {
|
||||
//digitalWrite(PCF_SDA_PIN,HIGH); //PCF_SDA_PIN HIGH -> LOW, while PCF_SCL_PIN is HIGH
|
||||
//digitalWrite(PCF_SCL_PIN,HIGH);
|
||||
//delayMS(4);
|
||||
digitalWrite(PCF_SDA_PIN,LOW);
|
||||
delayMS(3);
|
||||
digitalWrite(PCF_SCL_PIN,LOW);
|
||||
delayMS(4);
|
||||
|
||||
shiftout(address<<1); //shift address one bit left, because 0bit is READ/WRITE mode. 0=WRITE
|
||||
shiftout(val);
|
||||
|
||||
// STOP condition: SDA transitions LOW->HIGH while SCL is HIGH.
|
||||
//void ICACHE_RAM_ATTR stopCondition() {
|
||||
delayMS(4);
|
||||
digitalWrite(PCF_SDA_PIN,LOW); //PCF_SDA_PIN LOW -> HIGH, while PCF_SCL_PIN is HIGH
|
||||
delayMS(2);
|
||||
digitalWrite(PCF_SCL_PIN,HIGH);
|
||||
delayMS(4);
|
||||
digitalWrite(PCF_SDA_PIN,HIGH);
|
||||
delayMS(4);
|
||||
}
|
||||
|
||||
|
||||
void setup_pins() {
|
||||
char tmp[30];
|
||||
|
||||
DPRINTLN("PCF8574 MULTIPLEX driver");
|
||||
for (int i=0;i<maxDigits;i++) {
|
||||
if (digitEnablePins[i]<100) {
|
||||
pinMode(digitEnablePins[i], OUTPUT); //8266 pins setup, if needed
|
||||
sprintf(tmp,"digitEnPin[%d]",i);
|
||||
regPin(digitEnablePins[i],tmp);
|
||||
}
|
||||
// NOTE: pins >=100 (PCF-hosted) are intentionally NOT pinMode'd/regPin'd here -
|
||||
// they aren't real ESP GPIOs at all, just an index into the PCF8574's output byte
|
||||
// sent over I2C via sendBits() below, so there's nothing local to configure.
|
||||
}
|
||||
for (int i=0;i<4;i++) {
|
||||
pinMode(ABCDPins[i], OUTPUT);
|
||||
sprintf(tmp,"Pin[%c]",char('A'+i));
|
||||
regPin(ABCDPins[i],tmp);
|
||||
}
|
||||
pinMode(PCF_SDA_PIN,OUTPUT); regPin(PCF_SDA_PIN,"PCF_SDA_PIN");
|
||||
pinMode(PCF_SCL_PIN,OUTPUT); regPin(PCF_SCL_PIN,"PCF_SCL_PIN");
|
||||
pinMode(DP_PIN,OUTPUT); regPin(DP_PIN,"DP_PIN");
|
||||
digitalWrite(PCF_SDA_PIN,HIGH);
|
||||
digitalWrite(PCF_SCL_PIN,HIGH); //leave the bus idle (both lines HIGH) before the first transaction
|
||||
delay(100);
|
||||
sendBits(I2C_ADDR,0); //clear all 8 PCF8574 outputs (all tubes disabled) at startup
|
||||
|
||||
startTimer();
|
||||
// Software watchdog re-armed with a generous 8s timeout: the bit-banged I2C
|
||||
// transaction in sendBits() runs with interrupts effectively serialized inside the
|
||||
// ISR and can take a comparatively long time (many delayMS() busy-waits per call),
|
||||
// so the default (shorter) 8266 watchdog period could otherwise trip during startup.
|
||||
ESP.wdtDisable();
|
||||
ESP.wdtEnable(WDTO_8S);
|
||||
}
|
||||
|
||||
void ICACHE_RAM_ATTR writeDisplay(){ //https://circuits4you.com/2018/01/02/esp8266-timer-ticker-example/
|
||||
static byte pos = 0;
|
||||
static byte state=0;
|
||||
static int timer = PWMrefresh;
|
||||
static byte num,brightness;
|
||||
static byte p,DPpos;
|
||||
static int PWMtimeBrightness = PWM_min;
|
||||
|
||||
if (EEPROMsaving) { //stop refresh, while EEPROM write is in progress!
|
||||
//digitalWrite(digitEnablePins[pos],LOW);
|
||||
timer1_write(PWMrefresh);
|
||||
return;
|
||||
}
|
||||
|
||||
// POSSIBLE BUG: PWMtimeBrightness is computed here from `brightness` BEFORE
|
||||
// `brightness` is (re)assigned a few lines below - so this uses the value left
|
||||
// over from the PREVIOUS ISR call, not the current one (a one-call lag whenever
|
||||
// brightness/lx changes). Also, unlike multiplex74141.ino/multiplex74141_ESP32.ino
|
||||
// (which only recompute PWMtimeBrightness once per FULL multiplex cycle, when pos
|
||||
// wraps back to 0, specifically to keep brightness visually uniform across all
|
||||
// tubes within one cycle), this recomputes it on EVERY ISR call regardless of
|
||||
// state/pos - so if autoBrightness/lx changes mid-cycle, different tubes within
|
||||
// the same refresh pass could end up with different PWMtimeBrightness values.
|
||||
if (autoBrightness && displayON) {
|
||||
PWMtimeBrightness = max(PWM_min,PWM_max*lx/MAXIMUM_LUX);
|
||||
}
|
||||
else
|
||||
PWMtimeBrightness = max(PWM_min,PWM_max*brightness/MAXBRIGHTNESS);
|
||||
|
||||
intCounter++;
|
||||
timer = PWMrefresh;
|
||||
brightness = displayON ? prm.dayBright : prm.nightBright;
|
||||
if (brightness>MAXBRIGHTNESS) brightness = MAXBRIGHTNESS; //only for safety
|
||||
|
||||
// Same 4-state per-tube multiplex/animation state machine as multiplex74141.ino -
|
||||
// see that file's header for the full write-up. state 0: advance to next tube
|
||||
// (old digit during a wipe); state 1: new digit for the rest of a wipe; state 2:
|
||||
// blank for the remaining OFF portion of the cycle (brightness dimming); state 3:
|
||||
// one-tick marker consumed immediately below.
|
||||
switch (state) { //state machine...
|
||||
case 0:
|
||||
pos++; if (pos>maxDigits-1) pos = 0; //go to the first tube
|
||||
|
||||
if (animMask[pos] > 0) { //Animation?
|
||||
num = oldDigit[pos]; //show old character
|
||||
timer = (PWMtimeBrightness * (20-animMask[pos]))/20;
|
||||
state = 1; //next state is: show newDigit
|
||||
}
|
||||
else {
|
||||
num = digit[pos]; //show active character
|
||||
timer = PWMtimeBrightness;
|
||||
state = 2; //next state is: BLANK display
|
||||
}
|
||||
break;
|
||||
case 1: //show new character, if animation
|
||||
num = newDigit[pos];
|
||||
timer = (PWMtimeBrightness * animMask[pos])/20;
|
||||
state = 2; //default next state is: BLANK display
|
||||
break;
|
||||
case 2: //blank display
|
||||
timer = PWMrefresh-PWMtimeBrightness;
|
||||
state = 3;
|
||||
break;
|
||||
} //end switch
|
||||
|
||||
// if ((pos>0) && (num<=9)) num = convert[num]; //tube character conversion, if needed... (maybe bad pin numbering)
|
||||
|
||||
p = digitEnablePins[pos]; //resolve this cycle's tube-enable target: <100 = direct ESP GPIO, >=100 = PCF8574 port (p-100)
|
||||
|
||||
if ((brightness == 0) || (state == 3) || (num >9) || (!radarON)) { //blank digit
|
||||
state = 0;
|
||||
// Blanking is done purely by pushing an invalid BCD code (1111=15) to the shared
|
||||
// 74141 - unlike multiplex74141.ino/multiplex74141_ESP32.ino, which additionally
|
||||
// switch the CURRENT tube's digitEnablePins[pos] line LOW here. That matters:
|
||||
// POSSIBLE BUG - `p` (the just-selected tube's enable line) is never explicitly
|
||||
// turned back off, neither here nor anywhere else in this file. For a PCF-hosted
|
||||
// pin (p>=100) this is harmless in practice, because sendBits() below always
|
||||
// rewrites the WHOLE PCF8574 output byte with only the newly-selected tube's bit
|
||||
// set, which implicitly clears every previously-enabled PCF pin. But for a
|
||||
// DIRECT ESP GPIO enable pin (p<100), digitalWrite(p,HIGH) is a single-pin write
|
||||
// with no such "whole port" side effect - once such a pin is first driven HIGH it
|
||||
// is NEVER driven LOW again anywhere in this file. If a clock config mixes direct
|
||||
// and PCF-hosted digitEnablePins (the setup_pins() loop above clearly anticipates
|
||||
// this), every direct-GPIO tube's anode would stay permanently powered after its
|
||||
// first turn, relying solely on the BCD=1111 blanking to keep it dark the rest of
|
||||
// the time - but since ALL tubes share the same 74141/ABCDPins, that direct-GPIO
|
||||
// tube would then visibly ghost (light up showing) whatever digit is currently
|
||||
// being driven for a DIFFERENT tube's turn. The shipped default config
|
||||
// (digitEnablePins[] = {100,101,102,103,104,105}, all PCF-hosted) never exercises
|
||||
// this path, which is presumably why it has gone unnoticed.
|
||||
for (int i=0;i<4;i++) {digitalWrite(ABCDPins[i],HIGH); }
|
||||
digitalWrite(DP_PIN,LOW);
|
||||
}
|
||||
else {
|
||||
if (p<100) digitalWrite(p,HIGH); //switch ON new digit on 8266
|
||||
else sendBits(I2C_ADDR,1<<(p-100)) ; // or on PCF port - single-bit-set byte, see note above
|
||||
// LEFTDECIMAL: some tube types (Z574M) have their DP cathode positioned to the
|
||||
// LEFT of the printed digit, so its DP is lit together with the NEXT tube's slot
|
||||
// instead of the current one (see CLOCK_7's "#define LEFTDECIMAL true" in clocks.h).
|
||||
if (LEFTDECIMAL) DPpos = min(maxDigits-1,pos+1); else DPpos = pos;
|
||||
if (digitDP[DPpos] && (brightness>0)) digitalWrite(DP_PIN,HIGH); //switch ON decimal point, if needed
|
||||
for (int i=0;i<4;i++) {digitalWrite(ABCDPins[i],num & 1<<i); } //push BCD bits (bit i -> ABCDPins[i]) to the shared 74141
|
||||
}
|
||||
|
||||
#if COLON_PIN >= 0
|
||||
if (num==10) digitalWrite(COLON_PIN,LOW); // Colon pin OFF
|
||||
else digitalWrite(COLON_PIN,colonBlinkState); // Blink colon pin
|
||||
#endif
|
||||
|
||||
#if DECIMALPOINT_PIN >=0
|
||||
// POSSIBLE BUG: stray extra "}" at the end of this line - the `else` branch is a
|
||||
// single bare statement (no opening brace), so this closing brace has no match.
|
||||
// Currently harmless only because DECIMALPOINT_PIN is normally left at -1 for
|
||||
// this driver's clock configs, so this whole #if block never actually compiles;
|
||||
// it would very likely fail to compile as soon as a PCF_74141-based config ever
|
||||
// defined DECIMALPOINT_PIN >=0. Compare with the correctly-braced
|
||||
// "else {digitalWrite(DECIMALPOINT_PIN,decimalpointON); }" in multiplex74141.ino.
|
||||
if (num==10) {digitalWrite(DECIMALPOINT_PIN,LOW);}
|
||||
else digitalWrite(DECIMALPOINT_PIN,decimalpointON); }
|
||||
#endif
|
||||
|
||||
if (timer<500) timer = 500; //safety only...
|
||||
timer1_write(timer);
|
||||
}
|
||||
|
||||
void ICACHE_RAM_ATTR clearTubes() {
|
||||
digitalWrite(DP_PIN,LOW);
|
||||
sendBits(I2C_ADDR,0); //zero out all PCF8574 outputs - blanks every PCF-hosted tube; note direct-GPIO (p<100) tubes are NOT addressed here (see the ABCDPins-only comment below)
|
||||
//for (int i=0;i<4;i++) digitalWrite(ABCDPins[i],HIGH);
|
||||
}
|
||||
|
||||
// Not implemented for this driver: the multiplex ISR (writeDisplay) already refreshes
|
||||
// continuously on its own, so there's no separate blocking "write once" path here;
|
||||
// kept only for interface parity with drivers that need it.
|
||||
void writeDisplaySingle() {}
|
||||
#endif
|
||||
@@ -0,0 +1,186 @@
|
||||
// ============================================================================
|
||||
// PWM Tube Backlight subsystem (plain R/G/B PWM, no addressable LEDs)
|
||||
// ----------------------------------------------------------------------------
|
||||
// Drives a single RGB backlight (or 3 separate R/G/B LED strips wired in
|
||||
// parallel) behind/around the tubes using the ESP32 LEDC hardware PWM
|
||||
// peripheral directly on 3 GPIOs (PWM1_PIN/PWM2_PIN/PWM3_PIN = R/G/B). This
|
||||
// is the simpler alternative to the addressable-WS2812 backlight implemented
|
||||
// in z_neopixel.ino (USE_NEOPIXEL): here there is only ONE logical color for
|
||||
// the whole backlight (no per-tube pixel control), computed in software and
|
||||
// pushed out as 3 duty-cycle values instead of a serial LED protocol.
|
||||
//
|
||||
// Enabled per-config via #define USE_PWMLEDS in clocks.h. Currently the only
|
||||
// config using it is CLOCK_54 (D1 R32 ESP32 + NCS312 Nixie clock KIT) - grep
|
||||
// clocks.h for "#define USE_PWMLEDS" for the authoritative, current list.
|
||||
// USE_NEOPIXEL and USE_PWMLEDS are meant to be mutually exclusive per clock
|
||||
// config (CLOCK_54 keeps USE_NEOPIXEL commented out) - see the ESP8266/ESP32
|
||||
// and "both defined" caveats in the POSSIBLE BUG notes below.
|
||||
//
|
||||
// LEDC is an ESP32-only peripheral (esp32-hal-ledc.h: ledcAttachPin/
|
||||
// ledcSetup/ledcWrite have no ESP8266 equivalent). Pin setup (pinMode +
|
||||
// ledcAttachPin + ledcSetup, 200Hz/8-bit per channel) happens once in
|
||||
// ESP32_UniClock2.ino's setup(), NOT in this file - this file only ever
|
||||
// calls ledcWrite() to update duty cycle. See setup()'s "#ifdef USE_PWMLEDS"
|
||||
// block (channels 0/1/2 = R/G/B) for the actual pin/peripheral configuration.
|
||||
//
|
||||
// Called from the core program (ESP32_UniClock2.ino):
|
||||
// - doAnimationPWM() : every loop() iteration, AND from inside Fdelay()'s
|
||||
// busy-wait loop, so the backlight keeps animating
|
||||
// even while tube digits are mid-transition (Fdelay
|
||||
// blocks for the tube-switch duration). It rate-limits
|
||||
// itself internally (see FPS_MSEC / prm.rgbSpeed),
|
||||
// mirroring doAnimationMakuna() in z_neopixel.ino.
|
||||
//
|
||||
// Only a subset of prm's RGB settings is used here compared to the fuller
|
||||
// z_neopixel.ino: prm.rgbEffect (0=off, 1=fixed color, anything else=
|
||||
// rainbow flow), prm.rgbBrightness, prm.rgbFixColor, prm.rgbSpeed and
|
||||
// prm.rgbDir. There is no equivalent here of z_neopixel's effect1/effect2/
|
||||
// random-color modes - this file only ever shows "off", a single fixed
|
||||
// color, or one continuous rainbow sweep.
|
||||
// ============================================================================
|
||||
|
||||
#ifdef USE_PWMLEDS
|
||||
|
||||
#define FPS 60 //frames per second, animation frame-rate cap (matches z_neopixel.ino)
|
||||
#define FPS_MSEC 1000/FPS //ms per animation frame = minimum interval between doAnimationPWM() updates
|
||||
#define COLORSATURATION 255 //POSSIBLE BUG (minor/dead code): defined but never referenced in this file - leftover from z_neopixel.ino where it sizes RgbColor constants
|
||||
#define WHITE_INDEX 192 //reserved "color wheel" position (out of 0..255) that setPWMcolor() special-cases to pure white instead of a hue
|
||||
|
||||
long pwmBrightness;
|
||||
int pwmColorStep = 1; //POSSIBLE BUG (dead code, same pattern already noted in z_neopixel.ino's colorStep): computed from prm.rgbSpeed in doAnimationPWM() but never actually used to step the rainbow index - pwmRainbow() always advances j by exactly 1 per call, so animation speed is controlled only via the frame-timing gate below, not via this variable
|
||||
int pwmRed,pwmGreen,pwmBlue = 0; //POSSIBLE BUG (dead code): these globals are always shadowed by the same-named local variables declared in setPWMcolor() below, so they are never actually read or written - harmless (ints default-init to 0) but misleading, and the "= 0" here only initializes pwmBlue, not pwmRed/pwmGreen (equivalent for globals, which zero-init anyway, but worth noting the syntax doesn't do what it looks like)
|
||||
|
||||
// Converts a "color wheel" position (0..255 hue, or the special WHITE_INDEX,
|
||||
// or <0 for "off") plus a brightness (0..255) into R/G/B PWM duty cycles and
|
||||
// writes them out via LEDC. Same hue math as z_neopixel.ino's Wheel(), just
|
||||
// inlined here and applied directly to 3 PWM channels instead of an RgbColor.
|
||||
void setPWMcolor(int WheelPos, byte brightness) {
|
||||
#ifdef USE_PWMLEDS
|
||||
|
||||
int pwmRed,pwmGreen,pwmBlue; //local color components (deliberately shadow the unused globals above)
|
||||
|
||||
if (WheelPos == WHITE_INDEX) {
|
||||
pwmRed = 255; pwmGreen = 255; pwmBlue = 255;
|
||||
}
|
||||
else if (WheelPos<0) {
|
||||
pwmRed =0; pwmGreen = 0; pwmBlue = 0; //WheelPos<0 is the "backlight off" sentinel used by doAnimationPWM()/pwmAlarmLight()
|
||||
}
|
||||
else {
|
||||
// Standard 3-band color wheel: 0..255 hue split into 3 rising/falling
|
||||
// ramps (R->B, B->G, G->R) of 85 steps each, *3 to scale 0..85 up to 0..255.
|
||||
WheelPos = 255 - WheelPos;
|
||||
if(WheelPos < 85) {
|
||||
pwmRed =255 - WheelPos * 3; pwmGreen = 0; pwmBlue = WheelPos * 3;
|
||||
} else if(WheelPos < 170) {
|
||||
WheelPos -= 85;
|
||||
pwmRed = 0, pwmGreen = WheelPos * 3; pwmBlue = 255 - WheelPos * 3;
|
||||
} else {
|
||||
WheelPos -= 170;
|
||||
pwmRed = WheelPos * 3; pwmGreen = 255 - WheelPos * 3; pwmBlue = 0;
|
||||
}
|
||||
}
|
||||
|
||||
// Scale each color channel by brightness (0..255) and push out as PWM duty
|
||||
// cycle. Channel numbers (0/1/2) must match the ledcAttachPin() channel
|
||||
// assignment done once in ESP32_UniClock2.ino's setup() - this file does
|
||||
// not own pin/channel setup, only per-frame duty-cycle updates.
|
||||
#if PWM1_PIN>=0
|
||||
ledcWrite(0, pwmRed * brightness / 255);
|
||||
#endif
|
||||
#if PWM2_PIN>=0
|
||||
ledcWrite(1, pwmGreen * brightness / 255);
|
||||
#endif
|
||||
#if PWM3_PIN>=0
|
||||
ledcWrite(2, pwmBlue * brightness / 255);
|
||||
#endif
|
||||
#endif
|
||||
}
|
||||
|
||||
|
||||
// Continuous rainbow sweep ("flow" effect, prm.rgbEffect >= 2): walks the
|
||||
// color wheel index j by exactly 1 step per call (see pwmColorStep note
|
||||
// above - prm.rgbSpeed only affects how often doAnimationPWM() calls this,
|
||||
// not the step size), wrapping via byte overflow at 0/255. Direction is set
|
||||
// by prm.rgbDir.
|
||||
void pwmRainbow() {
|
||||
static byte j=0;
|
||||
|
||||
setPWMcolor(j,pwmBrightness);
|
||||
if (prm.rgbDir) { j++; }
|
||||
else { j--; }
|
||||
}
|
||||
|
||||
// Alarm indicator: while alarmON, ignore the normal effect/color settings and
|
||||
// blink the backlight white/off at full brightness every 500ms (2 calls per
|
||||
// on/off cycle = 1Hz blink), regardless of prm.rgbBrightness, so the alarm is
|
||||
// clearly visible even if the user configured a dim or off backlight.
|
||||
void pwmAlarmLight() {
|
||||
static unsigned long lastRun = 0;
|
||||
static byte counter;
|
||||
|
||||
if ((millis()-lastRun)<500) return;
|
||||
lastRun = millis();
|
||||
counter++;
|
||||
if (counter%2)
|
||||
setPWMcolor(WHITE_INDEX,255);
|
||||
else
|
||||
setPWMcolor(-1,0);
|
||||
}
|
||||
|
||||
|
||||
// Main entry point, called every loop()/Fdelay() iteration (see file header).
|
||||
// Self-throttles internally so it's safe to call at arbitrarily high
|
||||
// frequency - the actual color update rate is capped well below the caller's
|
||||
// call frequency.
|
||||
void doAnimationPWM() {
|
||||
static unsigned long lastRun = 0;
|
||||
|
||||
if (EEPROMsaving) return; //don't touch PWM/SPI-ish timing while an EEPROM write is in progress
|
||||
if (alarmON) {
|
||||
pwmAlarmLight();
|
||||
return;
|
||||
}
|
||||
|
||||
// Fixed color (or OFF) doesn't need 60fps updates - only re-check once a
|
||||
// second (e.g. in case displayON/radarON toggled) instead of every frame.
|
||||
if ((prm.rgbEffect <=1) && ((millis()-lastRun)<1000)) return; //fix color
|
||||
// Animated (rainbow) effect: gate to a frame rate derived from prm.rgbSpeed
|
||||
// (0..255), from ~60fps (FPS_MSEC, fast/high speed) down to ~3ms floor at
|
||||
// max speed and up to ~258ms at speed=0 (slowest). max() with FPS_MSEC caps
|
||||
// the update rate at 60fps even for high rgbSpeed values; 258-rgbSpeed can't
|
||||
// go negative since rgbSpeed is a byte (0..255).
|
||||
if ((millis()-lastRun)<max(FPS_MSEC,258-prm.rgbSpeed)) return;
|
||||
lastRun = millis();
|
||||
|
||||
if ((prm.rgbEffect == 0) || !displayON || !radarON) { //switch RGB backlight OFF: user disabled it, display is manually off, or radar/presence timed out
|
||||
pwmBrightness = 0;
|
||||
setPWMcolor(-1,0);
|
||||
return;
|
||||
}
|
||||
|
||||
pwmColorStep = max(1,prm.rgbSpeed/5); //see POSSIBLE BUG note above - computed but currently unused
|
||||
pwmBrightness = prm.rgbBrightness;
|
||||
if (autoBrightness) {
|
||||
// Scale brightness down proportionally to ambient light (lx) read from
|
||||
// the light sensor, floored at c_MinBrightness so the backlight never
|
||||
// goes fully invisible in auto-dim mode; halved (/2) vs. the raw
|
||||
// lx/MAXIMUM_LUX ratio so the backlight dims more aggressively than the
|
||||
// tube brightness curve (matches the same /2 in z_neopixel.ino).
|
||||
pwmBrightness = (pwmBrightness * lx) /(long) MAXIMUM_LUX/2;
|
||||
if (pwmBrightness< c_MinBrightness)
|
||||
pwmBrightness = c_MinBrightness;
|
||||
}
|
||||
pwmBrightness = min(pwmBrightness,long(RGB_MAX_BRIGHTNESS)); //safety only
|
||||
|
||||
//DPRINTLN(" NeoBrightness:"); DPRINT(neoBrightness);
|
||||
|
||||
if (prm.rgbEffect==1) setPWMcolor(prm.rgbFixColor,pwmBrightness);
|
||||
else pwmRainbow(); //flow
|
||||
}
|
||||
|
||||
#else
|
||||
// USE_PWMLEDS not defined for this clock config: provide a no-op stub so the
|
||||
// unconditional doAnimationPWM() calls in ESP32_UniClock2.ino's loop()/
|
||||
// Fdelay() still link even when this subsystem is compiled out entirely.
|
||||
void doAnimationPWM() {}
|
||||
#endif
|
||||
@@ -0,0 +1,534 @@
|
||||
#ifdef USE_RTC
|
||||
//====================================================================================================
|
||||
// rtc.ino - DS3231 realtime clock driver, with optional manual clock-setup pushbuttons
|
||||
//====================================================================================================
|
||||
// Chip: Maxim/Analog DS3231 I2C RTC (temperature-compensated crystal oscillator, +-2ppm accuracy).
|
||||
// Talked to via the Makuna "Rtc_by_Makuna" library (RtcDS3231<TwoWire>), see
|
||||
// libraries/Rtc_by_Makuna.zip. Uses the Arduino `Wire` (I2C) object, so it shares the
|
||||
// I2C bus with any other I2C sensors defined for the clock (see I2Csensors.ino).
|
||||
// Wiring: I2C bus only - PIN_SDA / PIN_SCL, set per-CLOCK_NN in clocks.h (any GPIO pair works,
|
||||
// it's not necessarily the chip's "default" I2C pins). Presence is auto-detected at
|
||||
// startup by I2Csensors.ino (probes address 0x68) and reported in the RTCexist flag.
|
||||
// Role in the system: the DS3231 is a battery-backed FALLBACK time source, not the primary one.
|
||||
// NTP (WiFi) and/or GPS are preferred whenever available; the RTC is only read to set
|
||||
// the running clock when neither NTP nor GPS refreshed the time on that update cycle,
|
||||
// and it is written to (kept in sync) whenever NTP/GPS DID refresh the time - so after
|
||||
// a power loss/reset with no network, the clock still starts from a decent time.
|
||||
// See calcTime() in ESP32_UniClock2.ino:
|
||||
// if (RTCexist) { if (refreshed || refreshed2) updateRTC(); else getRTC(); }
|
||||
// Manual set: on clocks that define PIN_FLD_BUTTON/PIN_SET_BUTTON (and optionally
|
||||
// PIN_MODE_SWITCH), a long-press on the FLD button enters/leaves a manual date/time
|
||||
// editor (editor()/showValue()); the SET button changes the value of the currently
|
||||
// selected field. See scanButFLD()/scanButSET() below for the button state machine.
|
||||
// Used by: CLOCK_5 (IVL-2 VFD clock, MAX6921, has RTC+buttons)
|
||||
// CLOCK_15a (IV-6 VFD clock RGB)
|
||||
// CLOCK_17 (PT6311 IV-22 russian clock)
|
||||
// CLOCK_35 (Z566M/IN-1 tubes, non-multiplex driver)
|
||||
// CLOCK_52 (2xHV5122 IN18 clock, PROTOTYPE)
|
||||
// CLOCK_71 (2x VQC10 clock - USE_RTC defined here only to also enable the pushbutton
|
||||
// code below; this build has no physical DS3231, RTCexist stays false)
|
||||
// CLOCK_80/81/82 (word clocks EN/DE/HU - RTC only, PIN_FLD_BUTTON/PIN_SET_BUTTON are
|
||||
// commented out on these builds, so the manual editor is never entered)
|
||||
// (CLOCK_50 and CLOCK_99 have USE_RTC available but commented out by default)
|
||||
//====================================================================================================
|
||||
#include <RtcDS3231.h> //Makuna/RTC
|
||||
RtcDS3231<TwoWire> Rtc(Wire);
|
||||
RtcDateTime rtcNow;
|
||||
RtcDateTime compiled = RtcDateTime(__DATE__, __TIME__); //firmware build timestamp, used as a sane fallback date if the RTC ever loses confidence (e.g. dead backup battery)
|
||||
#define EPOCH_OFFSET 946684800l //offset from 1970->2000 epoch start; the Makuna library's RtcDateTime counts seconds since 2000-01-01, while TimeLib's now() is a standard Unix epoch (1970) - subtract this constant to convert Unix-epoch -> DS3231 epoch
|
||||
//------ Mode Switch and Push Buttons ---------------------
|
||||
#ifndef PIN_MODE_SWITCH
|
||||
#define PIN_MODE_SWITCH -1 //-1 = "not wired on this clock", all buttoncode below is compiled out via the #if PIN_xxx>=0 guards
|
||||
#endif
|
||||
|
||||
#ifndef PIN_FLD_BUTTON
|
||||
#define PIN_FLD_BUTTON -1
|
||||
#endif
|
||||
#ifndef PIN_SET_BUTTON
|
||||
#define PIN_SET_BUTTON -1
|
||||
#endif
|
||||
|
||||
boolean pushedFldButtonValue = LOW; //which logic level means "button pushed"
|
||||
boolean pushedSetButtonValue = LOW; //which logic level means "button pushed"
|
||||
|
||||
//------- I2C bus definition Any pins are usable Please, SET in clocks.h --------
|
||||
//#define PIN_SDA 4 //D2 original general setup for 8266
|
||||
//#define PIN_SCL 5 //D1
|
||||
|
||||
#define MENU_UNSELECT_TIMEOUT 30000 //NOTE: defined but not actually read/used anywhere in this file (LastModify is set on every keypress but never compared against this timeout) - the editor only exits via a long FLD press, not by an idle timeout. Left in place for a possible future auto-exit-on-idle feature.
|
||||
|
||||
int monthdays[13] = {0,31,29,31,30,31,30,31,31,30,31,30,31}; //days-per-month lookup (index 1-12, Feb hardcoded to 29/leap); NOTE: not currently referenced anywhere in this file - no leap-year/day-overflow validation is actually performed when editing the Day field (see scanButSET())
|
||||
|
||||
struct Menu {
|
||||
short lowlimit; // lower limit for this field's value
|
||||
short uplimit; // upper limit for this field's value
|
||||
char name[5]; // short label shown on the display while editing this field
|
||||
};
|
||||
|
||||
// Editable fields for manual time setting, indexed by `fld` (1..MAXFLD). Index 0 is a dummy/unused
|
||||
// "no field selected" placeholder so `fld==0` can mean "editor not active" without needing -1 checks.
|
||||
const struct Menu m1[]= {
|
||||
{0,0," "},
|
||||
{2022,2099,"Year"},
|
||||
{1,12,"Mon "},
|
||||
{1,31,"Day "}, //NOTE: fixed 1..31 range regardless of month/leap-year - editing Feb 30 or Apr 31 is not prevented, see scanButSET()
|
||||
{0,23,"Hour"},
|
||||
{0,59,"Min "},
|
||||
};
|
||||
|
||||
#define MAXFLD 5
|
||||
|
||||
int mvar[MAXFLD+1]; //working copy of the field being edited: mvar[1]=year, [2]=month, [3]=day, [4]=hour, [5]=minute (seconds are always reset to 0 on save, see saveRTC())
|
||||
|
||||
// Pushbutton debounce/press-length state machine (used identically by both FLD and SET buttons):
|
||||
// IS_OPEN idle, button released
|
||||
// IS_PUSHING/PUSHED button just went down (one extra state to timestamp the press)
|
||||
// IS_LONGPRESSING/-ED button has been held past the long-press threshold (~2s) - action fires once, on entry
|
||||
// IS_OPENING button just released - if this happened quickly (<500ms since push) it's a "short press"
|
||||
// IS_LONGOPENING button released after a long press - swallow the release so it isn't also seen as a short press
|
||||
enum switchStates {IS_OPEN, IS_OPENING, IS_PUSHED, IS_LONGPRESSING, IS_LONGPRESSED, IS_PUSHING, IS_LONGOPENING};
|
||||
|
||||
int fld = 0; //0 = manual editor not active; 1..MAXFLD = index into m1[]/mvar[] of the field currently being edited
|
||||
unsigned long LastModify = 0; //timestamp of the last field change while editing; set by scanButFLD/scanButSET, only used as a "was anything changed?" flag by editor() to decide whether saveRTC() is needed on exit (NOT used for the MENU_UNSELECT_TIMEOUT idle-exit that its name might suggest)
|
||||
|
||||
void setupRTC() {
|
||||
#if PIN_MODE_SWITCH>=0
|
||||
pinMode(PIN_MODE_SWITCH,INPUT_PULLUP);
|
||||
regPin(PIN_MODE_SWITCH,"PIN_MODE_SWITCH");
|
||||
#endif
|
||||
#if PIN_FLD_BUTTON>=0
|
||||
pinMode(PIN_FLD_BUTTON,INPUT_PULLUP);
|
||||
regPin(PIN_FLD_BUTTON,"PIN_FLD_BUTTON");
|
||||
// Auto-detect button polarity at boot: with INPUT_PULLUP the idle (unpressed) line should read
|
||||
// HIGH. If it reads LOW here instead, the wiring/hardware inverts the logic (e.g. external
|
||||
// pull-down, or the button is wired to the pin differently than the internal-pullup default),
|
||||
// so remember that for this board "pressed" actually reads HIGH, not LOW.
|
||||
// NOTE: this assumes the button is NOT physically held down at power-on - if it were, polarity
|
||||
// detection here would be wrong for the rest of the session.
|
||||
if (digitalRead(PIN_FLD_BUTTON) == LOW) //inverted button?
|
||||
pushedFldButtonValue = HIGH;
|
||||
#endif
|
||||
#if PIN_SET_BUTTON>=0
|
||||
pinMode(PIN_SET_BUTTON,INPUT_PULLUP);
|
||||
regPin(PIN_SET_BUTTON,"PIN_SET_BUTTON");
|
||||
if (digitalRead(PIN_SET_BUTTON) == LOW) //inverted button?
|
||||
pushedSetButtonValue = HIGH;
|
||||
#endif
|
||||
|
||||
if (RTCexist) {
|
||||
DPRINTLN("Connecting to RTC clock...");
|
||||
Rtc.Begin();
|
||||
if (!Rtc.IsDateTimeValid()) {
|
||||
// IsDateTimeValid() also surfaces DS3231-specific fault conditions (oscillator stop flag,
|
||||
// invalid register values) via LastError(), distinct from a plain I2C communication failure.
|
||||
if (Rtc.LastError() != 0) {
|
||||
// A nonzero LastError() here means the I2C transaction itself failed (bus error / chip
|
||||
// not responding) - do NOT touch the RTC's stored time in that case, since we can't
|
||||
// trust what we'd be reading or writing over a broken bus.
|
||||
DPRINT("RTC communications error = "); DPRINTLN(Rtc.LastError());
|
||||
}
|
||||
else {
|
||||
// I2C is fine, but the chip reports its own datetime as invalid - typically means the
|
||||
// DS3231's Oscillator Stop Flag (OSF) is set, i.e. the coin-cell backup battery died (or
|
||||
// is missing/depleted) at some point and the chip lost power, so its date/time can no
|
||||
// longer be trusted. Reseed it with the firmware's own compile timestamp as a "better
|
||||
// than nothing" starting point until NTP/GPS can correct it.
|
||||
DPRINTLN("RTC lost confidence in the DateTime!");
|
||||
Rtc.SetDateTime(compiled); //Set RTC time to firmware compile time
|
||||
}
|
||||
}
|
||||
|
||||
if (!Rtc.GetIsRunning()) {
|
||||
// The DS3231 has an Enable Oscillator (EOSC) bit that can leave the clock halted even
|
||||
// though the chip itself answers on I2C - make sure the oscillator/counting is actually on.
|
||||
DPRINTLN("RTC was not actively running, starting now");
|
||||
Rtc.SetIsRunning(true);
|
||||
}
|
||||
|
||||
// Disable the 32kHz output and the SQW/INT pin's square-wave function - this firmware never
|
||||
// uses the DS3231 as an interrupt/alarm source, so both are turned off to save a little power
|
||||
// and avoid an unused, possibly-floating/toggling pin on the board.
|
||||
Rtc.Enable32kHzPin(false);
|
||||
Rtc.SetSquareWavePin(DS3231SquareWavePin_ModeNone);
|
||||
}
|
||||
/*
|
||||
while (true) {
|
||||
DPRINT(digitalRead(PIN_MODE_SWITCH)); DPRINT(" / ");
|
||||
DPRINT(digitalRead(PIN_FLD_BUTTON)); DPRINT(" / ");
|
||||
DPRINTLN(digitalRead(PIN_SET_BUTTON));
|
||||
delay(1000);
|
||||
}
|
||||
*/
|
||||
}
|
||||
|
||||
|
||||
// Called once per main loop() iteration (unconditionally, whenever USE_RTC is defined). Most of the
|
||||
// time this is a cheap no-op: it just polls the FLD button once (scanButFLD) and returns immediately
|
||||
// because fld==0 (editor not active). Only when a long FLD-press flips fld to nonzero does it drop
|
||||
// into the inner while(true) loop below - and while that loop runs, it BLOCKS the rest of loop() (no
|
||||
// WiFi/animation/sensor/etc. processing happens) until the user long-presses FLD again to save & exit.
|
||||
void editor() {
|
||||
static unsigned long lastDisplay = 0;
|
||||
if (RTCexist) {rtcNow = Rtc.GetDateTime();} //note: read but not otherwise used here - editor() re-reads current time into mvar[] via getRTC() only at the moment edit mode is entered (see scanButFLD's IS_LONGPRESSING case)
|
||||
LastModify = 0;
|
||||
#if PIN_FLD_BUTTON >= 0 && PIN_SET_BUTTON >= 0 //Are buttons installed?
|
||||
editorRunning = true; //flag read elsewhere (e.g. wordclock.ino) to show an "editing" indicator on the display
|
||||
while (true) {
|
||||
scanButFLD(100); //100ms poll interval for both buttons - also doubles as the blocking loop's own pacing/refresh rate
|
||||
if (fld == 0) {
|
||||
editorRunning = false;
|
||||
break;
|
||||
}
|
||||
scanButSET(100);
|
||||
showValue();
|
||||
#if defined(WORDCLOCK)
|
||||
if ((millis()-lastDisplay)>100) {
|
||||
lastDisplay = millis();
|
||||
writeDisplay2();
|
||||
}
|
||||
#else
|
||||
writeDisplaySingle();
|
||||
#endif
|
||||
printDigits(1000);
|
||||
Fdelay(100);
|
||||
}
|
||||
#endif
|
||||
if (LastModify != 0) saveRTC(); //only persist if at least one field was actually changed during this edit session
|
||||
}
|
||||
|
||||
// Renders the value of the field currently being edited (mvar[fld]) into the shared `digit[]`
|
||||
// display buffer (digit 10 = blank, used by memset to clear unused positions first).
|
||||
void showValue() {
|
||||
memset(digit,10,sizeof(digit)); //clear array
|
||||
memset(digitDP,0,sizeof(digitDP));
|
||||
if(fld==1) {
|
||||
//Year is the only 4-digit field (e.g. 2026) - split into thousands/hundreds/tens/units across digit[0..3]
|
||||
digit[3] = mvar[1] / 1000;
|
||||
digit[2] = (mvar[1] % 1000) / 100;
|
||||
digit[1] = (mvar[1] % 100) / 10;
|
||||
digit[0] = mvar[1] % 10;
|
||||
}
|
||||
else {
|
||||
//Month/Day/Hour/Minute are all 1-2 digit values - tens/units into digit[0..1]
|
||||
digit[1] = mvar[fld] / 10;
|
||||
digit[0] = mvar[fld] % 10;
|
||||
}
|
||||
#if defined(MAX6921) || defined(MAX6921_ESP32)
|
||||
//character set not available yet (MAX6921-driven displays can't render the alphabetic field-name label below, only digits)
|
||||
#else
|
||||
// On displays with more than 4 digit positions, show the field's short text label (e.g. "Year",
|
||||
// "Hour") in the upper digit positions alongside the numeric value, so the user knows which field
|
||||
// they're editing. Needs >=6 digits to fit 3 label characters + a ':' separator, or >=8 to fit the
|
||||
// full 4-character label without the ':' filler.
|
||||
if (!digitsOnly && (maxDigits>4)) {
|
||||
if (maxDigits>=8) digit[7] = m1[fld].name[0];
|
||||
if (maxDigits>=8) digit[6] = m1[fld].name[1];
|
||||
if (maxDigits>=6) digit[5] = m1[fld].name[2];
|
||||
else digit[5] = ':'; //not enough digit positions for the 3rd label character - show a separator instead
|
||||
digit[4] = m1[fld].name[3];
|
||||
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
//------------------------------------- RTC Clock functions --------------------------------------------------------------------
|
||||
// Called from calcTime() (ESP32_UniClock2.ino) ONLY on cycles where the system time (TimeLib's
|
||||
// now()) was just refreshed from NTP and/or GPS this same cycle - i.e. "we just got a trustworthy
|
||||
// time from the network/satellites, so push it into the battery-backed RTC to keep it in sync as
|
||||
// the fallback source for the next time we DON'T have network/GPS (e.g. after a reboot)."
|
||||
void updateRTC() {
|
||||
DPRINTLN("Update RTC?");
|
||||
if (!RTCexist) return;
|
||||
if (year()<2020) return; //invalid system date?? sanity guard so a not-yet-synced/garbage system clock can never be written into the RTC
|
||||
|
||||
rtcNow = Rtc.GetDateTime();
|
||||
// Only actually write the RTC if it disagrees with the system time by more than the noise you'd
|
||||
// expect from normal read/processing latency (>2s on seconds, or any mismatch on minute/hour/
|
||||
// day/month/year) - avoids needless I2C writes to the RTC on every single refresh cycle.
|
||||
if (abs(rtcNow.Second() - second())>2 || (rtcNow.Minute() != minute()) || (rtcNow.Hour() != hour())
|
||||
|| (rtcNow.Day() != day()) || (rtcNow.Month() != month()) || (rtcNow.Year() != year()) ) {
|
||||
DPRINTLN("Updating RTC from TimeServer...");
|
||||
DPRINT("- DS3231 date:"); DPRINT(rtcNow.Year()); DPRINT("/"); DPRINT(rtcNow.Month()); DPRINT("/"); DPRINT(rtcNow.Day());
|
||||
DPRINT(" time:"); DPRINT(rtcNow.Hour()); DPRINT(":"); DPRINT(rtcNow.Minute()); DPRINT(":"); DPRINTLN(rtcNow.Second());
|
||||
DPRINT("- Tserver date:"); DPRINT(year()); DPRINT("/"); DPRINT(month()); DPRINT("/"); DPRINT(day());
|
||||
DPRINT(" time:"); DPRINT(hour()); DPRINT(":"); DPRINT(minute()); DPRINT(":"); DPRINTLN(second());
|
||||
Rtc.SetDateTime(now()-EPOCH_OFFSET); //update RTC with UNIX epoch timestamp to 2000year epoch
|
||||
} //endif
|
||||
}
|
||||
|
||||
|
||||
// Called from calcTime() on cycles where NTP/GPS did NOT refresh the time this cycle - the DS3231
|
||||
// is the fallback: read its current date/time and push it into TimeLib via setTime() so the rest
|
||||
// of the firmware (display, animations, etc., all of which read time via now()/hour()/etc.) sees a
|
||||
// continuously-updated clock even with no network/GPS. Also refreshes mvar[] with the (now current)
|
||||
// date/time fields, which is what the manual editor uses as its starting point when entered next
|
||||
// (see scanButFLD's IS_LONGPRESSING case calling getRTC() to seed mvar[] with "now").
|
||||
void getRTC() {
|
||||
if (RTCexist) {
|
||||
if (!Rtc.IsDateTimeValid()) {
|
||||
if (Rtc.LastError() != 0) {
|
||||
// I2C communication problem - do not touch system time this cycle; whatever TimeLib
|
||||
// already has (from the last successful update) is left running/ticking on its own.
|
||||
DPRINT("RTC communications error = "); DPRINTLN(Rtc.LastError());
|
||||
}
|
||||
else {
|
||||
// Oscillator Stop Flag set (dead/missing backup battery, or never-yet-set RTC) - the
|
||||
// chip's stored date/time can't be trusted, so it is deliberately NOT read into
|
||||
// setTime() here; system time is simply left as-is for this cycle.
|
||||
DPRINTLN("RTC lost confidence in the DateTime!");
|
||||
}
|
||||
}
|
||||
else {
|
||||
rtcNow = Rtc.GetDateTime();
|
||||
//DPRINT("\nGet DS3231 data&time: "); DPRINT(rtcNow.Year()); DPRINT("/"); DPRINT(rtcNow.Month()); DPRINT("/"); DPRINT(rtcNow.Day());
|
||||
//DPRINT(" "); DPRINT(rtcNow.Hour()); DPRINT(":"); DPRINT(rtcNow.Minute()); DPRINT(":"); DPRINTLN(rtcNow.Second());
|
||||
setTime(rtcNow.Hour(),rtcNow.Minute(),rtcNow.Second(),rtcNow.Day(),rtcNow.Month(),rtcNow.Year()); //set the time (hr,min,sec,day,mnth,yr)
|
||||
}
|
||||
}
|
||||
// NOTE: mvar[] is refreshed from now()/year()/month()/etc. below unconditionally, even when
|
||||
// RTCexist is false (or the RTC read above was skipped due to an invalid/error state) - in that
|
||||
// case it simply reflects whatever TimeLib's current system time already is (e.g. last NTP time,
|
||||
// or millis()-derived free-running time), which is a reasonable "best current guess" starting
|
||||
// point for the manual editor on hardware with no RTC chip (see CLOCK_71 in clocks.h).
|
||||
mvar[1] = year(); mvar[2] = month(); mvar[3] = day();
|
||||
mvar[4] = hour(); mvar[5] = minute();
|
||||
}
|
||||
|
||||
// Commits the values edited via the manual FLD/SET buttons (mvar[1..5]) back into the running
|
||||
// system clock, and - if present - into the DS3231 as well. Called once, when the editor is exited
|
||||
// (long FLD press while fld>0), only if at least one field was actually changed (see editor()).
|
||||
void saveRTC() {
|
||||
setTime(mvar[4],mvar[5],0,mvar[3],mvar[2],mvar[1]); //set the time (hr,min,sec,day,mnth,yr) - seconds are always reset to :00 since there's no seconds field in the editor
|
||||
|
||||
if (RTCexist) {
|
||||
DPRINTLN("Updating RTC from Manual settings...");
|
||||
Rtc.SetDateTime(now()-EPOCH_OFFSET);
|
||||
}
|
||||
DPRINT("New Date & Time:"); DPRINT(year()); DPRINT("/"); DPRINT(month()); DPRINT("/"); DPRINT(day());
|
||||
DPRINT(" time:"); DPRINT(hour()); DPRINT(":"); DPRINT(minute()); DPRINT(":"); DPRINTLN(second());
|
||||
}
|
||||
|
||||
//-------------------------- check buttons ------------------------------------------------
|
||||
// FLD ("field") button:
|
||||
// - Long press (>2s) toggles the editor: OFF->ON (loads current time into mvar[] via getRTC(),
|
||||
// fld becomes 1 = "Year"); ON->OFF (saves mvar[] back via saveRTC(), fld becomes 0).
|
||||
// - Short press (<500ms) while already editing (fld>0) advances to the next field, wrapping
|
||||
// Year->Month->Day->Hour->Minute->Year... Has no effect while the editor is closed (fld==0),
|
||||
// since a short press there does nothing but a long press is needed to open it.
|
||||
// Debounce: state changes are only evaluated once per `mill` ms (called with 100ms from editor()),
|
||||
// so a button transition is only "seen" after at least one 100ms tick, which is enough to reject
|
||||
// mechanical contact bounce without any extra filtering.
|
||||
void scanButFLD(unsigned long mill) {
|
||||
static unsigned long lastRun = millis();
|
||||
static unsigned long lastPush = millis();;
|
||||
static switchStates butState;
|
||||
byte sw;
|
||||
|
||||
if (PIN_FLD_BUTTON<0) return;
|
||||
if ((millis() - lastRun) < mill) return; //refresh rate
|
||||
lastRun = millis();
|
||||
|
||||
sw = digitalRead(PIN_FLD_BUTTON);
|
||||
if (pushedFldButtonValue == HIGH) sw = !sw; //inverted button logic - normalize so downstream code can always treat LOW as "pressed"
|
||||
|
||||
switch (butState) {
|
||||
case IS_OPEN:
|
||||
if(sw == LOW) butState = IS_PUSHING;
|
||||
break;
|
||||
case IS_PUSHING:
|
||||
butState = IS_PUSHED;
|
||||
lastPush = millis(); //timestamp the press; used both for the 2s long-press threshold and the 500ms short-press classification on release
|
||||
DPRINTLN(" PUSHED");
|
||||
break;
|
||||
case IS_PUSHED:
|
||||
if(sw == HIGH) butState = IS_OPENING;
|
||||
if((sw == LOW) && ((millis()-lastPush)>2000)) butState = IS_LONGPRESSING;
|
||||
break;
|
||||
case IS_LONGPRESSING:
|
||||
// Entered exactly once per long press (state immediately advances to IS_LONGPRESSED below),
|
||||
// so this toggle fires a single time per press-and-hold, not repeatedly while held.
|
||||
butState = IS_LONGPRESSED;
|
||||
DPRINTLN(" IS_LONGPRESSED");
|
||||
if (fld>0) {
|
||||
fld = 0;
|
||||
saveRTC(); //leaving edit mode: commit mvar[] to system clock + RTC chip
|
||||
}
|
||||
else {
|
||||
fld = 1;
|
||||
getRTC(); //entering edit mode: seed mvar[] with the current date/time (field 1 = Year)
|
||||
}
|
||||
break;
|
||||
case IS_LONGPRESSED:
|
||||
if (sw == HIGH) butState = IS_LONGOPENING;
|
||||
break;
|
||||
case IS_OPENING:
|
||||
butState = IS_OPEN;
|
||||
if ((millis()-lastPush)<500) {
|
||||
//short press (press+release completed within 500ms of the initial push) - only meaningful while editing (fld>0), since fld is unaffected when already 0
|
||||
LastModify = millis();
|
||||
fld++; if (fld>MAXFLD) fld = 1; //wraps back to field 1 (Year), never to 0 - the editor can only be exited via a long press, not by cycling fields
|
||||
DPRINT(m1[fld].name); DPRINTLN(fld);
|
||||
} //endif millis()
|
||||
break;
|
||||
case IS_LONGOPENING:
|
||||
// Swallow the release-after-long-press here so it isn't misread as an additional short press
|
||||
// once control returns to IS_OPEN.
|
||||
butState = IS_OPEN;
|
||||
break;
|
||||
} //end switch
|
||||
}
|
||||
|
||||
// SET button: changes the value of whichever field FLD currently has selected (mvar[fld]).
|
||||
// - Short press (<500ms): increments the value by 1 (with wrap-around at m1[fld].lowlimit/uplimit).
|
||||
// - Held past the long-press threshold (2s): auto-repeats the +1 increment on every subsequent
|
||||
// 100ms poll tick for as long as the button stays down (see IS_LONGPRESSED case below) - lets
|
||||
// the user "run" a field up quickly (e.g. scrolling through years) instead of clicking 60 times
|
||||
// to advance the minute field.
|
||||
void scanButSET(unsigned long mill) {
|
||||
static unsigned long lastRun = millis();
|
||||
static unsigned long lastPush = millis();
|
||||
static switchStates butState;
|
||||
byte sw;
|
||||
|
||||
if (PIN_SET_BUTTON<0) return;
|
||||
if ((millis() - lastRun) < mill) return; //refresh rate
|
||||
lastRun = millis();
|
||||
|
||||
sw = digitalRead(PIN_SET_BUTTON);
|
||||
if (pushedSetButtonValue == HIGH) sw = !sw; //inverted button logic
|
||||
|
||||
switch (butState) {
|
||||
case IS_OPEN:
|
||||
if(sw == LOW) butState = IS_PUSHING;
|
||||
break;
|
||||
case IS_PUSHING:
|
||||
butState = IS_PUSHED;
|
||||
lastPush = millis();
|
||||
DPRINTLN(" PUSHED");
|
||||
break;
|
||||
case IS_PUSHED:
|
||||
if(sw == HIGH) butState = IS_OPENING;
|
||||
if((sw == LOW) && ((millis()-lastPush)>2000)) butState = IS_LONGPRESSING;
|
||||
break;
|
||||
case IS_LONGPRESSING:
|
||||
butState = IS_LONGPRESSED;
|
||||
DPRINTLN(" IS_LONGPRESSED");
|
||||
//if (noZero !=0) noZero =0; else noZero = 1; //dead code (leftover from another clock variant?) - no noZero/EEPROM_NOZERO symbol exists in this codebase
|
||||
//EEPROM.update(EEPROM_NOZERO, noZero);
|
||||
break;
|
||||
case IS_LONGPRESSED:
|
||||
// Stays in this state (re-entered every poll) for as long as the button remains held, which
|
||||
// is what produces the auto-repeat effect: mvar[fld] is bumped by 1 on every ~100ms tick.
|
||||
if (sw == HIGH) butState = IS_LONGOPENING;
|
||||
// if ((millis()-lastPush)>10000) {factoryReset(); resetFunc();} //reset to factory settings (dead code - factory-reset-via-long-hold was never wired up here)
|
||||
LastModify = millis();
|
||||
mvar[fld]+=1;
|
||||
if (mvar[fld] < m1[fld].lowlimit) mvar[fld] = m1[fld].uplimit;
|
||||
if (mvar[fld] > m1[fld].uplimit) mvar[fld] = m1[fld].lowlimit;
|
||||
DPRINT(fld); DPRINT(" : "); DPRINTLN(mvar[fld]);
|
||||
break;
|
||||
case IS_OPENING:
|
||||
butState = IS_OPEN;
|
||||
if ((millis()-lastPush)<500) {
|
||||
LastModify = millis();
|
||||
mvar[fld]++;
|
||||
if (mvar[fld] < m1[fld].lowlimit) mvar[fld] = m1[fld].uplimit;
|
||||
if (mvar[fld] > m1[fld].uplimit) mvar[fld] = m1[fld].lowlimit;
|
||||
//if (fld <= 2) mvar[2] = monthday_check(mvar[RTC_Ev], mvar[RTC_Ho], mvar[RTC_Nap]); //leap-year check (szökőév ellenőrzés) -- dead code: monthday_check()/RTC_Ev/RTC_Ho/RTC_Nap are not defined anywhere in this codebase, so a Day value like 30 in February can currently be set and saved without any leap-year/month-length validation
|
||||
DPRINT(fld); DPRINT(" : "); DPRINTLN(mvar[fld]);
|
||||
if (fld>MAXFLD) fld = 0; // POSSIBLE BUG: this resets fld (the FIELD SELECTOR), not mvar[fld] (the field's VALUE). fld is only ever changed by scanButFLD(), which already clamps it to 1..MAXFLD, so this condition should never be true in practice - looks like leftover/copy-pasted logic (or was meant to operate on something else) rather than an intentional part of the SET-button behavior.
|
||||
//flash(fld);
|
||||
} //endif millis()
|
||||
break;
|
||||
case IS_LONGOPENING:
|
||||
butState = IS_OPEN;
|
||||
break;
|
||||
} //end switch
|
||||
}
|
||||
|
||||
|
||||
// Manual I2C bus-recovery routine: bit-bangs SCL/SDA as plain GPIOs (bypassing the Wire/TWI
|
||||
// peripheral) to unstick an I2C bus where a slave device is stuck holding a line low (e.g. a
|
||||
// device that was reset/powered mid-transaction and is waiting for clocks it will never see from
|
||||
// a normal Wire.begin()). Follows the standard "clock out up to 9 pulses, then issue a STOP"
|
||||
// recovery procedure from the I2C spec / Adafruit/NXP application notes.
|
||||
// NOTE: currently DEAD CODE from this file's own call graph - not invoked anywhere in rtc.ino; the
|
||||
// only call site in the whole project is commented out (I2Csensors.ino, near where RTCexist gets
|
||||
// probed on the bus) rather than being wired into the normal startup/error-recovery path.
|
||||
#if defined(PIN_SDA) && defined(PIN_SCL)
|
||||
int I2C_ClearBus() {
|
||||
#if defined(TWCR) && defined(TWEN)
|
||||
TWCR &= ~(_BV(TWEN)); //Disable the Atmel 2-Wire interface so we can control the SDA and SCL pins directly
|
||||
#endif
|
||||
DPRINTLN("I2C_ClearBus() started.");
|
||||
pinMode(PIN_SDA, INPUT_PULLUP); // Make SDA (data) and SCL (clock) pins Inputs with pullup.
|
||||
pinMode(PIN_SCL, INPUT_PULLUP);
|
||||
|
||||
Fdelay(2500); // Wait 2.5 secs. This is strictly only necessary on the first power
|
||||
// up of the DS3231 module to allow it to initialize properly,
|
||||
// but is also assists in reliable programming of FioV3 boards as it gives the
|
||||
// IDE a chance to start uploaded the program
|
||||
// before existing sketch confuses the IDE by sending Serial data.
|
||||
|
||||
boolean SCL_LOW = (digitalRead(PIN_SCL) == LOW); // Check if SCL is Low.
|
||||
if (SCL_LOW) { //If it is held low, Arduino cannot become the I2C master.
|
||||
return 1; //I2C bus error. Could not clear SCL clock line held low
|
||||
}
|
||||
|
||||
boolean SDA_LOW = (digitalRead(PIN_SDA) == LOW); // vi. Check SDA input.
|
||||
int clockCount = 20; // > 2x9 clock
|
||||
|
||||
while (SDA_LOW && (clockCount > 0)) { // vii. If SDA is Low,
|
||||
clockCount--;
|
||||
// Note: I2C bus is open collector so do NOT drive SCL or SDA high.
|
||||
pinMode(PIN_SCL, INPUT); // release SCL pullup so that when made output it will be LOW
|
||||
pinMode(PIN_SCL, OUTPUT); // then clock SCL Low
|
||||
delayMicroseconds(10); // for >5uS
|
||||
pinMode(PIN_SCL, INPUT); // release SCL LOW
|
||||
pinMode(PIN_SCL, INPUT_PULLUP); // turn on pullup resistors again
|
||||
// do not force high as slave may be holding it low for clock stretching.
|
||||
delayMicroseconds(10); // for >5uS
|
||||
// The >5uS is so that even the slowest I2C devices are handled.
|
||||
SCL_LOW = (digitalRead(PIN_SCL) == LOW); // Check if SCL is Low.
|
||||
int counter = 20;
|
||||
while (SCL_LOW && (counter > 0)) { // loop waiting for SCL to become High only wait 2sec.
|
||||
counter--;
|
||||
delay(100);
|
||||
SCL_LOW = (digitalRead(PIN_SCL) == LOW);
|
||||
}
|
||||
if (SCL_LOW) { // still low after 2 sec error
|
||||
return 2; // I2C bus error. Could not clear. SCL clock line held low by slave clock stretch for >2sec
|
||||
}
|
||||
SDA_LOW = (digitalRead(PIN_SDA) == LOW); // and check SDA input again and loop
|
||||
}
|
||||
if (SDA_LOW) { // still low
|
||||
return 3; // I2C bus error. Could not clear. SDA data line held low
|
||||
}
|
||||
|
||||
// else pull SDA line low for Start or Repeated Start
|
||||
pinMode(PIN_SDA, INPUT); // remove pullup.
|
||||
pinMode(PIN_SDA, OUTPUT); // and then make it LOW i.e. send an I2C Start or Repeated start control.
|
||||
// When there is only one I2C master a Start or Repeat Start has the same function as a Stop and clears the bus.
|
||||
/// A Repeat Start is a Start occurring after a Start with no intervening Stop.
|
||||
delayMicroseconds(10); // wait >5uS
|
||||
pinMode(PIN_SDA, INPUT); // remove output low
|
||||
pinMode(PIN_SDA, INPUT_PULLUP); // and make SDA high i.e. send I2C STOP control.
|
||||
delayMicroseconds(10); // x. wait >5uS
|
||||
pinMode(PIN_SDA, INPUT); // and reset pins as tri-state inputs which is the default state on reset
|
||||
pinMode(PIN_SCL, INPUT);
|
||||
return 0; // all ok
|
||||
}
|
||||
#else
|
||||
int I2C_ClearBus() {return 1;} //PIN_SDA/PIN_SCL not defined for this build - can't bit-bang pins that don't exist, report a generic "bus error" instead
|
||||
#endif
|
||||
|
||||
//------------------------------------------------------------------------------------------
|
||||
#else
|
||||
// USE_RTC not defined for this clock build: provide empty stand-in implementations so the rest
|
||||
// of the codebase (ESP32_UniClock2.ino's setup()/calcTime()/loop()) can call these unconditionally
|
||||
// without needing #ifdef USE_RTC guards at every call site.
|
||||
void updateRTC() {}
|
||||
void setupRTC() {}
|
||||
void getRTC() {}
|
||||
void editor() {}
|
||||
#endif
|
||||
@@ -0,0 +1,823 @@
|
||||
// NeoPixel Word Clock
|
||||
//
|
||||
// ============================================================================
|
||||
// This driver is fundamentally different from the other (tube/VFD/Numitron/
|
||||
// LED segment) drivers in this codebase: instead of multiplexing digit tubes,
|
||||
// it lights up individual letters/words on a WS2812 ("NeoPixel") LED matrix
|
||||
// mounted behind a laser-cut/printed acrylic panel, so the time is spelled
|
||||
// out as a sentence (e.g. "IT IS HALF PAST TEN") rather than shown as digits.
|
||||
//
|
||||
// Hardware: an XMAX x YMAX (currently 11x11, see clocks.h) grid of WS2812
|
||||
// LEDs wired in a single serpentine ("boustrophedon") strip, plus a handful
|
||||
// of extra individually-addressed LEDs (weekday row, minute dots, wifi/temp
|
||||
// indicator) appended after the grid. Driven via NeoPixelBrightnessBus.
|
||||
// A 5x7 pixel font (font5x7.h) is reused from the tube drivers to render
|
||||
// small numeric overlays (temperature/humidity/date-time editor) on top of
|
||||
// the same matrix.
|
||||
//
|
||||
// Language variants: the actual word layout (the `matrix[][]` character grid
|
||||
// used to look up each word's on-screen position) and the HH:MM -> sentence
|
||||
// templates (`sequence[][]`) are NOT in this file -- they live in clocks.h,
|
||||
// one block per clock model:
|
||||
// CLOCK_80 = English ("it is quarter past ten")
|
||||
// CLOCK_81 = German ("es ist viertel nach zehn" style, uses "vor/nach halb")
|
||||
// CLOCK_82 = Hungarian ("negyed tiz" / "harom negyed tiz" style)
|
||||
// All three define WORDCLOCK and share this single .ino as their driver;
|
||||
// only the word/sequence/matrix tables (and START_CORNER wiring direction)
|
||||
// differ per config. Hungarian time phrasing is grammatically irregular
|
||||
// (quarters count towards the NEXT hour, e.g. "negyed" = quarter of the
|
||||
// hour to come, "haromnegyed" = three-quarters/quarter-to) -- see showTime()
|
||||
// below for how the generic #1/#2 token mechanism copes with that.
|
||||
//
|
||||
// Note: wordclock_hun.txt in the repo root is NOT read by this code at
|
||||
// runtime -- it is a captured serial debug log (DPRINT output) from a past
|
||||
// firmware build, kept only as human-readable reference documentation of
|
||||
// the Hungarian matrix/sequence layout, not a data file this .ino loads.
|
||||
// ============================================================================
|
||||
|
||||
#ifdef WORDCLOCK
|
||||
|
||||
//#define TEST_WORDCLOCK
|
||||
|
||||
#include "font5x7.h"
|
||||
char tubeDriver[] = "WORDCLOCK"; //identifies this driver in status/web page; "tubeDriver" name is generic/shared across all drivers
|
||||
#define NUMDIGITS 4
|
||||
int maxDigits = NUMDIGITS; //read by the core (changeDigit(), etc.) even though this driver has no physical digit tubes; keeps digit[]/newDigit[] sized for the numeric overlay (temp/humidity/IP/editor)
|
||||
long neoBrightness;
|
||||
int colorStep = 1;
|
||||
int maxPos; //highest linear pixel index inside the XMAX*YMAX word grid (excludes the extra indicator LEDs appended after it)
|
||||
int offsetX = 0; //current animation scroll offset (in grid cells), used to slide the whole sentence in/out of view
|
||||
int offsetY = 0;
|
||||
int animOffsetX = 0; //target scroll direction/step for the current prm.animMode (see setAnimOffset())
|
||||
int animOffsetY = 0;
|
||||
|
||||
enum LEDSTART {LEFTUP, RIGHTUP, LEFTDOWN, RIGHTDOWN }; //physical corner where LED index 0 of the serpentine strip starts -- set per clock model via START_CORNER in clocks.h
|
||||
enum NEO_RGB_FEATURE {RGB, GRB, BRG, BGR};
|
||||
|
||||
#define NUMCOLS 5
|
||||
#define NUMROWS 7
|
||||
boolean _upsidedown = false;
|
||||
byte row[NUMROWS];
|
||||
|
||||
char asciiConvert[] = "0123456789 -.APC~%oIF"; //maps a raw digit/code value (used as index) to the matching glyph in font5x7.h, for the numeric overlay display
|
||||
char DPchar = 0xFF; //decimal point
|
||||
byte dat[NUMDIGITS][NUMROWS]; //rasterized glyph rows for up to NUMDIGITS characters; in practice only dat[0] and dat[1] (the two digits of a number) are ever filled by writeNumericDisplay()
|
||||
|
||||
#define FPS 60 //frames per second
|
||||
#define FPS_MSEC 1000/FPS
|
||||
#define COLORSATURATION 255
|
||||
#define WHITE_INDEX 192
|
||||
#define RANDOM_WHEEL_DISTANCE 30 //minimum wheel-distance a new random color must keep from the existing color(s) to be accepted, in random mode
|
||||
#define RANDOM_MAX_COUNTER 100 //maximum number of attempts to find a sufficiently distinct new random color before giving up
|
||||
#define RANDOM_FROM_ALL_PIXELS true //true or false: when generating a new random color, measure its distance from all pixels' colors, or only from the current pixel's own color
|
||||
|
||||
RgbColor red(COLORSATURATION, 0, 0);
|
||||
RgbColor red2(COLORSATURATION/2, 0, 0);
|
||||
RgbColor green(0, COLORSATURATION, 0);
|
||||
RgbColor blue(0, 0, COLORSATURATION);
|
||||
RgbColor purple(COLORSATURATION, 0, COLORSATURATION);
|
||||
RgbColor white(COLORSATURATION/2,COLORSATURATION/2,COLORSATURATION/2);
|
||||
RgbColor black(0,0,0);
|
||||
RgbColor yellow(255, 255, 0);
|
||||
RgbColor orange(255,140,0);
|
||||
RgbColor turqouise(0xB1,0xFF,0xEC);
|
||||
RgbColor pink(0xFF,0xA0,0xB0);
|
||||
|
||||
RgbColor dispColor;
|
||||
|
||||
//some definitions are in clocks.h CLOCK_80 and CLOCK_81
|
||||
//(also CLOCK_82, the Hungarian variant -- see: struct wordDef, XMAX/YMAX, matrix[][],
|
||||
// sequence[][], hours[], words[], extraDots[], WEEKDAY_POSX/Y, SENSOR_POSX/Y, START_CORNER)
|
||||
|
||||
#define WORD_MAX sizeof(words)/sizeof(wordDef) //number of words
|
||||
#define HOUR_MAX sizeof(hours)/sizeof(wordDef) //number of words
|
||||
|
||||
//Total addressable pixel count: the XMAX*YMAX word-matrix grid plus 4 extra
|
||||
//individually-addressed LEDs wired after it (see extraDots[] in clocks.h) used
|
||||
//for the 1..4 "minute remainder" dots (word clocks only resolve to 5-minute
|
||||
//steps in words, see showTime()/m5 below -- these dots show the 0..4 leftover minutes).
|
||||
const int PixelCount = XMAX*YMAX+4;
|
||||
|
||||
|
||||
//NeoGrbFeature gives BRGW (G and B swapped)
|
||||
//NeoRgbFeature gives RBGW (G and B swapped)
|
||||
//NeoBrgFeature gives BGRW (G and R swapped)
|
||||
|
||||
#if defined(ESP32)
|
||||
byte PixelPin = NEOPIXEL_PIN; //on ESP32 usable any pin below 32
|
||||
NeoPixelBrightnessBus<NeoGrbFeature, NeoEsp32I2s1800KbpsMethod> strip(PixelCount+10,PixelPin); //NeoEsp32I2s1800KbpsMethod is used here because it is more reliable than NeoEsp32Rmt7Ws2812xMethod!
|
||||
#else
|
||||
#define NEOPIXEL_PIN 3
|
||||
byte PixelPin = 3; // on 8266 it MUST use GPIO3 (RX pin)
|
||||
NeoPixelBrightnessBus<NeoGrbFeature, Neo800KbpsMethod> strip(PixelCount+10);
|
||||
#endif
|
||||
|
||||
NeoGamma<NeoGammaTableMethod> colorGamma;
|
||||
|
||||
RgbColor Wheel(byte WheelPos) {
|
||||
if (WheelPos == WHITE_INDEX) return white;
|
||||
WheelPos = 255 - WheelPos;
|
||||
if(WheelPos < 85) {
|
||||
return RgbColor(255 - WheelPos * 3, 0, WheelPos * 3);
|
||||
} else if(WheelPos < 170) {
|
||||
WheelPos -= 85;
|
||||
return RgbColor(0, WheelPos * 3, 255 - WheelPos * 3);
|
||||
} else {
|
||||
WheelPos -= 170;
|
||||
return RgbColor(WheelPos * 3, 255 - WheelPos * 3, 0);
|
||||
}
|
||||
}
|
||||
|
||||
//Converts a logical (column,row) letter position in the word matrix -- 1-based,
|
||||
//x in [1..XMAX], y in [1..YMAX] -- into the linear NeoPixel strip index.
|
||||
//This is necessary because the physical strip is wired as a single serpentine
|
||||
//("boustrophedon": row 0 left-to-right, row 1 right-to-left, row 2 left-to-right, ...)
|
||||
//starting from whichever physical corner START_CORNER says (set per clock model
|
||||
//in clocks.h, since different panel builds route the data-in wire to a different
|
||||
//corner). x/y are first normalized to 0-based, then mirrored/flipped as needed for
|
||||
//the chosen starting corner, then folded into the zig-zag row order, and finally
|
||||
//(for the "up" corners) the whole index is mirrored via maxPos-pos because index 0
|
||||
//of the physical strip is at the BOTTOM of the panel even when the logical/visual
|
||||
//starting corner is at the top.
|
||||
int inline convertXYtoPOS(int x, int y) { //calculate position, depending on the first LED's position
|
||||
int pos;
|
||||
//DPRINT("X:"); DPRINT(x); DPRINT(" Y:"); DPRINT(y);
|
||||
if ((x>XMAX) || (y>YMAX) || (x<1) || (y<1)) {
|
||||
DPRINT(x); DPRINT("/"); DPRINTLN(y);
|
||||
return(maxPos+5); //out of area: park it just past the real grid+extraDots range (still inside the strip's PixelCount+10 safety buffer) so an out-of-range coordinate can't corrupt another LED nor crash
|
||||
}
|
||||
x--; y--; //move position 1,1 to 0,0
|
||||
|
||||
switch (START_CORNER) {
|
||||
case LEFTUP:
|
||||
y = YMAX-y-1;
|
||||
if (y%2==0) pos = (y+1)*XMAX - x-1;
|
||||
else pos = x + y*XMAX;
|
||||
pos = maxPos - pos; //*prm.ledsPerHold;
|
||||
break;
|
||||
case RIGHTUP:
|
||||
x = XMAX-x-1;
|
||||
// POSSIBLE BUG: uses XMAX here where LEFTUP (above) uses YMAX for the same
|
||||
// row-flip computation on y, and the y%2 zig-zag test's polarity is also
|
||||
// inverted vs. LEFTUP (here "if (y%2)", there "if (y%2==0)") without an
|
||||
// obvious reason tied to the extra x-mirroring done for this corner.
|
||||
// With the current hardware XMAX==YMAX==11 this is silently harmless,
|
||||
// but it looks like a copy/paste-and-tweak mistake from the LEFTUP case
|
||||
// rather than an intentional difference, and would break on a
|
||||
// non-square panel (XMAX != YMAX). Left unchanged; needs verification
|
||||
// against a real RIGHTUP-wired panel (CLOCK_81/CLOCK_82 hardware).
|
||||
y = XMAX-y-1;
|
||||
if (y%2) pos = (y+1)*XMAX - x-1;
|
||||
else pos = x + y*XMAX;
|
||||
pos = maxPos - pos; //*prm.ledsPerHold;
|
||||
break;
|
||||
case LEFTDOWN:
|
||||
if (y%2) pos = (y+1)*XMAX - x-1;
|
||||
else pos = x + y*XMAX;
|
||||
//pos = pos*prm.ledsPerHold;
|
||||
break;
|
||||
case RIGHTDOWN:
|
||||
x = XMAX-x-1;
|
||||
if (y%2) pos = (y+1)*XMAX - x-1;
|
||||
else pos = x + y*XMAX;
|
||||
//pos = pos*prm.ledsPerHold;
|
||||
break;
|
||||
default:
|
||||
pos = 0;
|
||||
} //end switch
|
||||
//pos += prm.ledOffset;
|
||||
//DPRINT(" POS:"); DPRINTLN(pos); DPRINT(" ");
|
||||
return(pos);
|
||||
}
|
||||
|
||||
|
||||
//Fill the struct's fields with X,Y and len values
|
||||
//For every entry in a word table (words[] or hours[], defined in clocks.h), find
|
||||
//where that literal text substring first occurs inside the `matrix[][]` character
|
||||
//grid (also in clocks.h, one row of text per matrix row) and record its start
|
||||
//column/row + length. This is how the human-readable word list gets turned into
|
||||
//pixel coordinates without having to hand-enter X/Y for every word.
|
||||
//Note: some hour words rely on this "find literal substring" approach in a
|
||||
//deliberately hacky way to dodge false-positive matches -- e.g. the Hungarian
|
||||
//hours[] table (clocks.h, CLOCK_82) uses placeholder text like "__1" and
|
||||
//"TIZEN__1" instead of the real letters "EGY"/"TIZENEGY" ("one"/"eleven"),
|
||||
//because "EGY" is also a substring of other words in that matrix row (e.g.
|
||||
//"NEGYED") and strstr() would otherwise lock onto the wrong occurrence. The
|
||||
//placeholder is unique in the matrix text but still highlights the correct
|
||||
//physical letters when displayed.
|
||||
void generateTable(struct wordDef *w,int maxLine) {
|
||||
DPRINT("\nNumber of lines:"); DPRINTLN(maxLine);
|
||||
char* ret;
|
||||
boolean found;
|
||||
for (int num=0;num<maxLine;num++) { //find all words
|
||||
found = false;
|
||||
DPRINT(w[num].txt);
|
||||
for (int j=0;j<YMAX;j++) { //scan all matrix lines
|
||||
ret = strstr(matrix[j],w[num].txt);
|
||||
if (ret!=NULL){
|
||||
found = true;
|
||||
w[num].startX = ret-matrix[j]+1;
|
||||
w[num].startY = j+1;
|
||||
w[num].len = strlen(w[num].txt);
|
||||
DPRINT(" x:"); DPRINT(w[num].startX); DPRINT(" y:"); DPRINT(w[num].startY); DPRINT(" len:"); DPRINTLN(w[num].len);
|
||||
break;
|
||||
}
|
||||
}//endfor j
|
||||
if (!found) {
|
||||
DPRINTLN(" not found:");
|
||||
}
|
||||
} //endfor i
|
||||
}
|
||||
|
||||
void testSequences() { //print all hour:min
|
||||
//Debug-only self-test (enabled via #define TEST_WORDCLOCK near the top of
|
||||
//this file): walks every possible 24h time (00:00..23:59) through showTime(),
|
||||
//which under TEST_WORDCLOCK dumps each resolved word sequence over serial --
|
||||
//useful for proofreading every generated sentence in a new language/layout
|
||||
//without waiting for the clock to actually reach that minute.
|
||||
DPRINTLN("------------ Testing Expressions --------------------");
|
||||
for (int i=0;i<=23;i++) {
|
||||
for (int j=0;j<=59;j++) {
|
||||
showTime(i,j);
|
||||
}
|
||||
}
|
||||
}
|
||||
void testPanel(){ //boot-time self-test: lights each pixel red in strip order, one at a time, wrapping every XMAX pixels -- lets you visually confirm the serpentine wiring/START_CORNER mapping matches the physical panel
|
||||
int n = 0;
|
||||
for (int i=0;i<PixelCount;i++) {strip.SetPixelColor(i,black);}
|
||||
strip.Show();
|
||||
for (int i=0;i<PixelCount;i++) {
|
||||
strip.SetPixelColor(i,red);
|
||||
strip.Show();
|
||||
delay(50);
|
||||
n++;
|
||||
if (n>=XMAX) {
|
||||
n = 0;
|
||||
for (int i=0;i<PixelCount;i++) {strip.SetPixelColor(i,black);}
|
||||
}
|
||||
}
|
||||
delay(2000);
|
||||
}
|
||||
|
||||
|
||||
void setup_pins() { //this driver's equivalent of the tube drivers' pin/hardware init; called once from the core's setup()
|
||||
DPRINTLN("Setup Word Clock's NeoPixel LEDS");
|
||||
regPin(PixelPin,"NEOPIXEL_PIN"); //register the GPIO with the core's pin-conflict tracker, as every driver must
|
||||
maxPos = XMAX * YMAX - 1;
|
||||
DPRINT("Pixel count: "); DPRINTLN(PixelCount);
|
||||
DPRINT("Brightness:"); DPRINT(c_MinBrightness); DPRINT(" - "); DPRINTLN(c_MaxBrightness);
|
||||
neoBrightness = 50;
|
||||
strip.Begin();
|
||||
strip.Show();
|
||||
strip.SetBrightness(neoBrightness);
|
||||
testPanel();
|
||||
for (int i=0;i<PixelCount;i++) {strip.SetPixelColor(i,black);}
|
||||
dispColor = Wheel(prm.rgbFixColor);
|
||||
int pos;
|
||||
//Boot animation: trace the two diagonals, then the outer border, in red,
|
||||
//as a further visual wiring/orientation check (see testPanel() above) --
|
||||
//purely cosmetic/diagnostic, no effect on later clock operation.
|
||||
for (int i=1;i<=YMAX;i++) {
|
||||
pos = convertXYtoPOS(i, i);
|
||||
strip.SetPixelColor(pos,red);
|
||||
pos = convertXYtoPOS(XMAX-i+1, i);
|
||||
strip.SetPixelColor(pos,red);
|
||||
strip.Show();
|
||||
delay(50);
|
||||
}
|
||||
for (int i=1;i<=XMAX;i++) {
|
||||
pos = convertXYtoPOS(XMAX-i+1, YMAX);
|
||||
strip.SetPixelColor(pos,red);
|
||||
pos = convertXYtoPOS(i, 1);
|
||||
strip.SetPixelColor(pos,red);
|
||||
strip.Show();
|
||||
delay(50);
|
||||
}
|
||||
for (int i=1;i<=YMAX;i++) {
|
||||
pos = convertXYtoPOS(XMAX, i);
|
||||
strip.SetPixelColor(pos,red);
|
||||
pos = convertXYtoPOS(1, YMAX-i+1);
|
||||
strip.SetPixelColor(pos,red);
|
||||
strip.Show();
|
||||
delay(50);
|
||||
}
|
||||
DPRINTLN("\n--- MATRIX ---");
|
||||
for (int i=0;i<YMAX;i++) {
|
||||
DPRINTLN(matrix[i]);
|
||||
}
|
||||
DPRINTLN("\n------ SEQUENCE ------");
|
||||
for (int i=0;i<12;i++) {
|
||||
DPRINTLN(sequence[i]);
|
||||
}
|
||||
DPRINTLN("--------------------\n");
|
||||
generateTable(words,WORD_MAX); //resolve X/Y/len for every entry in the general word list (see generateTable() above)
|
||||
generateTable(hours,HOUR_MAX); //resolve X/Y/len for every entry in the hour-number word list (ONE..TWELVE / EIN..ZWOLF / __1..TIZENKETTO)
|
||||
#ifdef TEST_WORDCLOCK
|
||||
testSequences(); //print all hour:min
|
||||
#endif
|
||||
}
|
||||
|
||||
int clock24to12(int hr) { //convert a (possibly out-of-range, see showTime()'s h++ for "#2" tokens) 24h hour into the 1..12 range used to index hours[]
|
||||
if (hr==0)
|
||||
hr=12;
|
||||
else
|
||||
if(hr>12) hr-=12;
|
||||
return(hr);
|
||||
}
|
||||
|
||||
void firstRun() { //switch off (tube-only) settings that don't apply to a word-clock display, e.g. no digit blink/leading-zero-blank/double-blink animation and no cathode-protection exercise cycle exist on a word matrix; called once on first showTime() invocation
|
||||
prm.enableBlink = false;
|
||||
prm.showZero = false;
|
||||
prm.enableTimeDisplay = true; //disable date display
|
||||
prm.dateRepeatMin = 0;
|
||||
prm.enableDoubleBlink = false;
|
||||
prm.interval = 0; //disable cathode protect
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*
|
||||
RgbColor getLedColor(RgbColor in) {
|
||||
RgbColor out;
|
||||
|
||||
switch (prm.neoRgbFeature) {
|
||||
case RGB:
|
||||
out = in;
|
||||
break;
|
||||
case GRB:
|
||||
out.R = in.G;
|
||||
out.G = in.R;
|
||||
out.B = in.B;
|
||||
break;
|
||||
case BRG:
|
||||
out.R = in.B;
|
||||
out.G = in.R;
|
||||
out.B = in.G;
|
||||
break;
|
||||
case BGR:
|
||||
out.R = in.B;
|
||||
out.G = in.G;
|
||||
out.B = in.R;
|
||||
break;
|
||||
}
|
||||
return (out);
|
||||
}
|
||||
*/
|
||||
|
||||
void clearScr(){ //blank the whole panel; either instantly (static display, animMode 0) or as a row-by-row wipe (any scroll animation mode), so a transition between two times has a visible "clean slate" moment
|
||||
if (prm.animMode==0) {
|
||||
for (int i=0;i<PixelCount;i++) {
|
||||
strip.SetPixelColor(i,black);
|
||||
}
|
||||
}
|
||||
else {
|
||||
for (int y=1;y<=YMAX;y++){
|
||||
for (int x=1;x<=XMAX;x++) {strip.SetPixelColor(convertXYtoPOS(x,y),black);}
|
||||
strip.Show();
|
||||
delay(25);
|
||||
} //endfor y
|
||||
} //end else
|
||||
}
|
||||
|
||||
//Light up one word: walk its `len` characters starting at (startX,startY) --
|
||||
//both filled in by generateTable() from the matrix[][] text -- offset by the
|
||||
//current scroll animation position (offsetX/offsetY), skipping any character
|
||||
//that has scrolled outside the visible XMAX x YMAX grid.
|
||||
void disp(struct wordDef *w) {
|
||||
int x1,y1;
|
||||
static char tmp[20] = " "; //NOTE: WORD_LENGTH (clocks.h) is 15 (+1 for NUL) so this 20-byte buffer is safe for all current word tables; if a future language ever needs longer words than 19 chars, this fixed-size buffer (and the one in showTime() below) would need to grow too.
|
||||
|
||||
if (strcmp(tmp,w->txt)!=0) {
|
||||
//DPRINT(w->txt); DPRINT(" ");
|
||||
strncpy(tmp,w->txt,sizeof(tmp)); //vestigial: this comparison/copy only fed the (now commented-out) DPRINT above; with that print disabled, tmp no longer affects the drawing below, which always executes unconditionally
|
||||
}
|
||||
|
||||
for (int i=0; i<w->len; i++) {
|
||||
x1 = w->startX + i + offsetX;
|
||||
y1 = w->startY + offsetY;
|
||||
if ((x1>=1) && (x1<=XMAX) && (y1>=1) && (y1<=YMAX)) {
|
||||
strip.SetPixelColor(convertXYtoPOS(x1,y1),dispColor);
|
||||
}
|
||||
} //endfor
|
||||
}
|
||||
|
||||
|
||||
void dispNum(int num){ //display one hour-number word (num must already be in the 1..12 range hours[] is indexed by, see clock24to12())
|
||||
#ifdef TEST_WORDCLOCK
|
||||
if ((num<0) || (num>12)) {
|
||||
DPRINT("dispNumError:"); DPRINT(num); DPRINT(" ");
|
||||
}
|
||||
else {
|
||||
DPRINT(num); DPRINT(" >> "); DPRINT(hours[num].txt); DPRINT(" ");
|
||||
}
|
||||
#endif
|
||||
disp(&hours[num]);
|
||||
}
|
||||
|
||||
void dispWord(char* w){ //find a word by its literal text in the words[] table (linear search -- table is tiny, so no need for anything smarter) and light it up
|
||||
int num=0;
|
||||
boolean found = false;
|
||||
do {
|
||||
if (strcmp(w,words[num].txt)==0) {
|
||||
found = true;
|
||||
#ifdef TEST_WORDCLOCK
|
||||
DPRINT(words[num].txt); DPRINT(" ");
|
||||
#endif
|
||||
disp(&words[num]);
|
||||
return;
|
||||
}
|
||||
num++;
|
||||
} while (num<WORD_MAX);
|
||||
#ifdef TEST_WORDCLOCK
|
||||
DPRINT("NOT_FOUND:"); DPRINTLN(w);
|
||||
#endif
|
||||
}
|
||||
|
||||
void resetOffset(){ //place the sentence one full panel-width/height off-screen in the direction it will scroll IN from, so displayClock() can animate it sliding back to offset (0,0)
|
||||
offsetX = -1 * animOffsetX * XMAX;
|
||||
offsetY = -1 * animOffsetY * YMAX;
|
||||
}
|
||||
|
||||
//Map the generic prm.animMode setting (shared EEPROM setting across all
|
||||
//drivers) to a scroll direction for this word-clock's slide-in transition.
|
||||
//animOffsetX/Y only encode the *direction* (-1/0/1 per axis); the actual
|
||||
//per-frame step size/speed is handled by how often displayClock() is called
|
||||
//(see writeDisplay2()'s `delay(258 - prm.rgbSpeed)`).
|
||||
void setAnimOffset(){
|
||||
switch (prm.animMode) {
|
||||
case 0:
|
||||
default:
|
||||
animOffsetX = 0;
|
||||
animOffsetY = 0;
|
||||
offsetX = 0;
|
||||
offsetY = 0;
|
||||
break;
|
||||
case 1:
|
||||
animOffsetX = 1;
|
||||
animOffsetY = 0;
|
||||
break;
|
||||
case 2:
|
||||
animOffsetX = -1;
|
||||
animOffsetY = 0;
|
||||
break;
|
||||
case 3:
|
||||
animOffsetX = 0;
|
||||
animOffsetY = 1;
|
||||
break;
|
||||
case 4:
|
||||
animOffsetX = 0;
|
||||
animOffsetY = -1;
|
||||
break;
|
||||
case 5:
|
||||
animOffsetX = 1;
|
||||
animOffsetY = 1;
|
||||
break;
|
||||
case 6:
|
||||
animOffsetX = -1;
|
||||
animOffsetY = -1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
//Cathode-protection is a tube-driver concept (periodically cycling digits to
|
||||
//avoid uneven wear on Nixie/VFD cathodes); a word-clock LED matrix has no such
|
||||
//wear mechanism, so this word-clock version is a no-op cosmetically: it simply
|
||||
//shows the current minute's ones-digit as a big number using the numeric-overlay
|
||||
//font, reusing the same code path/hook the tube drivers use (`cathodeProtRunning`,
|
||||
//set by the core), for interface parity rather than an actual physical need.
|
||||
void displayCathodeProtectShow() {
|
||||
offsetX = 0; offsetY = 0;
|
||||
dispColor = Wheel(prm.rgbFixColor);
|
||||
for (int i=0;i<PixelCount;i++) {strip.SetPixelColor(i,black);}
|
||||
//dispNum(digit[1]);
|
||||
writeNumericDisplay(digit[1],-1,true);
|
||||
strip.Show();
|
||||
}
|
||||
|
||||
void displayWifiConnecting(){ //shown while connecting to WiFi (wifiConnectRunning, set by the core): lights the word "wifi" in yellow
|
||||
char txt[] = "wifi";
|
||||
|
||||
dispColor = yellow;
|
||||
for (int i=0;i<PixelCount;i++) {strip.SetPixelColor(i,black);}
|
||||
dispWord(txt);
|
||||
strip.Show();
|
||||
}
|
||||
|
||||
//Shown once connected (ipShowRunning, set by the core) to read out the
|
||||
//assigned IP address: alternates the "wifi" word with each of the last 3
|
||||
//digit[] entries (populated by the core's showMyIp()) rendered as big numbers,
|
||||
//one per second, i=2..0 (most- to least-significant of the octet shown).
|
||||
void displayIpShow() {
|
||||
char txt[] = "wifi";
|
||||
|
||||
offsetX = 0; offsetY = 0;
|
||||
for (int i=2;i>=0;i--) {
|
||||
for (int i=0;i<PixelCount;i++) {strip.SetPixelColor(i,black);}
|
||||
dispColor = yellow;
|
||||
dispWord(txt);
|
||||
strip.Show();
|
||||
delay(200);
|
||||
for (int j=0;j<=(2-i);j++) {
|
||||
strip.SetPixelColor(extraDots[j], blue); //here the 4 "minute" indicator dots are repurposed as a digit-position progress marker (1 dot for the 1st digit shown, up to 3 dots for the 3rd) -- comment text is copy-pasted from showTime()'s actual per-minute usage, this call site's meaning is different
|
||||
} //endfor
|
||||
dispColor = red;
|
||||
writeNumericDisplay(digit[i],-1,true);
|
||||
//dispNum(digit[i]);
|
||||
strip.Show();
|
||||
delay(1000);
|
||||
} //endfor
|
||||
for (int i=0;i<PixelCount;i++) {strip.SetPixelColor(i,black);}
|
||||
strip.Show();
|
||||
delay(1000);
|
||||
resetOffset(); //reset animation position offset
|
||||
}
|
||||
|
||||
|
||||
void displayClock(){
|
||||
static int oldH = 99;
|
||||
static int oldM = 99;
|
||||
static byte oldAnimMode = 0;
|
||||
|
||||
setAnimOffset();
|
||||
if ((hour() != oldH) || (minute() != oldM) || (prm.animMode != oldAnimMode)) {
|
||||
oldH = hour(); oldM = minute(); oldAnimMode = prm.animMode;
|
||||
resetOffset();
|
||||
clearScr();
|
||||
}
|
||||
if ((animOffsetX>0) && (offsetX<0)) offsetX += animOffsetX;
|
||||
else if ((animOffsetX<0) && (offsetX>0)) offsetX += animOffsetX;
|
||||
else offsetX = 0;
|
||||
|
||||
if ((animOffsetY>0) && (offsetY<0)) offsetY += animOffsetY;
|
||||
else if ((animOffsetY<0) && (offsetY>0)) offsetY += animOffsetY;
|
||||
else offsetY = 0;
|
||||
//DPRINT("\no:"); DPRINT(offsetX); DPRINT("/"); DPRINT(offsetY); DPRINT(" a:");DPRINT(animOffsetX); DPRINT("/"); DPRINT(animOffsetY);
|
||||
showTime(hour(), minute());
|
||||
}
|
||||
|
||||
void writeNumericDisplay(int num,int dot,boolean lowerDigitOnly) { //write big numbers to the LED matrix display (temp, humidity)
|
||||
char dispChar;
|
||||
int p,pos;
|
||||
int st,offset;
|
||||
|
||||
dispColor = askColor(num,dot);
|
||||
num = abs(num);
|
||||
for (int i=0;i<=1;i++) { //generate new line
|
||||
if (i==0) {
|
||||
dispChar = num/10; //upper digit
|
||||
if (dispChar==0) dispChar = ' '; //space instead of leading zero
|
||||
}
|
||||
else
|
||||
dispChar = num%10; //lower digit
|
||||
|
||||
if (dispChar<sizeof(asciiConvert)) dispChar = asciiConvert[(byte)dispChar];
|
||||
show(dispChar);
|
||||
for (int k=0;k<NUMROWS;k++) { dat[i][k] = row[k]; }
|
||||
} //endfor
|
||||
|
||||
//printBitmap();
|
||||
if (lowerDigitOnly) { //move lower digit to center
|
||||
offset = 3;
|
||||
st = 1;
|
||||
}
|
||||
else {
|
||||
offset = 6;
|
||||
st = 0;
|
||||
}
|
||||
for (int j=0;j<NUMROWS;j++) {
|
||||
for (int dig=st;dig<=1;dig++) {
|
||||
p = 5;
|
||||
for (int b=3;b<=7;b++) {
|
||||
if (dat[dig][j] & (1<<b)) {
|
||||
int pos =convertXYtoPOS(dig*offset+p,j+3);
|
||||
strip.SetPixelColor(pos,dispColor);
|
||||
}
|
||||
p--;
|
||||
} //endfor b
|
||||
} //endfor i
|
||||
} //endfor j
|
||||
|
||||
if (dot>=0) {
|
||||
pos = convertXYtoPOS(SENSOR_POSX+dot, SENSOR_POSY);
|
||||
strip.SetPixelColor(pos,dispColor); //switch on sensor led;
|
||||
}
|
||||
strip.Show();
|
||||
delay(50);
|
||||
}
|
||||
|
||||
void displayAlarmShow() {
|
||||
static boolean phase = true;
|
||||
static unsigned long lastRun = 0;
|
||||
int pos;
|
||||
|
||||
if ((millis()-lastRun)<500) {
|
||||
return;
|
||||
}
|
||||
lastRun = millis();
|
||||
|
||||
neoBrightness = MAXBRIGHTNESS;
|
||||
for (int i=0;i<PixelCount;i++) {strip.SetPixelColor(i,black);}
|
||||
|
||||
for (int i=1;i<=YMAX;i++) {
|
||||
pos = convertXYtoPOS(i, i);
|
||||
strip.SetPixelColor(pos,phase ? white : red);
|
||||
pos = convertXYtoPOS(XMAX-i+1, i);
|
||||
strip.SetPixelColor(pos,phase ? white : red);
|
||||
}
|
||||
for (int i=1;i<=XMAX;i++) {
|
||||
pos = convertXYtoPOS(XMAX-i+1, YMAX);
|
||||
strip.SetPixelColor(pos,phase ? white : red);
|
||||
pos = convertXYtoPOS(i, 1);
|
||||
strip.SetPixelColor(pos,phase ? white : red);
|
||||
}
|
||||
strip.Show();
|
||||
phase = !phase;
|
||||
}
|
||||
|
||||
void writeDisplay2() {
|
||||
int dot = -1;
|
||||
if (alarmON) {
|
||||
displayAlarmShow();
|
||||
}
|
||||
else if (cathodeProtRunning) {
|
||||
displayCathodeProtectShow();
|
||||
}
|
||||
else if (ipShowRunning) { //WIFI IP address display
|
||||
neoBrightness = prm.dayBright;
|
||||
displayIpShow();
|
||||
}
|
||||
else if (wifiConnectRunning) {
|
||||
neoBrightness = prm.dayBright;
|
||||
displayWifiConnecting();
|
||||
}
|
||||
else { //sensor or clock display
|
||||
neoBrightness = displayON ? prm.dayBright : prm.nightBright;
|
||||
if (autoBrightness && (neoBrightness>0)) {
|
||||
neoBrightness = (neoBrightness*lx)/long(MAXIMUM_LUX);
|
||||
if (neoBrightness< c_MinBrightness)
|
||||
neoBrightness = c_MinBrightness;
|
||||
}
|
||||
|
||||
neoBrightness = min(neoBrightness,long(RGB_MAX_BRIGHTNESS)); //safety only
|
||||
if (!radarON) neoBrightness = 0;
|
||||
|
||||
if (editorRunning) dot = 3; //editor mode
|
||||
else if (newDigit[0] == TEMP_CHARCODE) dot = 0; //Celsius
|
||||
else if (newDigit[0] == (TEMP_CHARCODE+5)) dot = 1; //Fahrenheit
|
||||
else if (newDigit[0] == PERCENT_CHARCODE) dot = 2; //Humidity %
|
||||
//DPRINT(newDigit[0]); DPRINT("/"); DPRINT(dot); DPRINT(" ");
|
||||
|
||||
if (dot>=0) {
|
||||
for (int i=0;i<PixelCount;i++) {strip.SetPixelColor(i,black);}
|
||||
strip.SetBrightness(neoBrightness);
|
||||
if (dot==3) writeNumericDisplay(digit[1]*10+digit[0],dot,false); //date/time editor display
|
||||
else if (dot==2) writeNumericDisplay(humid[0],dot,false); //humidity sensor display
|
||||
else writeNumericDisplay(temperature[0],dot,false); //temperature sensor display
|
||||
resetOffset(); //reset animation position offset
|
||||
}
|
||||
else {
|
||||
delay(258 - prm.rgbSpeed);
|
||||
displayClock();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
//-------------- character display -----------------------------------
|
||||
void show(byte c){
|
||||
byte loccol[NUMCOLS];
|
||||
for (byte i=0; i < NUMCOLS; i++)
|
||||
loccol[i] = _font[c*NUMCOLS+i];
|
||||
transposePattern(loccol,row);
|
||||
}
|
||||
|
||||
void transposePattern(byte cols[], volatile byte rows[]){
|
||||
byte cmask;
|
||||
cmask = (_upsidedown ? 0x40 : 1);
|
||||
for (byte r=0; r < NUMROWS; r++) {
|
||||
rows[r] = 0;
|
||||
for (byte c=0; c < NUMCOLS; c++) {
|
||||
if (cmask&cols[(_upsidedown ? (NUMCOLS-c-1) : c)]) rows[r] |= 1;
|
||||
rows[r] = rows[r] << 1;
|
||||
}
|
||||
if (_upsidedown) cmask = cmask >> 1;
|
||||
else cmask = cmask << 1;
|
||||
rows[r] = rows[r] << (7-NUMCOLS);
|
||||
}
|
||||
}
|
||||
|
||||
void bPrint(byte in) {
|
||||
for (int i=7;i>2;i--) {
|
||||
boolean b = in & (1<<i);
|
||||
DPRINT(b ? '1' : '0');
|
||||
}
|
||||
}
|
||||
|
||||
void printBitmap() { //print bitmap for testing
|
||||
for (int j=0;j<NUMROWS;j++) {
|
||||
for (int i=0;i<maxDigits;i++) {
|
||||
bPrint(dat[i][j]); Serial.print(" ");
|
||||
}
|
||||
DPRINTLN(" ");
|
||||
}
|
||||
DPRINTLN("__________________________________");
|
||||
}
|
||||
|
||||
RgbColor askColor(int num, int dot){ //calculate the color of the numbers
|
||||
RgbColor col;
|
||||
|
||||
col = green;
|
||||
|
||||
switch (dot) {
|
||||
case 0: //celsius
|
||||
if (num<=0) col = blue;
|
||||
else if (num>28) col = red;
|
||||
break;
|
||||
case 1: //fahrenheit
|
||||
if (num<=32) col = blue;
|
||||
else if (num>82) col = red;
|
||||
break;
|
||||
case 2: //humidity
|
||||
if (num<40) col = yellow;
|
||||
else if (num>70) col = blue;
|
||||
break;
|
||||
default:
|
||||
col = white;
|
||||
break;
|
||||
}
|
||||
return(col);
|
||||
}
|
||||
|
||||
|
||||
void showTime(int h,int m){ //write the word-clock sentence to the LED display
|
||||
static unsigned long lastRun = 0;
|
||||
int ptrSeq,ptrWord,pos;
|
||||
char ch;
|
||||
char tmp[20];
|
||||
int m5;
|
||||
|
||||
if (lastRun==0) {firstRun(); lastRun = 1;}
|
||||
//if ((millis()-lastRun)>200) {DPRINT(h); DPRINT(":"); DPRINT(m); DPRINT(" ");lastRun = millis();}
|
||||
|
||||
if ((h>23) || (m>59)) return; //safety only
|
||||
|
||||
dispColor = Wheel(prm.rgbFixColor);
|
||||
|
||||
strip.SetBrightness(neoBrightness);
|
||||
#ifdef TEST_WORDCLOCK
|
||||
DPRINTLN(""); DPRINT(h); DPRINT(":"); DPRINT(m); DPRINT(" ");
|
||||
#endif
|
||||
memset(tmp,0,sizeof(tmp));
|
||||
m5 = m / 5; //5min step
|
||||
ptrSeq = 0; //pointer to the sequence chars
|
||||
ptrWord = 0; //pointer to the found word chars
|
||||
for (int i=0;i<PixelCount;i++) {strip.SetPixelColor(i,black);}
|
||||
|
||||
for (ptrSeq=0; ptrSeq<=strlen(sequence[m5]); ptrSeq++) { //process sequence of current minute
|
||||
ch = sequence[m5][ptrSeq];
|
||||
//DPRINTLN(ch);
|
||||
tmp[ptrWord] = ch;
|
||||
if ((ch == ' ') || (ch == 0)) { //end of word??
|
||||
tmp[ptrWord] = 0; //end char
|
||||
//DPRINT("Word:"); DPRINTLN(tmp);
|
||||
if (tmp[0]=='#') { //is it HOUR to show ???
|
||||
if (strncmp(tmp,"#2",2)==0) h++;
|
||||
h = clock24to12(h);
|
||||
dispNum(h);
|
||||
}
|
||||
else {
|
||||
dispWord(tmp);
|
||||
}
|
||||
ptrWord = 0;
|
||||
memset(tmp,0,sizeof(tmp));
|
||||
} //endif: end of word found
|
||||
else {
|
||||
ptrWord++;
|
||||
}
|
||||
} //endfor
|
||||
if ((offsetX==0) && (offsetY==0)) { //animation finished???
|
||||
if (m>0) { //valid minute?
|
||||
pos = convertXYtoPOS(WEEKDAY_POSX+weekday()-1, WEEKDAY_POSY);
|
||||
strip.SetPixelColor(pos,dispColor); //switch on weekday led
|
||||
|
||||
int dot = m % 5; //extra dot for 1..4 minutes
|
||||
if (dot>0) {
|
||||
for (int i=0;i<dot;i++) {
|
||||
strip.SetPixelColor(extraDots[i], dispColor); //switch on 1..4 minute leds
|
||||
} //endfor
|
||||
} //endif dot
|
||||
} //endif m>0
|
||||
if (WiFi.status() == WL_CONNECTED) {
|
||||
strip.SetPixelColor(convertXYtoPOS(SENSOR_POSX+3, SENSOR_POSY), dispColor); // 'W' sign for wifi
|
||||
}
|
||||
} //endif animation finished
|
||||
strip.Show();
|
||||
}
|
||||
|
||||
|
||||
|
||||
//-------------------------------------------------------------
|
||||
|
||||
void clearTubes() {}
|
||||
void writeDisplaySingle() {}
|
||||
#endif
|
||||
@@ -0,0 +1,621 @@
|
||||
// ============================================================================
|
||||
// NeoPixel Tube Backlight subsystem
|
||||
// ----------------------------------------------------------------------------
|
||||
// Drives addressable WS2812/WS2812B RGB LEDs mounted behind/around the tubes
|
||||
// (Nixie/VFD/Numitron/etc.) using the NeoPixelBrightnessBus library (Makuna's
|
||||
// NeoPixelBus). Each clock config maps its physical LEDs to logical tube
|
||||
// positions via the `byte tubePixels[]` array defined in clocks.h (see the
|
||||
// commented examples below), so the same effect code works regardless of how
|
||||
// many LEDs are wired per tube or in which physical order.
|
||||
//
|
||||
// Enabled per-config via #define USE_NEOPIXEL in clocks.h. As of writing this
|
||||
// is used by the large majority of CLOCK_NN configs that have RGB backlight
|
||||
// hardware (e.g. CLOCK_4, 6, 8, 15, 17, 18, 22, 26, 30-32, 34-36, 40-46, 48-52,
|
||||
// 55-57, 57A and others) - grep clocks.h for "#define USE_NEOPIXEL" (not the
|
||||
// commented-out "//#define" ones) for the authoritative, current list.
|
||||
// Mutually exclusive in practice with the simpler PWM-based backlight in
|
||||
// pwmleds.ino (USE_PWMLEDS), which is used instead by configs that only wire
|
||||
// 3 plain R/G/B PWM pins (e.g. CLOCK_54).
|
||||
//
|
||||
// Called from the core program (ESP32_UniClock2.ino):
|
||||
// - setupNeopixel() : once from setup(), after pins/config are known.
|
||||
// - doAnimationMakuna() : every loop() iteration, AND from inside Fdelay()'s
|
||||
// busy-wait loop, so animations keep advancing even
|
||||
// while the tube digits are mid-transition (Fdelay
|
||||
// blocks for the tube-switch duration). It rate-limits
|
||||
// itself internally (see FPS_MSEC / prm.rgbSpeed).
|
||||
// - darkenNeopixels() : once from alarmSound(), when the alarm's ring
|
||||
// period naturally times out (not when the alarm is
|
||||
// silenced early via the button).
|
||||
// ============================================================================
|
||||
|
||||
#ifdef USE_NEOPIXEL
|
||||
|
||||
long neoBrightness;
|
||||
int colorStep = 1;
|
||||
|
||||
#define FPS 60 //frames per second
|
||||
#define FPS_MSEC 1000/FPS //ms per animation frame, used as the minimum interval between doAnimationMakuna() updates (frame-rate cap)
|
||||
#define COLORSATURATION 255
|
||||
#define WHITE_INDEX 192 //reserved "color wheel" position (out of 0..255) that Wheel() special-cases to return pure white instead of a hue
|
||||
#define RANDOM_WHEEL_DISTANCE 30 //minimum wheel-distance a new random color must keep from the existing color(s) to be accepted, in random mode
|
||||
#define RANDOM_MAX_COUNTER 100 //maximum number of attempts to find a sufficiently distinct new random color before giving up
|
||||
#define RANDOM_FROM_ALL_PIXELS true //true or false: when generating a new random color, measure its distance from all pixels' colors, or only from the current pixel's own color
|
||||
|
||||
RgbColor red(COLORSATURATION, 0, 0);
|
||||
RgbColor red2(COLORSATURATION/2, 0, 0);
|
||||
RgbColor green(0, COLORSATURATION, 0);
|
||||
RgbColor blue(0, 0, COLORSATURATION);
|
||||
RgbColor purple(COLORSATURATION, 0, COLORSATURATION);
|
||||
RgbColor white(COLORSATURATION/2,COLORSATURATION/2,COLORSATURATION/2);
|
||||
RgbColor black(0,0,0);
|
||||
|
||||
//Definition: which pixels are near which tube? Make sure to set all pixels of the strip!
|
||||
// Valid: 0..(maxDigits-1). If a number is equal to maxDigits or higher, that pixel always stays dark!
|
||||
//byte tubePixels[] = {0,1,2,3}; //4 tubes, single leds
|
||||
//byte tubePixels[] = {3,2,1,0}; //4 tubes, single leds, reverse direction
|
||||
//byte tubePixels[] = {0,9,1,9,2,9,3}; //4 tubes, single leds, 3 leds not used
|
||||
//byte tubePixels[] = {0,0,1,2,3,3}; //4 tubes, 6 leds
|
||||
//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
|
||||
//byte tubePixels[] = {0,1,2,3,4,5,6,7}; //8 tubes, single leds
|
||||
//byte tubePixels[] = {3,2,6,1,0}; //Numitron 4 tubes, 4 x single leds + 1. The extra led in the middle is unused and always stays dark!
|
||||
//byte tubePixels[] = {0,1,2,3,3,2,1,0}; //4 tubes, double row, 8 leds
|
||||
//byte tubePixels[] = {0,0,1,1,2,2,3,3}; //4 tubes, double row, 8 leds
|
||||
//byte tubePixels[] = {3,3,2,2,1,1,0,0, 0,0,1,1,2,2,3,3}; //4 tubes, double row, 16 leds (GB)
|
||||
//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)
|
||||
//byte tubePixels[] = {0,0,0,1,1,2,2,3,3,3,3, 3,3,2,2,2,1,1,0,0}; //4 tubes, double row, 20 leds (Robi)
|
||||
|
||||
// tubePixels is a byte[] so sizeof(tubePixels) already equals its element count; PixelCount is
|
||||
// set 2 *higher* than that count on purpose, as headroom for a couple of always-dark "spare"
|
||||
// pixels at the tail of the strip (see the further +4 used when sizing `strip` below). Every loop
|
||||
// in this file iterates `for (i=0; i<PixelCount; i++)`; use tubeForPixel(i) below instead of
|
||||
// indexing tubePixels[i] directly, so those last 2 "spare" positions (i == length and
|
||||
// i == length+1, past the real end of tubePixels[]) resolve to a safe sentinel instead of reading
|
||||
// past the array. See alarmLight(), rainbow(), effect1(), effect2(), setPixels(), fixColor().
|
||||
const int PixelCount = sizeof(tubePixels)+2;
|
||||
|
||||
// FIXED: centralizes what used to be a direct "tubePixels[i]" array read at 6 call sites (alarmLight(),
|
||||
// rainbow(), effect1(), effect2(), setPixels(), fixColor()) - those all loop i up to PixelCount-1,
|
||||
// which is 2 past the end of the real tubePixels[] array (see the PixelCount note above), so a raw
|
||||
// tubePixels[i] there used to read undefined/out-of-bounds memory for the "spare" positions. This
|
||||
// helper instead returns a sentinel (255, always >= any real maxDigits/tube-index value) for those
|
||||
// positions, preserving the original "always dark / never matches a real tube" intent safely.
|
||||
inline byte tubeForPixel(int i) {
|
||||
return (i < (int)sizeof(tubePixels)) ? tubePixels[i] : 255;
|
||||
}
|
||||
|
||||
//NeoGrbFeature gives BRGW (G and B swapped)
|
||||
//NeoRgbFeature gives RBGW (G and B swapped)
|
||||
//NeoBrgFeature gives BGRW (G and R swapped)
|
||||
|
||||
#if defined(ESP32)
|
||||
byte PixelPin = NEOPIXEL_PIN; //on ESP32 usable any pin below 32
|
||||
//Strip is allocated with PixelCount+4 physical pixels (a few more than
|
||||
//PixelCount, which is itself already tubePixels-length+2 - see note above)
|
||||
//as headroom so the driver never writes past its own frame buffer, even
|
||||
//though every loop only ever indexes strip positions 0..PixelCount-1.
|
||||
//NeoEsp32I2s1800KbpsMethod drives the strip via the ESP32's I2S peripheral
|
||||
//(DMA-fed) instead of RMT; this is more tolerant of interrupts/WiFi jitter
|
||||
//than NeoEsp32Rmt7Ws2812xMethod, which can glitch the strict WS2812 bit
|
||||
//timing (~1.25us/bit) if an ISR delays the RMT hardware mid-frame.
|
||||
NeoPixelBrightnessBus<NeoGrbFeature, NeoEsp32I2s1800KbpsMethod> strip(PixelCount+4,PixelPin); //NeoEsp32I2s1800KbpsMethod is used here because it is more reliable than NeoEsp32Rmt7Ws2812xMethod!
|
||||
#else
|
||||
#define NEOPIXEL_PIN 3
|
||||
byte PixelPin = 3; // on 8266 it MUST use GPIO3 (RX pin)
|
||||
//8266 has no I2S/RMT WS2812 helper here; Neo800KbpsMethod bit-bangs the
|
||||
//protocol with cycle-counted delays, which is why GPIO3/RX is mandated -
|
||||
//it's one of the few pins whose timing isn't disturbed by the UART/boot ROM.
|
||||
NeoPixelBrightnessBus<NeoGrbFeature, Neo800KbpsMethod> strip(PixelCount+4);
|
||||
#endif
|
||||
|
||||
//Gamma-correction lookup table object - NOTE: currently declared but never
|
||||
//applied (colorGamma.Correct(...) is not called anywhere in this file), so
|
||||
//colors are sent to the strip linearly, not perceptually corrected. Possible
|
||||
//leftover from an earlier version / an intended-but-unfinished improvement.
|
||||
NeoGamma<NeoGammaTableMethod> colorGamma;
|
||||
|
||||
void setupNeopixel() {
|
||||
DPRINTLN("Setup NeoPixel LEDS");
|
||||
regPin(PixelPin,"NEOPIXEL_PIN");
|
||||
DPRINT("Pixel count: "); DPRINTLN(PixelCount);
|
||||
DPRINT("Brightness:"); DPRINT(c_MinBrightness); DPRINT(" - "); DPRINTLN(c_MaxBrightness);
|
||||
neoBrightness = prm.rgbBrightness;
|
||||
strip.Begin(); //initialize the underlying I2S/RMT/bit-bang driver and GPIO
|
||||
strip.Show(); //push the (still all-black, default-constructed) buffer out so the strip starts dark, not showing stale/garbage pixel data
|
||||
strip.SetBrightness(neoBrightness);
|
||||
fixColor(prm.rgbFixColor); //apply the persisted fixed color immediately so the backlight is correct even before the first doAnimationMakuna() tick
|
||||
}
|
||||
|
||||
//Classic Adafruit/NeoPixel "color wheel": maps a single byte (0..255) onto a
|
||||
//position around the RGB hue circle, producing a smooth rainbow as WheelPos
|
||||
//increases. Split into 3 arcs of 85 steps (255/3) - red->blue, blue->green,
|
||||
//green->red - each linearly interpolating between the two arc endpoints
|
||||
//(hence the `* 3` scaling: 85*3 = 255, so each arc's channel spans the full
|
||||
//0..255 range). WHITE_INDEX (192) is carved out of the wheel as a special
|
||||
//sentinel value that instead returns solid white, since a pure hue rotation
|
||||
//can never itself produce white (R=G=B).
|
||||
RgbColor Wheel(byte WheelPos) {
|
||||
|
||||
if (WheelPos == WHITE_INDEX)
|
||||
return white;
|
||||
|
||||
WheelPos = 255 - WheelPos; //invert so increasing WheelPos visually rotates the same direction as prm.rgbDir expects
|
||||
if(WheelPos < 85) {
|
||||
return RgbColor(255 - WheelPos * 3, 0, WheelPos * 3);
|
||||
} else if(WheelPos < 170) {
|
||||
WheelPos -= 85;
|
||||
return RgbColor(0, WheelPos * 3, 255 - WheelPos * 3);
|
||||
} else {
|
||||
WheelPos -= 170;
|
||||
return RgbColor(WheelPos * 3, 255 - WheelPos * 3, 0);
|
||||
}
|
||||
}
|
||||
|
||||
//Flashes all tube-backlight pixels white on/off at 2Hz (500ms half-period)
|
||||
//while the alarm is ringing, overriding whatever normal rgbEffect was active.
|
||||
//Brightness is hardcoded to 200 here (ignores prm.rgbBrightness/autoBrightness)
|
||||
//so the alarm flash is always clearly visible regardless of the user's normal
|
||||
//backlight/dimming settings. Called every doAnimationMakuna() tick while
|
||||
//alarmON is true (see that function), so the 500ms gate below is what actually
|
||||
//paces the blink - doAnimationMakuna() itself may be called much more often.
|
||||
void alarmLight() {
|
||||
static unsigned long lastRun = 0;
|
||||
static byte counter;
|
||||
|
||||
if ((millis()-lastRun)<500) return;
|
||||
lastRun = millis();
|
||||
counter++;
|
||||
strip.SetBrightness(200);
|
||||
for(volatile int i=0; i<PixelCount; i++) {
|
||||
if ((counter%2) && tubeForPixel(i)<maxDigits) //>=maxDigits marks a "no tube here" filler pixel - always kept dark, even during the flash
|
||||
strip.SetPixelColor(i, RgbColor(255,255,255));
|
||||
else
|
||||
strip.SetPixelColor(i,black);
|
||||
}
|
||||
strip.Show();
|
||||
}
|
||||
|
||||
//Called once from the core's alarmSound() when the alarm's ring period times
|
||||
//out naturally, to make sure the alarmLight() flashing doesn't get left mid-
|
||||
//flash (i.e. stuck lit white) - forces brightness to 0 and every pixel black,
|
||||
//then pushes it out immediately rather than waiting for the next
|
||||
//doAnimationMakuna() tick to restore the normal effect.
|
||||
void darkenNeopixels() {
|
||||
strip.SetBrightness(0);
|
||||
for (int i=0;i<PixelCount;i++) {
|
||||
strip.SetPixelColor(i,black);
|
||||
}
|
||||
strip.Show();
|
||||
}
|
||||
|
||||
//"Flow" rainbow: each physical pixel gets a color offset by its own index i
|
||||
//plus a global phase j, so a continuous rainbow gradient appears to slide
|
||||
//across the whole strip. `& 255` wraps the wheel position instead of using
|
||||
//modulo (index is built from small ints, so this is just a cheap wraparound).
|
||||
//index==WHITE_INDEX is nudged by +1 to skip the special "solid white" wheel
|
||||
//position - otherwise a pixel would randomly flash white as the gradient
|
||||
//scrolls past it, which would look like a glitch rather than part of the flow.
|
||||
//Speed is controlled purely by how often doAnimationMakuna() calls this
|
||||
//function (via its own millis() gate), not by anything in here - j advances
|
||||
//exactly one step per call.
|
||||
void rainbow() {
|
||||
static byte j=0;
|
||||
volatile int i;
|
||||
int index;
|
||||
|
||||
for(i=0; i<PixelCount; i++) {
|
||||
index = (i+j) & 255;
|
||||
if (index == WHITE_INDEX) index++;
|
||||
if (tubeForPixel(i)<maxDigits) strip.SetPixelColor(i, Wheel(index));
|
||||
else strip.SetPixelColor(i,black);
|
||||
}
|
||||
if (prm.rgbDir) { j++; }
|
||||
else { j--; }
|
||||
strip.Show();
|
||||
}
|
||||
|
||||
//"Rainbow Stepper": unlike rainbow() which shifts every pixel together every
|
||||
//frame, this lights one tube (i) at a time with the current wheel color (j),
|
||||
//then advances to the next tube - so the rainbow visibly steps tube-by-tube
|
||||
//instead of flowing continuously. Uses its own millis()-based rate limiter
|
||||
//(spd) on top of doAnimationMakuna()'s, because prm.rgbSpeed here controls the
|
||||
//per-tube step delay directly rather than the frame rate: `steps * (258-speed)`
|
||||
//means higher rgbSpeed -> smaller multiplier -> shorter delay -> faster
|
||||
//stepping. max(0, ...) is defensive: rgbSpeed is expected to stay in 1..255
|
||||
//per the settings JSON, so (258-speed) should never go negative, but this
|
||||
//guards against underflow/overflow if it ever does.
|
||||
void rainbow2() { //Rainbow Stepper
|
||||
static int16_t j=0;
|
||||
static int16_t i=0;
|
||||
static unsigned long lastRun = 0;
|
||||
int steps = 15;
|
||||
unsigned long spd = max(0,steps * (258-prm.rgbSpeed));
|
||||
|
||||
if ((millis()-lastRun)<spd) return;
|
||||
lastRun = millis();
|
||||
|
||||
if (i>=maxDigits) { //walked off the end of the tubes -> wrap back to the first tube and advance the color phase by a full "steps" jump
|
||||
if (j>255) j=0;
|
||||
i=0;
|
||||
j+=steps;
|
||||
} //endif
|
||||
|
||||
if (i<0) { //same wrap, but for the reverse (prm.rgbDir==0) direction
|
||||
if (j>255) j=0;
|
||||
i=maxDigits-1;
|
||||
j+=steps;
|
||||
} //endif
|
||||
|
||||
//DPRINT(i); DPRINT("/"); DPRINTLN(j);
|
||||
|
||||
setPixels(i, Wheel((j==192) ? j+1 : j)); //skip WHITE_INDEX (192) here too, same reasoning as in rainbow()
|
||||
|
||||
if (prm.rgbDir)i++;
|
||||
else i--;
|
||||
|
||||
strip.Show();
|
||||
}
|
||||
|
||||
//All tubes show the same solid color (c), while the overall strip brightness
|
||||
//ramps down from neoBrightness to c_MinBrightness and back up (a "breathing"
|
||||
//dimmer), picking a new random color each time it bottoms out. `counter` is
|
||||
//the amount subtracted from neoBrightness; dir flips its sign at each end of
|
||||
//the ramp. The `(cnt%3==0)` half of the OR is dead in practice: cnt is a
|
||||
//static byte that is declared but never incremented anywhere in this
|
||||
//function, so it is always 0 and `cnt%3==0` is always true - meaning the
|
||||
//`(neoBrightness-counter)>20` guard is effectively bypassed and counter
|
||||
//advances by `dir` (i.e. 1) every single call regardless. Net effect: the
|
||||
//breathing speed is fixed at 1 brightness step per doAnimationMakuna() tick,
|
||||
//not throttled as the "> 20" condition alone would suggest.
|
||||
void effect1() { //color dimmer
|
||||
static int c = 0; //actual color
|
||||
static int dir = 1; //direction
|
||||
static long counter = 0;
|
||||
static byte cnt = 0;
|
||||
|
||||
strip.SetBrightness(max(0l,neoBrightness - counter));
|
||||
|
||||
for(volatile int i=0; i<PixelCount; i++) {
|
||||
if (tubeForPixel(i)>=maxDigits)
|
||||
strip.SetPixelColor(i,black);
|
||||
else
|
||||
strip.SetPixelColor(i, Wheel(c));
|
||||
}
|
||||
|
||||
// POSSIBLE BUG: cnt is never incremented (nor reset), so `cnt%3==0` is
|
||||
// always true and this condition always fires - see function comment above.
|
||||
if (((neoBrightness - counter)>20) || (cnt%3 == 0)) {
|
||||
counter += dir;
|
||||
//DPRINTLN(counter);
|
||||
}
|
||||
|
||||
if (counter <= 0) {
|
||||
dir = 1; //hit the bright end of the ramp -> start dimming again
|
||||
}
|
||||
if (counter >= (neoBrightness-c_MinBrightness)) {
|
||||
c = random(0,256); //hit the dim floor -> pick a new random hue and start brightening back up
|
||||
dir = -1;
|
||||
}
|
||||
strip.Show();
|
||||
}
|
||||
|
||||
|
||||
//All tubes show the same solid color, but the color itself jumps to a new
|
||||
//random hue every `100*(258-rgbSpeed)` ms - i.e. higher rgbSpeed means shorter
|
||||
//wait, so colors change faster. Unlike effect1, there's no fade between colors
|
||||
//here, just an instant switch once the timer elapses.
|
||||
void effect2() { //random color picker
|
||||
static int c = random(0,256); //actual color
|
||||
static unsigned long lastRun = 0;
|
||||
|
||||
if ((millis()-lastRun)>100*max(0,(258-prm.rgbSpeed))) {
|
||||
lastRun = millis();
|
||||
c = random(0,256);
|
||||
}
|
||||
|
||||
for(volatile int i=0; i<PixelCount; i++) {
|
||||
if (tubeForPixel(i)>=maxDigits)
|
||||
strip.SetPixelColor(i, black);
|
||||
else
|
||||
strip.SetPixelColor(i, Wheel(c));
|
||||
}
|
||||
strip.Show();
|
||||
}
|
||||
|
||||
//Distance between two positions on the 0..255 color wheel, accounting for
|
||||
//wraparound (the wheel is circular: color 255 is adjacent to color 0). d1 is
|
||||
//the naive linear distance; d2 is the distance going "the other way around"
|
||||
//through the wrap point. Returning the smaller of the two gives the true
|
||||
//shortest hue distance, which the random-color pickers below use to make sure
|
||||
//a freshly rolled random color is visually distinct from the current one
|
||||
//(not just numerically different, since e.g. 250 and 5 are close in hue
|
||||
//despite being far apart as raw numbers).
|
||||
int colorDistance(int c1,int c2) {
|
||||
int d1,d2;
|
||||
d1 = abs(c1-c2);
|
||||
d2 = (c1<c2) ? c1 + 255-c2 : c2 + 255-c1;
|
||||
//DPRINT("c1:"); DPRINT(c1); DPRINT(" c2:"); DPRINT(c2); DPRINT(" d1:"); DPRINT(d1); DPRINT(" d2:"); DPRINTLN(d2);
|
||||
return min(d1,d2);
|
||||
}
|
||||
|
||||
//Shared engine behind rgbEffect 6/7/8 ("color stepflow" variants), selected by
|
||||
//the doAnimationMakuna() dispatcher via two flags:
|
||||
// enableRandom=false -> cycle through the fixed palette table c[]
|
||||
// enableRandom=true -> pick random colors (rejecting ones too
|
||||
// close to the current color, via
|
||||
// colorDistance()/RANDOM_WHEEL_DISTANCE)
|
||||
// eachPixelRandom=true (implies enableRandom) -> every tube gets its own
|
||||
// independent target color instead of all
|
||||
// tubes chasing a single shared color
|
||||
//For each tube, actColor[] is faded step-by-step from oldColor[] towards
|
||||
//newColor[] (see the fade loop near the bottom); once a tube's actColor
|
||||
//catches up to its newColor, the "current tube" pointer i advances (direction
|
||||
//per prm.rgbDir) and, once it wraps around all tubes (or immediately, in
|
||||
//eachPixelRandom mode), a new target color is picked. `step` controls how many
|
||||
//wheel-positions actColor moves per call, chosen so the fade from old to new
|
||||
//color takes roughly 20 calls regardless of how far apart they are.
|
||||
void effect3(boolean enableRandom,boolean eachPixelRandom) {
|
||||
static const int c[] = {255,5,12,22,30,40,54,62,78,85,100,110,122,137,177,WHITE_INDEX,210,227,240};
|
||||
static const int cMax = sizeof(c) / sizeof(c[0]); //size of array
|
||||
//Fixed-size [10] per-tube state arrays, indexed 0..maxDigits-1: only safe as
|
||||
//long as no config's maxDigits exceeds 10 (all loops below use maxDigits as
|
||||
//the bound, never sizeof(newColor)/sizeof(...), so this isn't re-checked at
|
||||
//runtime - a config with more than 10 tubes would silently overrun these).
|
||||
static int newColor[10];
|
||||
static int oldColor[10];
|
||||
static int actColor[10];
|
||||
static int i = 2;
|
||||
static int step = 1;
|
||||
static int idx = 0;
|
||||
static boolean firstRun = true;
|
||||
static int counter = 0;
|
||||
static int dir = 1;
|
||||
int newC = 0;
|
||||
boolean changeColor = false;
|
||||
boolean colorOK;
|
||||
|
||||
if (firstRun) {
|
||||
firstRun = false;
|
||||
for (volatile int k=0;k<maxDigits;k++) {
|
||||
oldColor[k] = 0;
|
||||
newColor[k] = 100;
|
||||
actColor[k] = 0;
|
||||
}
|
||||
step = max(1,abs(newColor[2]-oldColor[2])/20);
|
||||
}
|
||||
|
||||
if (newColor[i] == actColor[i]) { //newColor reached...
|
||||
if (prm.rgbDir) i++; else i--; //goto next pixel
|
||||
|
||||
if (i>=maxDigits) { i=0; changeColor = true;}
|
||||
else if (i<0) {i=maxDigits-1; changeColor = true;}
|
||||
|
||||
if (eachPixelRandom) { //each pixel is random color
|
||||
changeColor = true;
|
||||
}
|
||||
|
||||
if (changeColor) {
|
||||
changeColor = false;
|
||||
if (enableRandom) {
|
||||
counter = 0;
|
||||
while (true) { //random color
|
||||
newC = random(0,256); //get a new random color
|
||||
colorOK = true;
|
||||
if (RANDOM_FROM_ALL_PIXELS) {
|
||||
//require the candidate to be far enough (RANDOM_WHEEL_DISTANCE) from
|
||||
//every tube's current target color, not just this tube's - avoids two
|
||||
//tubes converging on near-identical hues at the same time
|
||||
for (int j=0;j<maxDigits;j++) {
|
||||
if (colorDistance(newC,newColor[j]) < RANDOM_WHEEL_DISTANCE)
|
||||
colorOK = false; //note: at this point newColor[j] still holds each tube's previous (not yet reassigned) color
|
||||
}
|
||||
}
|
||||
else {
|
||||
if (colorDistance(newC,newColor[i]) < RANDOM_WHEEL_DISTANCE) //check random only from actual pixel
|
||||
colorOK = false; //note: at this point newColor[i] still holds this tube's previous (not yet reassigned) color
|
||||
}
|
||||
counter++;
|
||||
//escape valve: if no sufficiently-distinct color turns up within
|
||||
//RANDOM_MAX_COUNTER tries, give up and use the last candidate anyway
|
||||
//rather than risk looping forever (can happen when maxDigits is large
|
||||
//relative to 255/RANDOM_WHEEL_DISTANCE, leaving few "free" hues)
|
||||
if (colorOK || (counter>RANDOM_MAX_COUNTER)) break;
|
||||
} //end while
|
||||
//DPRINT("Pix:"); DPRINT(i); DPRINT(" old:"); DPRINT(newColor[i]); DPRINT(" / newC:"); DPRINT(newC); DPRINT(" No of tries:"); DPRINTLN(counter);
|
||||
}
|
||||
else { //new color from table
|
||||
idx++; if (idx>=cMax) idx = 0;
|
||||
newC = c[idx];
|
||||
}
|
||||
|
||||
if (eachPixelRandom) {
|
||||
oldColor[i] = newColor[i];
|
||||
newColor[i] = newC;
|
||||
}
|
||||
else {
|
||||
for (volatile int k=0;k<maxDigits;k++) {
|
||||
oldColor[k] = newColor[k];
|
||||
newColor[k] = newC;
|
||||
}
|
||||
}
|
||||
|
||||
step = max(1,abs(newColor[i]-oldColor[i])/20);
|
||||
if (newColor[i] > oldColor[i])
|
||||
dir = 1;
|
||||
else
|
||||
dir = -1;
|
||||
|
||||
//DPRINT("Old-New simple distance:"); DPRINT(abs(newC-oldColor[i])); DPRINT(" step:"); DPRINTLN(step);
|
||||
} //endif changeColor
|
||||
|
||||
actColor[i] = oldColor[i]; //starting color
|
||||
} //endif (newColor[i] == actColor[i])
|
||||
|
||||
|
||||
if (actColor[i] != newColor[i]) { //next rainbow color
|
||||
if (dir==1) actColor[i] = min(newColor[i],actColor[i]+step);
|
||||
else actColor[i] = max(newColor[i],actColor[i]-step);
|
||||
}
|
||||
|
||||
for (int j=0;j<maxDigits;j++)
|
||||
setPixels(j, Wheel(actColor[j]));
|
||||
|
||||
strip.Show();
|
||||
//DPRINT("Pix:"); DPRINT(i); DPRINT(" Old:"); DPRINT(oldColor[i]); DPRINT(" ActCol:"); DPRINT(actColor[i]); DPRINT(" New:"); DPRINT(newColor[i]); DPRINT(" Step:"); DPRINTLN(step);
|
||||
}
|
||||
|
||||
|
||||
//Independent per-tube random color fader: each tube runs its own old->new
|
||||
//color fade on its own schedule (no shared "current tube" pointer like
|
||||
//effect3), so all tubes are constantly, independently drifting between random
|
||||
//hues. Same [10]-element fixed-array-size caveat as effect3 above applies here.
|
||||
void effect4() { //every pixel is random changer
|
||||
static int newColor[10];
|
||||
static int oldColor[10];
|
||||
static int actColor[10];
|
||||
static int step[10];
|
||||
static boolean firstRun = true;
|
||||
//unsigned long spd = max(0, 25*(258-prm.rgbSpeed));
|
||||
|
||||
|
||||
//if ((millis()-lastRun)<spd return;
|
||||
|
||||
if (firstRun) {
|
||||
firstRun = false;
|
||||
for (int i=0;i<maxDigits;i++) {
|
||||
newColor[i] = random(0,256);
|
||||
oldColor[i] = random(0,256);
|
||||
actColor[i] = oldColor[i];
|
||||
step[i] = 1;
|
||||
}
|
||||
}
|
||||
|
||||
for (int t=0;t<maxDigits;t++) {
|
||||
|
||||
if (actColor[t] == newColor[t]) { //change color
|
||||
oldColor[t] = newColor[t];
|
||||
do {
|
||||
newColor[t] = random(0,256); //get a new random color
|
||||
} while (colorDistance(newColor[t],oldColor[t])<RANDOM_WHEEL_DISTANCE);
|
||||
|
||||
actColor[t] = oldColor[t]; //starting color
|
||||
step[t] = 1; //max(1,colorDistance(newColor[t],oldColor[t])/20);
|
||||
} //endif changeColor
|
||||
|
||||
//DPRINT(i); DPRINT("/"); DPRINTLN(j);
|
||||
setPixels(t, Wheel(actColor[t]));
|
||||
|
||||
if (newColor[t] != actColor[t]) { //next rainbow color
|
||||
if (oldColor[t] < newColor[t]) actColor[t] = min(newColor[t],actColor[t]+step[t]);
|
||||
else actColor[t] = max(newColor[t],actColor[t]-step[t]);
|
||||
|
||||
}
|
||||
//DPRINT("Pix:"); DPRINT(i); DPRINT(" ActCol:"); DPRINTLN(actColor);
|
||||
} //end for t
|
||||
strip.Show();
|
||||
}
|
||||
|
||||
//Sets the color of every physical pixel that belongs to logical tube `tubeNo`
|
||||
//(a tube can be backed by more than one LED - see the tubePixels[] mapping
|
||||
//examples near the top of this file - so this scans the whole strip rather
|
||||
//than writing a single index). Does NOT call strip.Show() itself; callers
|
||||
//batch up several setPixels() calls and Show() once for efficiency.
|
||||
void setPixels(byte tubeNo, RgbColor c) {
|
||||
for (volatile int i=0;i<PixelCount;i++) {
|
||||
if (tubeNo == tubeForPixel(i)) {
|
||||
strip.SetPixelColor(i,c);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//Sets every tube to a single solid wheel color (rgbEffect==1), or blanks the
|
||||
//whole strip when col==-1 (used as an "off"/error sentinel - see
|
||||
//doAnimationMakuna()'s off-branch and its final fallback for invalid
|
||||
//rgbEffect values).
|
||||
void fixColor(int col) {
|
||||
|
||||
for (int i=0;i<PixelCount;i++) {
|
||||
if ((col == -1) || (tubeForPixel(i)>=maxDigits))
|
||||
strip.SetPixelColor(i,black);
|
||||
else
|
||||
strip.SetPixelColor(i,Wheel(col));
|
||||
}
|
||||
strip.Show();
|
||||
}
|
||||
|
||||
//Knight Rider "KITT" scanner: a single lit tube (using the fixed
|
||||
//prm.rgbFixColor, not the rainbow wheel) bounces back and forth across all
|
||||
//tubes. counter2 divides doAnimationMakuna()'s call rate by 10 so the scanner
|
||||
//advances one tube every 10 ticks instead of every tick (prm.rgbSpeed is not
|
||||
//consulted here, unlike most other effects - the bounce speed is fixed).
|
||||
void kitt() {
|
||||
static int dir = 1; //direction
|
||||
static int counter = 0;
|
||||
static int counter2 = 0;
|
||||
|
||||
counter2 ++;
|
||||
if (counter2 >=10) {
|
||||
counter2 = 0;
|
||||
counter += dir;
|
||||
if (counter >= maxDigits-1) dir = -1; //bounce off the last tube
|
||||
else if (counter <=0 ) dir = 1; //bounce off the first tube
|
||||
}
|
||||
for (int i=0;i<PixelCount;i++) {
|
||||
strip.SetPixelColor(i,black); //clear every frame since only one tube is ever lit
|
||||
}
|
||||
|
||||
setPixels(counter,Wheel(prm.rgbFixColor));
|
||||
strip.Show();
|
||||
}
|
||||
|
||||
|
||||
void doAnimationMakuna() {
|
||||
static unsigned long lastRun = 0;
|
||||
|
||||
if (EEPROMsaving) return;
|
||||
|
||||
if (alarmON) {
|
||||
alarmLight();
|
||||
return;
|
||||
}
|
||||
|
||||
if ((prm.rgbEffect <=1) && ((millis()-lastRun)<1000)) return; //fix color
|
||||
if ((millis()-lastRun)<max(FPS_MSEC,258-prm.rgbSpeed)) return;
|
||||
lastRun = millis();
|
||||
|
||||
if ((prm.rgbEffect == 0) || !displayON || !radarON) { //switch RGB backlight OFF
|
||||
neoBrightness = 0;
|
||||
fixColor(-1);
|
||||
return;
|
||||
}
|
||||
|
||||
colorStep = max(1,prm.rgbSpeed/5);
|
||||
neoBrightness = prm.rgbBrightness;
|
||||
if (autoBrightness) {
|
||||
neoBrightness = (neoBrightness*lx)/long(MAXIMUM_LUX/2);
|
||||
if (neoBrightness< c_MinBrightness)
|
||||
neoBrightness = c_MinBrightness;
|
||||
}
|
||||
neoBrightness = min(neoBrightness,long(RGB_MAX_BRIGHTNESS)); //safety only
|
||||
strip.SetBrightness(neoBrightness);
|
||||
//DPRINTLN(" NeoBrightness:"); DPRINT(neoBrightness);
|
||||
|
||||
if (prm.rgbEffect==1) fixColor(prm.rgbFixColor);
|
||||
else if (prm.rgbEffect==2) rainbow(); //flow
|
||||
else if (prm.rgbEffect==3) rainbow2(); //stepper
|
||||
else if (prm.rgbEffect==4) effect1(); //color dimmer
|
||||
else if (prm.rgbEffect==5) effect2(); //color stepper
|
||||
else if (prm.rgbEffect==6) effect3(false,false); //color stepflow table
|
||||
else if (prm.rgbEffect==7) effect3(true,false); //color stepflow random
|
||||
else if (prm.rgbEffect==8) effect3(true,true); //color stepflow each pixel random
|
||||
else if (prm.rgbEffect==9) effect4(); //color stepflow random all pixels
|
||||
else if (prm.rgbEffect==10) kitt(); //Knight Rider's KITT car
|
||||
else fixColor(-1); //darken leds, if any error happens
|
||||
}
|
||||
|
||||
#else
|
||||
void setupNeopixel() {}
|
||||
void doAnimationMakuna() {}
|
||||
void darkenNeopixels() {}
|
||||
#endif
|
||||
@@ -0,0 +1,24 @@
|
||||
# ESP_UniClock for the NixieIN12v2 clock
|
||||
|
||||
Firmware for this clock, based on [ESP_UniClock](https://github.com/gpeter62/ESP_UniClock) by
|
||||
Peter Gautier (GPL-3.0-or-later), vendored from upstream commit `1189810` (2026-09-07).
|
||||
|
||||
- `ESP32_UniClock2/`: the upstream sketch folder (all `.ino`/`.h` files and the web UI in `data/`).
|
||||
It keeps the upstream layout, so it still opens in the Arduino IDE.
|
||||
- `lib/`: the library versions bundled upstream (`libraries/*.zip`), only those the ESP8266 build
|
||||
needs. The upstream manuals, datasheets, photos and ESP32-only libraries were left out.
|
||||
- `platformio.ini`: PlatformIO build for the ESP-12 (4 MB flash, 2 MB SPIFFS).
|
||||
|
||||
Local changes are separate commits on top of the import (`git log -- firmware/uniclock`).
|
||||
|
||||
## Build and flash
|
||||
|
||||
```sh
|
||||
pio run # build
|
||||
pio run -t upload # flash the firmware over COM7
|
||||
pio run -t uploadfs # flash the web UI (data/) to SPIFFS
|
||||
pio device monitor # serial log, 115200 Bd
|
||||
```
|
||||
|
||||
The CH340C adapter has no auto-reset: before each upload, hold GPIO0 to GND, tap RST to GND, then
|
||||
release GPIO0. See `CLAUDE.md` in the repo root for the wiring.
|
||||
@@ -0,0 +1,5 @@
|
||||
// ArduinoJson - https://arduinojson.org
|
||||
// Copyright © 2014-2022, Benoit BLANCHON
|
||||
// MIT License
|
||||
|
||||
#include "src/ArduinoJson.h"
|
||||
@@ -0,0 +1,783 @@
|
||||
ArduinoJson: change log
|
||||
=======================
|
||||
|
||||
v6.19.4 (2022-04-05)
|
||||
-------
|
||||
|
||||
* Add `ElementProxy::memoryUsage()`
|
||||
* Add `MemberProxy::memoryUsage()` (issue #1730)
|
||||
* Add implicit conversion from `JsonDocument` to `JsonVariant`
|
||||
* Fix comparisons operators with `const JsonDocument&`
|
||||
|
||||
v6.19.3 (2022-03-08)
|
||||
-------
|
||||
|
||||
* Fix `call of overloaded 'String(const char*, int)' is ambiguous`
|
||||
* Fix `JsonString` operator `==` and `!=` for non-zero-terminated string
|
||||
* Fix `-Wsign-conversion` on GCC 8 (issue #1715)
|
||||
* MessagePack: serialize round floats as integers (issue #1718)
|
||||
|
||||
v6.19.2 (2022-02-14)
|
||||
-------
|
||||
|
||||
* Fix `cannot convert 'pgm_p' to 'const void*'` (issue #1707)
|
||||
|
||||
v6.19.1 (2022-01-14)
|
||||
-------
|
||||
|
||||
* Fix crash when adding an object member in a too small `JsonDocument`
|
||||
* Fix filter not working in zero-copy mode (issue #1697)
|
||||
|
||||
v6.19.0 (2022-01-08)
|
||||
-------
|
||||
|
||||
* Remove `ARDUINOJSON_EMBEDDED_MODE` and assume we run on an embedded platform.
|
||||
Dependent settings (like `ARDUINOJSON_DEFAULT_NESTING_LIMIT`) must be set individually.
|
||||
* Change the default of `ARDUINOJSON_USE_DOUBLE` to `1`
|
||||
* Change the default of `ARDUINOJSON_USE_LONG_LONG` to `1` on 32-bit platforms
|
||||
* Add `as<JsonString>()` and `is<JsonString>()`
|
||||
* Add safe bool idiom in `JsonString`
|
||||
* Add support for NUL in string values (issue #1646)
|
||||
* Add support for arbitrary array rank in `copyArray()`
|
||||
* Add support for `char[][]` in `copyArray()`
|
||||
* Remove `DeserializationError == bool` and `DeserializationError != bool`
|
||||
* Renamed undocumented function `isUndefined()` to `isUnbound()`
|
||||
* Fix `JsonVariant::memoryUsage()` for raw strings
|
||||
* Fix `call of overloaded 'swap(BasicJsonDocument&, BasicJsonDocument&)' is ambiguous` (issue #1678)
|
||||
* Fix inconsistent pool capacity between `BasicJsonDocument`'s copy and move constructors
|
||||
* Fix inconsistent pool capacity between `BasicJsonDocument`'s copy and move assignments
|
||||
* Fix return type of `StaticJsonDocument::operator=`
|
||||
* Avoid pool reallocation in `BasicJsonDocument`'s copy assignment if capacity is the same
|
||||
* Avoid including `Arduino.h` when all its features are disabled (issue #1692, PR #1693 by @paulocsanz)
|
||||
* Assume `PROGMEM` is available as soon as `ARDUINO` is defined (consequence of #1693)
|
||||
|
||||
v6.18.5 (2021-09-28)
|
||||
-------
|
||||
|
||||
* Set `ARDUINOJSON_EMBEDDED_MODE` to `1` on Nios II (issue #1657)
|
||||
|
||||
v6.18.4 (2021-09-06)
|
||||
-------
|
||||
|
||||
* Fixed error `'dummy' may be used uninitialized` on GCC 11
|
||||
* Fixed error `expected unqualified-id before 'const'` on GCC 11 (issue #1622)
|
||||
* Filter: exact match takes precedence over wildcard (issue #1628)
|
||||
* Fixed deserialization of `\u0000` (issue #1646)
|
||||
|
||||
v6.18.3 (2021-07-27)
|
||||
-------
|
||||
|
||||
* Changed return type of `convertToJson()` and `Converter<T>::toJson()` to `void`
|
||||
* Added `as<std::string_view>()` and `is<std::string_view>()`
|
||||
|
||||
v6.18.2 (2021-07-19)
|
||||
-------
|
||||
|
||||
* Removed a symlink because the Arduino Library Specification forbids it
|
||||
|
||||
v6.18.1 (2021-07-03)
|
||||
-------
|
||||
|
||||
* Fixed support for `volatile float` and `volatile double` (issue #1557)
|
||||
* Fixed error `[Pe070]: incomplete type is not allowed` on IAR (issue #1560)
|
||||
* Fixed `serializeJson(doc, String)` when allocation fails (issue #1572)
|
||||
* Fixed clang-tidy warnings (issue #1574, PR #1577 by @armandas)
|
||||
* Added fake class `InvalidConversion<T1,T2>` to easily identify invalid conversions (issue #1585)
|
||||
* Added support for `std::string_view` (issue #1578, PR #1554 by @0xFEEDC0DE64)
|
||||
* Fixed warning `definition of implicit copy constructor for 'MsgPackDeserializer' is deprecated because it has a user-declared copy assignment operator`
|
||||
* Added `JsonArray::clear()` (issue #1597)
|
||||
* Fixed `JsonVariant::as<unsigned>()` (issue #1601)
|
||||
* Added support for ESP-IDF component build (PR #1562 by @qt1, PR #1599 by @andreaskuster)
|
||||
|
||||
v6.18.0 (2021-05-05)
|
||||
-------
|
||||
|
||||
* Added support for custom converters (issue #687)
|
||||
* Added support for `Printable` (issue #1444)
|
||||
* Removed support for `char` values, see below (issue #1498)
|
||||
* `deserializeJson()` leaves `\uXXXX` unchanged instead of returning `NotSupported`
|
||||
* `deserializeMsgPack()` inserts `null` instead of returning `NotSupported`
|
||||
* Removed `DeserializationError::NotSupported`
|
||||
* Added `JsonVariant::is<JsonArrayConst/JsonObjectConst>()` (issue #1412)
|
||||
* Added `JsonVariant::is<JsonVariant/JsonVariantConst>()` (issue #1412)
|
||||
* Changed `JsonVariantConst::is<JsonArray/JsonObject>()` to return `false` (issue #1412)
|
||||
* Simplified `JsonVariant::as<T>()` to always return `T` (see below)
|
||||
* Updated folders list in `.mbedignore` (PR #1515 by @AGlass0fMilk)
|
||||
* Fixed member-call-on-null-pointer in `getMember()` when array is empty
|
||||
* `serializeMsgPack(doc, buffer, size)` doesn't add null-terminator anymore (issue #1545)
|
||||
* `serializeJson(doc, buffer, size)` adds null-terminator only if there is enough room
|
||||
* PlatformIO: set `build.libArchive` to `false` (PR #1550 by @askreet)
|
||||
|
||||
> ### BREAKING CHANGES
|
||||
>
|
||||
> #### Support for `char` removed
|
||||
>
|
||||
> We cannot cast a `JsonVariant` to a `char` anymore, so the following will break:
|
||||
> ```c++
|
||||
> char age = doc["age"]; // error: no matching function for call to 'variantAs(VariantData*&)'
|
||||
> ```
|
||||
> Instead, you must use another integral type, such as `int8_t`:
|
||||
> ```c++
|
||||
> int8_t age = doc["age"]; // OK
|
||||
> ```
|
||||
>
|
||||
> Similarly, we cannot assign from a `char` anymore, so the following will break:
|
||||
> ```c++
|
||||
> char age;
|
||||
> doc["age"] = age; // error: no matching function for call to 'VariantRef::set(const char&)'
|
||||
> ```
|
||||
> Instead, you must use another integral type, such as `int8_t`:
|
||||
> ```c++
|
||||
> int8_t age;
|
||||
> doc["age"] = age; // OK
|
||||
> ```
|
||||
> A deprecation warning with the message "Support for `char` is deprecated, use `int8_t` or `uint8_t` instead" was added to allow a smooth transition.
|
||||
>
|
||||
> #### `as<T>()` always returns `T`
|
||||
>
|
||||
> Previously, `JsonVariant::as<T>()` could return a type different from `T`.
|
||||
> The most common example is `as<char*>()` that returned a `const char*`.
|
||||
> While this feature simplified a few use cases, it was confusing and complicated the
|
||||
> implementation of custom converters.
|
||||
>
|
||||
> Starting from this version, `as<T>` doesn't try to auto-correct the return type and always return `T`,
|
||||
> which means that you cannot write this anymore:
|
||||
>
|
||||
> ```c++
|
||||
> Serial.println(doc["sensor"].as<char*>()); // error: invalid conversion from 'const char*' to 'char*' [-fpermissive]
|
||||
> ```
|
||||
>
|
||||
> Instead, you must write:
|
||||
>
|
||||
> ```c++
|
||||
> Serial.println(doc["sensor"].as<const char*>()); // OK
|
||||
> ```
|
||||
>
|
||||
> A deprecation warning with the message "Replace `as<char*>()` with `as<const char*>()`" was added to allow a smooth transition.
|
||||
>
|
||||
> #### `DeserializationError::NotSupported` removed
|
||||
>
|
||||
> On a different topic, `DeserializationError::NotSupported` has been removed.
|
||||
> Instead of returning this error:
|
||||
>
|
||||
> * `deserializeJson()` leaves `\uXXXX` unchanged (only when `ARDUINOJSON_DECODE_UNICODE` is `0`)
|
||||
> * `deserializeMsgPack()` replaces unsupported values with `null`s
|
||||
>
|
||||
> #### Const-aware `is<T>()`
|
||||
>
|
||||
> Lastly, a very minor change concerns `JsonVariantConst::is<T>()`.
|
||||
> It used to return `true` for `JsonArray` and `JsonOject`, but now it returns `false`.
|
||||
> Instead, you must use `JsonArrayConst` and `JsonObjectConst`.
|
||||
|
||||
v6.17.3 (2021-02-15)
|
||||
-------
|
||||
|
||||
* Made `JsonDocument`'s destructor protected (issue #1480)
|
||||
* Added missing calls to `client.stop()` in `JsonHttpClient.ino` (issue #1485)
|
||||
* Fixed error `expected ')' before 'char'` when `isdigit()` is a macro (issue #1487)
|
||||
* Fixed error `definition of implicit copy constructor is deprecated` on Clang 10
|
||||
* PlatformIO: set framework compatibility to `*` (PR #1490 by @maxgerhardt)
|
||||
|
||||
v6.17.2 (2020-11-14)
|
||||
-------
|
||||
|
||||
* Fixed invalid conversion error in `operator|(JsonVariant, char*)` (issue #1432)
|
||||
* Changed the default value of `ARDUINOJSON_ENABLE_PROGMEM` (issue #1433).
|
||||
It now checks that the `pgm_read_XXX` macros are defined before enabling `PROGMEM`.
|
||||
|
||||
v6.17.1 (2020-11-07)
|
||||
-------
|
||||
|
||||
* Fixed error `ambiguous overload for 'operator|'` (issue #1411)
|
||||
* Fixed `operator|(MemberProxy, JsonObject)` (issue #1415)
|
||||
* Allowed more than 32767 values in non-embedded mode (issue #1414)
|
||||
|
||||
v6.17.0 (2020-10-19)
|
||||
-------
|
||||
|
||||
* Added a build failure when nullptr is defined as a macro (issue #1355)
|
||||
* Added `JsonDocument::overflowed()` which tells if the memory pool was too small (issue #1358)
|
||||
* Added `DeserializationError::EmptyInput` which tells if the input was empty
|
||||
* Added `DeserializationError::f_str()` which returns a `const __FlashStringHelper*` (issue #846)
|
||||
* Added `operator|(JsonVariantConst, JsonVariantConst)`
|
||||
* Added filtering for MessagePack (issue #1298, PR #1394 by Luca Passarella)
|
||||
* Moved float convertion tables to PROGMEM
|
||||
* Fixed `JsonVariant::set((char*)0)` which returned false instead of true (issue #1368)
|
||||
* Fixed error `No such file or directory #include <WString.h>` (issue #1381)
|
||||
|
||||
v6.16.1 (2020-08-04)
|
||||
-------
|
||||
|
||||
* Fixed `deserializeJson()` that stopped reading after `{}` (issue #1335)
|
||||
|
||||
v6.16.0 (2020-08-01)
|
||||
-------
|
||||
|
||||
* Added comparisons (`>`, `>=`, `==`, `!=`, `<`, and `<=`) between `JsonVariant`s
|
||||
* Added string deduplication (issue #1303)
|
||||
* Added `JsonString::operator!=`
|
||||
* Added wildcard key (`*`) for filters (issue #1309)
|
||||
* Set `ARDUINOJSON_DECODE_UNICODE` to `1` by default
|
||||
* Fixed `copyArray()` not working with `String`, `ElementProxy`, and `MemberProxy`
|
||||
* Fixed error `getOrAddElement is not a member of ElementProxy` (issue #1311)
|
||||
* Fixed excessive stack usage when compiled with `-Og` (issues #1210 and #1314)
|
||||
* Fixed `Warning[Pa093]: implicit conversion from floating point to integer` on IAR compiler (PR #1328 by @stawiski)
|
||||
|
||||
v6.15.2 (2020-05-15)
|
||||
-------
|
||||
|
||||
* CMake: don't build tests when imported in another project
|
||||
* CMake: made project arch-independent
|
||||
* Visual Studio: fixed error C2766 with flag `/Zc:__cplusplus` (issue #1250)
|
||||
* Added support for `JsonDocument` to `copyArray()` (issue #1255)
|
||||
* Added support for `enum`s in `as<T>()` and `is<T>()` (issue #1256)
|
||||
* Added `JsonVariant` as an input type for `deserializeXxx()`
|
||||
For example, you can do: `deserializeJson(doc2, doc1["payload"])`
|
||||
* Break the build if using 64-bit integers with ARDUINOJSON_USE_LONG_LONG==0
|
||||
|
||||
v6.15.1 (2020-04-08)
|
||||
-------
|
||||
|
||||
* Fixed "maybe-uninitialized" warning (issue #1217)
|
||||
* Fixed "statement is unreachable" warning on IAR (issue #1233)
|
||||
* Fixed "pointless integer comparison" warning on IAR (issue #1233)
|
||||
* Added CMake "install" target (issue #1209)
|
||||
* Disabled alignment on AVR (issue #1231)
|
||||
|
||||
v6.15.0 (2020-03-22)
|
||||
-------
|
||||
|
||||
* Added `DeserializationOption::Filter` (issue #959)
|
||||
* Added example `JsonFilterExample.ino`
|
||||
* Changed the array subscript operator to automatically add missing elements
|
||||
* Fixed "deprecated-copy" warning on GCC 9 (fixes #1184)
|
||||
* Fixed `MemberProxy::set(char[])` not duplicating the string (issue #1191)
|
||||
* Fixed enums serialized as booleans (issue #1197)
|
||||
* Fixed incorrect string comparison on some platforms (issue #1198)
|
||||
* Added move-constructor and move-assignment to `BasicJsonDocument`
|
||||
* Added `BasicJsonDocument::garbageCollect()` (issue #1195)
|
||||
* Added `StaticJsonDocument::garbageCollect()`
|
||||
* Changed copy-constructor of `BasicJsonDocument` to preserve the capacity of the source.
|
||||
* Removed copy-constructor of `JsonDocument` (issue #1189)
|
||||
|
||||
> ### BREAKING CHANGES
|
||||
>
|
||||
> #### Copy-constructor of `BasicJsonDocument`
|
||||
>
|
||||
> In previous versions, the copy constructor of `BasicJsonDocument` looked at the source's `memoryUsage()` to choose its capacity.
|
||||
> Now, the copy constructor of `BasicJsonDocument` uses the same capacity as the source.
|
||||
>
|
||||
> Example:
|
||||
>
|
||||
> ```c++
|
||||
> DynamicJsonDocument doc1(64);
|
||||
> doc1.set(String("example"));
|
||||
>
|
||||
> DynamicJsonDocument doc2 = doc1;
|
||||
> Serial.print(doc2.capacity()); // 8 with ArduinoJson 6.14
|
||||
> // 64 with ArduinoJson 6.15
|
||||
> ```
|
||||
>
|
||||
> I made this change to get consistent results between copy-constructor and move-constructor, and whether RVO applies or not.
|
||||
>
|
||||
> If you use the copy-constructor to optimize your documents, you can use `garbageCollect()` or `shrinkToFit()` instead.
|
||||
>
|
||||
> #### Copy-constructor of `JsonDocument`
|
||||
>
|
||||
> In previous versions, it was possible to create a function that take a `JsonDocument` by value.
|
||||
>
|
||||
> ```c++
|
||||
> void myFunction(JsonDocument doc) {}
|
||||
> ```
|
||||
>
|
||||
> This function gives the wrong clues because it doesn't receive a copy of the `JsonDocument`, only a sliced version.
|
||||
> It worked because the copy constructor copied the internal pointers, but it was an accident.
|
||||
>
|
||||
> From now, if you need to pass a `JsonDocument` to a function, you must use a reference:
|
||||
>
|
||||
> ```c++
|
||||
> void myFunction(JsonDocument& doc) {}
|
||||
> ```
|
||||
|
||||
v6.14.1 (2020-01-27)
|
||||
-------
|
||||
|
||||
* Fixed regression in UTF16 decoding (issue #1173)
|
||||
* Fixed `containsKey()` on `JsonVariantConst`
|
||||
* Added `getElement()` and `getMember()` to `JsonVariantConst`
|
||||
|
||||
v6.14.0 (2020-01-16)
|
||||
-------
|
||||
|
||||
* Added `BasicJsonDocument::shrinkToFit()`
|
||||
* Added support of `uint8_t` for `serializeJson()`, `serializeJsonPretty()`, and `serializeMsgPack()` (issue #1142)
|
||||
* Added `ARDUINOJSON_ENABLE_COMMENTS` to enable support for comments (defaults to 0)
|
||||
* Auto enable support for `std::string` and `std::stream` on modern compilers (issue #1156)
|
||||
(No need to define `ARDUINOJSON_ENABLE_STD_STRING` and `ARDUINOJSON_ENABLE_STD_STREAM` anymore)
|
||||
* Improved decoding of UTF-16 surrogate pairs (PR #1157 by @kaysievers)
|
||||
(ArduinoJson now produces standard UTF-8 instead of CESU-8)
|
||||
* Added `measureJson`, `measureJsonPretty`, and `measureMsgPack` to `keywords.txt`
|
||||
(This file is used for syntax highlighting in the Arduino IDE)
|
||||
* Fixed `variant.is<nullptr_t>()`
|
||||
* Fixed value returned by `serializeJson()`, `serializeJsonPretty()`, and `serializeMsgPack()` when writing to a `String`
|
||||
* Improved speed of `serializeJson()`, `serializeJsonPretty()`, and `serializeMsgPack()` when writing to a `String`
|
||||
|
||||
> ### BREAKING CHANGES
|
||||
>
|
||||
> #### Comments
|
||||
>
|
||||
> Support for comments in input is now optional and disabled by default.
|
||||
>
|
||||
> If you need support for comments, you must defined `ARDUINOJSON_ENABLE_COMMENTS` to `1`; otherwise, you'll receive `InvalidInput` errors.
|
||||
>
|
||||
> ```c++
|
||||
> #define ARDUINOJSON_ENABLE_COMMENTS 1
|
||||
> #include <ArduinoJson.h>
|
||||
> ```
|
||||
|
||||
v6.13.0 (2019-11-01)
|
||||
-------
|
||||
|
||||
* Added support for custom writer/reader classes (issue #1088)
|
||||
* Added conversion from `JsonArray` and `JsonObject` to `bool`, to be consistent with `JsonVariant`
|
||||
* Fixed `deserializeJson()` when input contains duplicate keys (issue #1095)
|
||||
* Improved `deserializeMsgPack()` speed by reading several bytes at once
|
||||
* Added detection of Atmel AVR8/GNU C Compiler (issue #1112)
|
||||
* Fixed deserializer that stopped reading at the first `0xFF` (PR #1118 by @mikee47)
|
||||
* Fixed dangling reference in copies of `MemberProxy` and `ElementProxy` (issue #1120)
|
||||
|
||||
v6.12.0 (2019-09-05)
|
||||
-------
|
||||
|
||||
* Use absolute instead of relative includes (issue #1072)
|
||||
* Changed `JsonVariant::as<bool>()` to return `true` for any non-null value (issue #1005)
|
||||
* Moved ancillary files to `extras/` (issue #1011)
|
||||
|
||||
v6.11.5 (2019-08-23)
|
||||
-------
|
||||
|
||||
* Added fallback implementations of `strlen_P()`, `strncmp_P()`, `strcmp_P()`, and `memcpy_P()` (issue #1073)
|
||||
|
||||
v6.11.4 (2019-08-12)
|
||||
-------
|
||||
|
||||
* Added `measureJson()` to the `ArduinoJson` namespace (PR #1069 by @nomis)
|
||||
* Added support for `basic_string<char, traits, allocator>` (issue #1045)
|
||||
* Fixed example `JsonConfigFile.ino` for ESP8266
|
||||
* Include `Arduino.h` if `ARDUINO` is defined (PR #1071 by @nomis)
|
||||
|
||||
v6.11.3 (2019-07-22)
|
||||
-------
|
||||
|
||||
* Added operators `==` and `!=` for `JsonDocument`, `ElementProxy`, and `MemberProxy`
|
||||
* Fixed comparison of `JsonVariant` when one contains a linked string and the other contains an owned string (issue #1051)
|
||||
|
||||
v6.11.2 (2019-07-08)
|
||||
-------
|
||||
|
||||
* Fixed assignment of `JsonDocument` to `JsonVariant` (issue #1023)
|
||||
* Fix invalid conversion error on Particle Argon (issue #1035)
|
||||
|
||||
v6.11.1 (2019-06-21)
|
||||
-------
|
||||
|
||||
* Fixed `serialized()` not working with Flash strings (issue #1030)
|
||||
|
||||
v6.11.0 (2019-05-26)
|
||||
-------
|
||||
|
||||
* Fixed `deserializeJson()` silently accepting a `Stream*` (issue #978)
|
||||
* Fixed invalid result from `operator|` (issue #981)
|
||||
* Made `deserializeJson()` more picky about trailing characters (issue #980)
|
||||
* Added `ARDUINOJSON_ENABLE_NAN` (default=0) to enable NaN in JSON (issue #973)
|
||||
* Added `ARDUINOJSON_ENABLE_INFINITY` (default=0) to enable Infinity in JSON
|
||||
* Removed implicit conversion in comparison operators (issue #998)
|
||||
* Added lexicographical comparison for `JsonVariant`
|
||||
* Added support for `nullptr` (issue #998)
|
||||
|
||||
> ### BREAKING CHANGES
|
||||
>
|
||||
> #### NaN and Infinity
|
||||
>
|
||||
> The JSON specification allows neither NaN not Infinity, but previous
|
||||
> versions of ArduinoJson supported it. Now, ArduinoJson behaves like most
|
||||
> other libraries: a NaN or and Infinity in the `JsonDocument`, becomes
|
||||
> a `null` in the output JSON. Also, `deserializeJson()` returns
|
||||
> `InvalidInput` if the JSON document contains NaN or Infinity.
|
||||
>
|
||||
> This version still supports NaN and Infinity in JSON documents, but
|
||||
> it's disabled by default to be compatible with other JSON parsers.
|
||||
> If you need the old behavior back, define `ARDUINOJSON_ENABLE_NAN` and
|
||||
> `ARDUINOJSON_ENABLE_INFINITY` to `1`;:
|
||||
>
|
||||
> ```c++
|
||||
> #define ARDUINOJSON_ENABLE_NAN 1
|
||||
> #define ARDUINOJSON_ENABLE_INFINITY 1
|
||||
> #include <ArduinoJson.h>
|
||||
> ```
|
||||
>
|
||||
> #### The "or" operator
|
||||
>
|
||||
> This version slightly changes the behavior of the | operator when the
|
||||
> variant contains a float and the user requests an integer.
|
||||
>
|
||||
> Older versions returned the floating point value truncated.
|
||||
> Now, it returns the default value.
|
||||
>
|
||||
> ```c++
|
||||
> // suppose variant contains 1.2
|
||||
> int value = variant | 3;
|
||||
>
|
||||
> // old behavior:
|
||||
> value == 1
|
||||
>
|
||||
> // new behavior
|
||||
> value == 3
|
||||
> ```
|
||||
>
|
||||
> If you need the old behavior, you must add `if (variant.is<float>())`.
|
||||
|
||||
v6.10.1 (2019-04-23)
|
||||
-------
|
||||
|
||||
* Fixed error "attributes are not allowed on a function-definition"
|
||||
* Fixed `deserializeJson()` not being picky enough (issue #969)
|
||||
* Fixed error "no matching function for call to write(uint8_t)" (issue #972)
|
||||
|
||||
v6.10.0 (2019-03-22)
|
||||
-------
|
||||
|
||||
* Fixed an integer overflow in the JSON deserializer
|
||||
* Added overflow handling in `JsonVariant::as<T>()` and `JsonVariant::is<T>()`.
|
||||
- `as<T>()` returns `0` if the integer `T` overflows
|
||||
- `is<T>()` returns `false` if the integer `T` overflows
|
||||
* Added `BasicJsonDocument` to support custom allocator (issue #876)
|
||||
* Added `JsonDocument::containsKey()` (issue #938)
|
||||
* Added `JsonVariant::containsKey()`
|
||||
|
||||
v6.9.1 (2019-03-01)
|
||||
------
|
||||
|
||||
* Fixed warning "unused variable" with GCC 4.4 (issue #912)
|
||||
* Fixed warning "cast increases required alignment" (issue #914)
|
||||
* Fixed warning "conversion may alter value" (issue #914)
|
||||
* Fixed naming conflict with "CAPACITY" (issue #839)
|
||||
* Muted warning "will change in GCC 7.1" (issue #914)
|
||||
* Added a clear error message for `StaticJsonBuffer` and `DynamicJsonBuffer`
|
||||
* Marked ArduinoJson.h as a "system header"
|
||||
|
||||
v6.9.0 (2019-02-26)
|
||||
------
|
||||
|
||||
* Decode escaped Unicode characters like \u00DE (issue #304, PR #791)
|
||||
Many thanks to Daniel Schulte (aka @trilader) who implemented this feature.
|
||||
* Added option ARDUINOJSON_DECODE_UNICODE to enable it
|
||||
* Converted `JsonArray::copyFrom()/copyTo()` to free functions `copyArray()`
|
||||
* Renamed `JsonArray::copyFrom()` and `JsonObject::copyFrom()` to `set()`
|
||||
* Renamed `JsonArray::get()` to `getElement()`
|
||||
* Renamed `JsonArray::add()` (without arg) to `addElement()`
|
||||
* Renamed `JsonObject::get()` to `getMember()`
|
||||
* Renamed `JsonObject::getOrCreate()` to `getOrAddMember()`
|
||||
* Fixed `JsonVariant::isNull()` not returning `true` after `set((char*)0)`
|
||||
* Fixed segfault after `variant.set(serialized((char*)0))`
|
||||
* Detect `IncompleteInput` in `false`, `true`, and `null`
|
||||
* Added `JsonDocument::size()`
|
||||
* Added `JsonDocument::remove()`
|
||||
* Added `JsonVariant::clear()`
|
||||
* Added `JsonVariant::remove()`
|
||||
|
||||
v6.8.0-beta (2019-01-30)
|
||||
-----------
|
||||
|
||||
* Import functions in the ArduinoJson namespace to get clearer errors
|
||||
* Improved syntax highlighting in Arduino IDE
|
||||
* Removed default capacity of `DynamicJsonDocument`
|
||||
* `JsonArray::copyFrom()` accepts `JsonArrayConst`
|
||||
* `JsonVariant::set()` accepts `JsonArrayConst` and `JsonObjectConst`
|
||||
* `JsonDocument` was missing in the ArduinoJson namespace
|
||||
* Added `memoryUsage()` to `JsonArray`, `JsonObject`, and `JsonVariant`
|
||||
* Added `nesting()` to `JsonArray`, `JsonDocument`, `JsonObject`, and `JsonVariant`
|
||||
* Replaced `JsonDocument::nestingLimit` with an additional parameter
|
||||
to `deserializeJson()` and `deserializeMsgPack()`
|
||||
* Fixed uninitialized variant in `JsonDocument`
|
||||
* Fixed `StaticJsonDocument` copy constructor and copy assignment
|
||||
* The copy constructor of `DynamicJsonDocument` chooses the capacity according to the memory usage of the source, not from the capacity of the source.
|
||||
* Added the ability to create/assign a `StaticJsonDocument`/`DynamicJsonDocument` from a `JsonArray`/`JsonObject`/`JsonVariant`
|
||||
* Added `JsonDocument::isNull()`
|
||||
* Added `JsonDocument::operator[]`
|
||||
* Added `ARDUINOJSON_TAB` to configure the indentation character
|
||||
* Reduced the size of the pretty JSON serializer
|
||||
* Added `add()`, `createNestedArray()` and `createNestedObject()` to `JsonVariant`
|
||||
* `JsonVariant` automatically promotes to `JsonObject` or `JsonArray` on write.
|
||||
Calling `JsonVariant::to<T>()` is not required anymore.
|
||||
* `JsonDocument` now support the same operations as `JsonVariant`.
|
||||
Calling `JsonDocument::as<T>()` is not required anymore.
|
||||
* Fixed example `JsonHttpClient.ino`
|
||||
* User can now use a `JsonString` as a key or a value
|
||||
|
||||
> ### BREAKING CHANGES
|
||||
>
|
||||
> #### `DynamicJsonDocument`'s constructor
|
||||
>
|
||||
> The parameter to the constructor of `DynamicJsonDocument` is now mandatory
|
||||
>
|
||||
> Old code:
|
||||
>
|
||||
> ```c++
|
||||
> DynamicJsonDocument doc;
|
||||
> ```
|
||||
>
|
||||
> New code:
|
||||
>
|
||||
> ```c++
|
||||
> DynamicJsonDocument doc(1024);
|
||||
> ```
|
||||
>
|
||||
> #### Nesting limit
|
||||
>
|
||||
> `JsonDocument::nestingLimit` was replaced with a new parameter to `deserializeJson()` and `deserializeMsgPack()`.
|
||||
>
|
||||
> Old code:
|
||||
>
|
||||
> ```c++
|
||||
> doc.nestingLimit = 15;
|
||||
> deserializeJson(doc, input);
|
||||
> ```
|
||||
>
|
||||
> New code:
|
||||
>
|
||||
> ```c++
|
||||
> deserializeJson(doc, input, DeserializationOption::NestingLimit(15));
|
||||
> ```
|
||||
|
||||
v6.7.0-beta (2018-12-07)
|
||||
-----------
|
||||
|
||||
* Removed the automatic expansion of `DynamicJsonDocument`, it now has a fixed capacity.
|
||||
* Restored the monotonic allocator because the code was getting too big
|
||||
* Reduced the memory usage
|
||||
* Reduced the code size
|
||||
* Renamed `JsonKey` to `JsonString`
|
||||
* Removed spurious files in the Particle library
|
||||
|
||||
v6.6.0-beta (2018-11-13)
|
||||
-----------
|
||||
|
||||
* Removed `JsonArray::is<T>(i)` and `JsonArray::set(i,v)`
|
||||
* Removed `JsonObject::is<T>(k)` and `JsonObject::set(k,v)`
|
||||
* Replaced `T JsonArray::get<T>(i)` with `JsonVariant JsonArray::get(i)`
|
||||
* Replaced `T JsonObject::get<T>(k)` with `JsonVariant JsonObject::get(k)`
|
||||
* Added `JSON_STRING_SIZE()`
|
||||
* ~~Replacing or removing a value now releases the memory~~
|
||||
* Added `DeserializationError::code()` to be used in switch statements (issue #846)
|
||||
|
||||
v6.5.0-beta (2018-10-13)
|
||||
-----------
|
||||
|
||||
* Added implicit conversion from `JsonArray` and `JsonObject` to `JsonVariant`
|
||||
* Allow mixed configuration in compilation units (issue #809)
|
||||
* Fixed object keys not being duplicated
|
||||
* `JsonPair::key()` now returns a `JsonKey`
|
||||
* Increased the default capacity of `DynamicJsonDocument`
|
||||
* Fixed `JsonVariant::is<String>()` (closes #763)
|
||||
* Added `JsonArrayConst`, `JsonObjectConst`, and `JsonVariantConst`
|
||||
* Added copy-constructor and copy-assignment-operator for `JsonDocument` (issue #827)
|
||||
|
||||
v6.4.0-beta (2018-09-11)
|
||||
-----------
|
||||
|
||||
* Copy `JsonArray` and `JsonObject`, instead of storing pointers (issue #780)
|
||||
* Added `JsonVariant::to<JsonArray>()` and `JsonVariant::to<JsonObject>()`
|
||||
|
||||
v6.3.0-beta (2018-08-31)
|
||||
-----------
|
||||
|
||||
* Implemented reference semantics for `JsonVariant`
|
||||
* Replaced `JsonPair`'s `key` and `value` with `key()` and `value()`
|
||||
* Fixed `serializeJson(obj[key], dst)` (issue #794)
|
||||
|
||||
> ### BREAKING CHANGES
|
||||
>
|
||||
> #### JsonVariant
|
||||
>
|
||||
> `JsonVariant` now has a semantic similar to `JsonObject` and `JsonArray`.
|
||||
> It's a reference to a value stored in the `JsonDocument`.
|
||||
> As a consequence, a `JsonVariant` cannot be used as a standalone variable anymore.
|
||||
>
|
||||
> Old code:
|
||||
>
|
||||
> ```c++
|
||||
> JsonVariant myValue = 42;
|
||||
> ```
|
||||
>
|
||||
> New code:
|
||||
>
|
||||
> ```c++
|
||||
> DynamicJsonDocument doc;
|
||||
> JsonVariant myValue = doc.to<JsonVariant>();
|
||||
> myValue.set(42);
|
||||
> ```
|
||||
>
|
||||
> #### JsonPair
|
||||
>
|
||||
> Old code:
|
||||
>
|
||||
> ```c++
|
||||
> for(JsonPair p : myObject) {
|
||||
> Serial.println(p.key);
|
||||
> Serial.println(p.value.as<int>());
|
||||
> }
|
||||
> ```
|
||||
>
|
||||
> New code:
|
||||
>
|
||||
> ```c++
|
||||
> for(JsonPair p : myObject) {
|
||||
> Serial.println(p.key());
|
||||
> Serial.println(p.value().as<int>());
|
||||
> }
|
||||
> ```
|
||||
>
|
||||
> CAUTION: the key is now read only!
|
||||
|
||||
v6.2.3-beta (2018-07-19)
|
||||
-----------
|
||||
|
||||
* Fixed exception when using Flash strings as object keys (issue #784)
|
||||
|
||||
v6.2.2-beta (2018-07-18)
|
||||
-----------
|
||||
|
||||
* Fixed `invalid application of 'sizeof' to incomplete type '__FlashStringHelper'` (issue #783)
|
||||
* Fixed `char[]` not duplicated when passed to `JsonVariant::operator[]`
|
||||
|
||||
v6.2.1-beta (2018-07-17)
|
||||
-----------
|
||||
|
||||
* Fixed `JsonObject` not inserting keys of type `String` (issue #782)
|
||||
|
||||
v6.2.0-beta (2018-07-12)
|
||||
-----------
|
||||
|
||||
* Disabled lazy number deserialization (issue #772)
|
||||
* Fixed `JsonVariant::is<int>()` that returned true for empty strings
|
||||
* Improved float serialization when `-fsingle-precision-constant` is used
|
||||
* Renamed function `RawJson()` to `serialized()`
|
||||
* `serializeMsgPack()` now supports values marked with `serialized()`
|
||||
|
||||
> ### BREAKING CHANGES
|
||||
>
|
||||
> #### Non quoted strings
|
||||
>
|
||||
> Non quoted strings are now forbidden in values, but they are still allowed in keys.
|
||||
> For example, `{key:"value"}` is accepted, but `{key:value}` is not.
|
||||
>
|
||||
> #### Preformatted values
|
||||
>
|
||||
> Old code:
|
||||
>
|
||||
> ```c++
|
||||
> object["values"] = RawJson("[1,2,3,4]");
|
||||
> ```
|
||||
>
|
||||
> New code:
|
||||
>
|
||||
> ```c++
|
||||
> object["values"] = serialized("[1,2,3,4]");
|
||||
> ```
|
||||
|
||||
v6.1.0-beta (2018-07-02)
|
||||
-----------
|
||||
|
||||
* Return `JsonArray` and `JsonObject` by value instead of reference (issue #309)
|
||||
* Replaced `success()` with `isNull()`
|
||||
|
||||
> ### BREAKING CHANGES
|
||||
>
|
||||
> Old code:
|
||||
>
|
||||
> ```c++
|
||||
> JsonObject& obj = doc.to<JsonObject>();
|
||||
> JsonArray& arr = obj.createNestedArray("key");
|
||||
> if (!arr.success()) {
|
||||
> Serial.println("Not enough memory");
|
||||
> return;
|
||||
> }
|
||||
> ```
|
||||
>
|
||||
> New code:
|
||||
>
|
||||
> ```c++
|
||||
> JsonObject obj = doc.to<JsonObject>();
|
||||
> JsonArray arr = obj.createNestedArray("key");
|
||||
> if (arr.isNull()) {
|
||||
> Serial.println("Not enough memory");
|
||||
> return;
|
||||
> }
|
||||
> ```
|
||||
|
||||
v6.0.1-beta (2018-06-11)
|
||||
-----------
|
||||
|
||||
* Fixed conflicts with `isnan()` and `isinf()` macros (issue #752)
|
||||
|
||||
v6.0.0-beta (2018-06-07)
|
||||
-----------
|
||||
|
||||
* Added `DynamicJsonDocument` and `StaticJsonDocument`
|
||||
* Added `deserializeJson()`
|
||||
* Added `serializeJson()` and `serializeJsonPretty()`
|
||||
* Added `measureJson()` and `measureJsonPretty()`
|
||||
* Added `serializeMsgPack()`, `deserializeMsgPack()` and `measureMsgPack()` (issue #358)
|
||||
* Added example `MsgPackParser.ino` (issue #358)
|
||||
* Added support for non zero-terminated strings (issue #704)
|
||||
* Removed `JsonBuffer::parseArray()`, `parseObject()` and `parse()`
|
||||
* Removed `JsonBuffer::createArray()` and `createObject()`
|
||||
* Removed `printTo()` and `prettyPrintTo()`
|
||||
* Removed `measureLength()` and `measurePrettyLength()`
|
||||
* Removed all deprecated features
|
||||
|
||||
> ### BREAKING CHANGES
|
||||
>
|
||||
> #### Deserialization
|
||||
>
|
||||
> Old code:
|
||||
>
|
||||
> ```c++
|
||||
> DynamicJsonBuffer jb;
|
||||
> JsonObject& obj = jb.parseObject(json);
|
||||
> if (obj.success()) {
|
||||
>
|
||||
> }
|
||||
> ```
|
||||
>
|
||||
> New code:
|
||||
>
|
||||
> ```c++
|
||||
> DynamicJsonDocument doc;
|
||||
> DeserializationError error = deserializeJson(doc, json);
|
||||
> if (error) {
|
||||
>
|
||||
> }
|
||||
> JsonObject& obj = doc.as<JsonObject>();
|
||||
> ```
|
||||
>
|
||||
> #### Serialization
|
||||
>
|
||||
> Old code:
|
||||
>
|
||||
> ```c++
|
||||
> DynamicJsonBuffer jb;
|
||||
> JsonObject& obj = jb.createObject();
|
||||
> obj["key"] = "value";
|
||||
> obj.printTo(Serial);
|
||||
> ```
|
||||
>
|
||||
> New code:
|
||||
>
|
||||
> ```c++
|
||||
> DynamicJsonDocument obj;
|
||||
> JsonObject& obj = doc.to<JsonObject>();
|
||||
> obj["key"] = "value";
|
||||
> serializeJson(doc, Serial);
|
||||
> ```
|
||||
@@ -0,0 +1,25 @@
|
||||
# ArduinoJson - https://arduinojson.org
|
||||
# Copyright © 2014-2022, Benoit BLANCHON
|
||||
# MIT License
|
||||
|
||||
cmake_minimum_required(VERSION 3.15)
|
||||
|
||||
if(ESP_PLATFORM)
|
||||
# Build ArduinoJson as an ESP-IDF component
|
||||
idf_component_register(INCLUDE_DIRS src)
|
||||
return()
|
||||
endif()
|
||||
|
||||
project(ArduinoJson VERSION 6.19.4)
|
||||
|
||||
if(CMAKE_PROJECT_NAME STREQUAL PROJECT_NAME)
|
||||
include(CTest)
|
||||
endif()
|
||||
|
||||
add_subdirectory(src)
|
||||
|
||||
if(CMAKE_PROJECT_NAME STREQUAL PROJECT_NAME AND BUILD_TESTING)
|
||||
include(extras/CompileOptions.cmake)
|
||||
add_subdirectory(extras/tests)
|
||||
add_subdirectory(extras/fuzzing)
|
||||
endif()
|
||||
@@ -0,0 +1,10 @@
|
||||
# Contribution to ArduinoJson
|
||||
|
||||
First, thank you for taking the time to contribute to this project.
|
||||
|
||||
You can submit changes via GitHub Pull Requests.
|
||||
|
||||
Please:
|
||||
|
||||
1. Update the test suite for any change of behavior
|
||||
2. Use clang-format in "file" mode to format the code
|
||||
@@ -0,0 +1,10 @@
|
||||
The MIT License (MIT)
|
||||
---------------------
|
||||
|
||||
Copyright © 2014-2022, Benoit BLANCHON
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
@@ -0,0 +1,162 @@
|
||||

|
||||
|
||||
---
|
||||
|
||||
[](https://github.com/bblanchon/ArduinoJson/actions?query=workflow%3A%22Continuous+Integration%22+branch%3A6.x)
|
||||
[](https://ci.appveyor.com/project/bblanchon/arduinojson/branch/6.x)
|
||||
[](https://bugs.chromium.org/p/oss-fuzz/issues/list?sort=-opened&can=1&q=proj:arduinojson)
|
||||
[](https://lgtm.com/projects/g/bblanchon/ArduinoJson/)
|
||||
[](https://coveralls.io/github/bblanchon/ArduinoJson?branch=6.x)
|
||||
[](https://www.ardu-badge.com/ArduinoJson/6.19.4)
|
||||
[](https://registry.platformio.org/packages/libraries/bblanchon/ArduinoJson?version=6.19.4)
|
||||
[](https://github.com/bblanchon/ArduinoJson/stargazers)
|
||||
[](https://github.com/sponsors/bblanchon)
|
||||
|
||||
ArduinoJson is a C++ JSON library for Arduino and IoT (Internet Of Things).
|
||||
|
||||
## Features
|
||||
|
||||
* [JSON deserialization](https://arduinojson.org/v6/api/json/deserializejson/?utm_source=github&utm_medium=readme)
|
||||
* [Optionally decodes UTF-16 escape sequences to UTF-8](https://arduinojson.org/v6/api/config/decode_unicode/?utm_source=github&utm_medium=readme)
|
||||
* [Optionally stores links to the input buffer (zero-copy)](https://arduinojson.org/v6/api/json/deserializejson/?utm_source=github&utm_medium=readme)
|
||||
* [Optionally supports comments in the input](https://arduinojson.org/v6/api/config/enable_comments/?utm_source=github&utm_medium=readme)
|
||||
* [Optionally filters the input to keep only desired values](https://arduinojson.org/v6/api/json/deserializejson/?utm_source=github&utm_medium=readme#filtering)
|
||||
* Supports single quotes as a string delimiter
|
||||
* Compatible with [NDJSON](http://ndjson.org/) and [JSON Lines](https://jsonlines.org/)
|
||||
* [JSON serialization](https://arduinojson.org/v6/api/json/serializejson/?utm_source=github&utm_medium=readme)
|
||||
* [Can write to a buffer or a stream](https://arduinojson.org/v6/api/json/serializejson/?utm_source=github&utm_medium=readme)
|
||||
* [Optionally indents the document (prettified JSON)](https://arduinojson.org/v6/api/json/serializejsonpretty/?utm_source=github&utm_medium=readme)
|
||||
* [MessagePack serialization](https://arduinojson.org/v6/api/msgpack/serializemsgpack/?utm_source=github&utm_medium=readme)
|
||||
* [MessagePack deserialization](https://arduinojson.org/v6/api/msgpack/deserializemsgpack/?utm_source=github&utm_medium=readme)
|
||||
* Efficient
|
||||
* [Twice smaller than the "official" Arduino_JSON library](https://arduinojson.org/2019/11/19/arduinojson-vs-arduino_json/?utm_source=github&utm_medium=readme)
|
||||
* [Almost 10% faster than the "official" Arduino_JSON library](https://arduinojson.org/2019/11/19/arduinojson-vs-arduino_json/?utm_source=github&utm_medium=readme)
|
||||
* [Consumes roughly 10% less RAM than the "official" Arduino_JSON library](https://arduinojson.org/2019/11/19/arduinojson-vs-arduino_json/?utm_source=github&utm_medium=readme)
|
||||
* [Fixed memory allocation, no heap fragmentation](https://arduinojson.org/v6/api/jsondocument/?utm_source=github&utm_medium=readme)
|
||||
* [Optionally works without heap memory (zero malloc)](https://arduinojson.org/v6/api/staticjsondocument/?utm_source=github&utm_medium=readme)
|
||||
* [Deduplicates strings](https://arduinojson.org/news/2020/08/01/version-6-16-0/?utm_source=github&utm_medium=readme)
|
||||
* Versatile
|
||||
* Supports [custom allocators (to use external RAM chip, for example)](https://arduinojson.org/v6/how-to/use-external-ram-on-esp32/?utm_source=github&utm_medium=readme)
|
||||
* Supports [`String`](https://arduinojson.org/v6/api/config/enable_arduino_string/?utm_source=github&utm_medium=readme), [`std::string`](https://arduinojson.org/v6/api/config/enable_std_string/?utm_source=github&utm_medium=readme), and [`std::string_view`](https://arduinojson.org/v6/api/config/enable_string_view/?utm_source=github&utm_medium=readme)
|
||||
* Supports [`Stream`](https://arduinojson.org/v6/api/config/enable_arduino_stream/?utm_source=github&utm_medium=readme) and [`std::istream`/`std::ostream`](https://arduinojson.org/v6/api/config/enable_std_stream/?utm_source=github&utm_medium=readme)
|
||||
* Supports [Flash strings](https://arduinojson.org/v6/api/config/enable_progmem/?utm_source=github&utm_medium=readme)
|
||||
* Supports [custom readers](https://arduinojson.org/v6/api/json/deserializejson/?utm_source=github&utm_medium=readme#custom-reader) and [custom writers](https://arduinojson.org/v6/api/json/serializejson/?utm_source=github&utm_medium=readme#custom-writer)
|
||||
* Supports [custom converters](https://arduinojson.org/news/2021/05/04/version-6-18-0/?utm_source=github&utm_medium=readme)
|
||||
* Portable
|
||||
* Usable on any C++ project (not limited to Arduino)
|
||||
* Compatible with C++98, C++11, C++14 and C++17
|
||||
* Zero warnings with `-Wall -Wextra -pedantic` and `/W4`
|
||||
* [Header-only library](https://en.wikipedia.org/wiki/Header-only)
|
||||
* Works with virtually any board
|
||||
* Arduino boards: [Uno](https://amzn.to/38aL2ik), [Due](https://amzn.to/36YkWi2), [Micro](https://amzn.to/35WkdwG), [Nano](https://amzn.to/2QTvwRX), [Mega](https://amzn.to/36XWhuf), [Yun](https://amzn.to/30odURc), [Leonardo](https://amzn.to/36XWjlR)...
|
||||
* Espressif chips: [ESP8266](https://amzn.to/36YluV8), [ESP32](https://amzn.to/2G4pRCB)
|
||||
* Lolin (WeMos) boards: [D1 mini](https://amzn.to/2QUpz7q), [D1 Mini Pro](https://amzn.to/36UsGSs)...
|
||||
* Teensy boards: [4.0](https://amzn.to/30ljXGq), [3.2](https://amzn.to/2FT0EuC), [2.0](https://amzn.to/2QXUMXj)
|
||||
* Particle boards: [Argon](https://amzn.to/2FQHa9X), [Boron](https://amzn.to/36WgLUd), [Electron](https://amzn.to/30vEc4k), [Photon](https://amzn.to/387F9Cd)...
|
||||
* Texas Instruments boards: [MSP430](https://amzn.to/30nJWgg)...
|
||||
* Soft cores: [Nios II](https://en.wikipedia.org/wiki/Nios_II)...
|
||||
* Tested on all major development environments
|
||||
* [Arduino IDE](https://www.arduino.cc/en/Main/Software)
|
||||
* [Atmel Studio](http://www.atmel.com/microsite/atmel-studio/)
|
||||
* [Atollic TrueSTUDIO](https://atollic.com/truestudio/)
|
||||
* [Energia](http://energia.nu/)
|
||||
* [IAR Embedded Workbench](https://www.iar.com/iar-embedded-workbench/)
|
||||
* [Keil uVision](http://www.keil.com/)
|
||||
* [MPLAB X IDE](http://www.microchip.com/mplab/mplab-x-ide)
|
||||
* [Particle](https://www.particle.io/)
|
||||
* [PlatformIO](http://platformio.org/)
|
||||
* [Sloeber plugin for Eclipse](https://eclipse.baeyens.it/)
|
||||
* [Visual Micro](http://www.visualmicro.com/)
|
||||
* [Visual Studio](https://www.visualstudio.com/)
|
||||
* [Even works with online compilers like wandbox.org](https://wandbox.org/permlink/RlZSKy17DjJ6HcdN)
|
||||
* [CMake friendly](https://arduinojson.org/v6/how-to/use-arduinojson-with-cmake/?utm_source=github&utm_medium=readme)
|
||||
* Well designed
|
||||
* [Elegant API](http://arduinojson.org/v6/example/?utm_source=github&utm_medium=readme)
|
||||
* [Thread-safe](https://en.wikipedia.org/wiki/Thread_safety)
|
||||
* Self-contained (no external dependency)
|
||||
* `const` friendly
|
||||
* [`for` friendly](https://arduinojson.org/v6/api/jsonobject/begin_end/?utm_source=github&utm_medium=readme)
|
||||
* [TMP friendly](https://en.wikipedia.org/wiki/Template_metaprogramming)
|
||||
* Handles [integer overflows](https://arduinojson.org/v6/api/jsonvariant/as/?utm_source=github&utm_medium=readme#integer-overflows)
|
||||
* Well tested
|
||||
* [Unit test coverage close to 100%](https://coveralls.io/github/bblanchon/ArduinoJson?branch=6.x)
|
||||
* Continuously tested on
|
||||
* [Visual Studio 2010, 2012, 2013, 2015, 2017, 2019, 2022](https://ci.appveyor.com/project/bblanchon/arduinojson/branch/6.x)
|
||||
* [GCC 4.4, 4.6, 4.7, 4.8, 4.9, 5, 6, 7, 8, 9, 10, 11](https://github.com/bblanchon/ArduinoJson/actions?query=workflow%3A%22Continuous+Integration%22)
|
||||
* [Clang 3.5, 3.6, 3.7, 3.8, 3.9, 4.0, 5.0, 6.0, 7, 8, 9, 10](https://github.com/bblanchon/ArduinoJson/actions?query=workflow%3A%22Continuous+Integration%22)
|
||||
* [Continuously fuzzed with Google OSS Fuzz](https://bugs.chromium.org/p/oss-fuzz/issues/list?sort=-opened&can=1&q=proj:arduinojson)
|
||||
* Passes all default checks of [clang-tidy](https://releases.llvm.org/10.0.0/tools/clang/tools/extra/docs/clang-tidy/)
|
||||
* Well documented
|
||||
* [Tutorials](https://arduinojson.org/v6/doc/deserialization/?utm_source=github&utm_medium=readme)
|
||||
* [Examples](https://arduinojson.org/v6/example/?utm_source=github&utm_medium=readme)
|
||||
* [How-tos](https://arduinojson.org/v6/example/?utm_source=github&utm_medium=readme)
|
||||
* [FAQ](https://arduinojson.org/v6/faq/?utm_source=github&utm_medium=readme)
|
||||
* [Troubleshooter](https://arduinojson.org/v6/troubleshooter/?utm_source=github&utm_medium=readme)
|
||||
* [Book](https://arduinojson.org/book/?utm_source=github&utm_medium=readme)
|
||||
* [Changelog](CHANGELOG.md)
|
||||
* Vibrant user community
|
||||
* Most popular of all Arduino libraries on [GitHub](https://github.com/search?o=desc&q=arduino+library&s=stars&type=Repositories)
|
||||
* [Used in hundreds of projects](https://www.hackster.io/search?i=projects&q=arduinojson)
|
||||
* [Responsive support](https://github.com/bblanchon/ArduinoJson/issues?q=is%3Aissue+is%3Aclosed)
|
||||
* [Discord server](https://discord.gg/DzN6hHHD4h)
|
||||
|
||||
## Quickstart
|
||||
|
||||
### Deserialization
|
||||
|
||||
Here is a program that parses a JSON document with ArduinoJson.
|
||||
|
||||
```c++
|
||||
char json[] = "{\"sensor\":\"gps\",\"time\":1351824120,\"data\":[48.756080,2.302038]}";
|
||||
|
||||
DynamicJsonDocument doc(1024);
|
||||
deserializeJson(doc, json);
|
||||
|
||||
const char* sensor = doc["sensor"];
|
||||
long time = doc["time"];
|
||||
double latitude = doc["data"][0];
|
||||
double longitude = doc["data"][1];
|
||||
```
|
||||
|
||||
See the [tutorial on arduinojson.org](https://arduinojson.org/doc/decoding/?utm_source=github&utm_medium=readme)
|
||||
|
||||
### Serialization
|
||||
|
||||
Here is a program that generates a JSON document with ArduinoJson:
|
||||
|
||||
```c++
|
||||
DynamicJsonDocument doc(1024);
|
||||
|
||||
doc["sensor"] = "gps";
|
||||
doc["time"] = 1351824120;
|
||||
doc["data"][0] = 48.756080;
|
||||
doc["data"][1] = 2.302038;
|
||||
|
||||
serializeJson(doc, Serial);
|
||||
// This prints:
|
||||
// {"sensor":"gps","time":1351824120,"data":[48.756080,2.302038]}
|
||||
```
|
||||
|
||||
See the [tutorial on arduinojson.org](https://arduinojson.org/doc/encoding/?utm_source=github&utm_medium=readme)
|
||||
|
||||
## Sponsors
|
||||
|
||||
ArduinoJson is thankful to its sponsors. Please give them a visit; they deserve it!
|
||||
|
||||
<p>
|
||||
<a href="https://techexplorations.com/" rel="sponsored">
|
||||
<img alt="Tech Explorations" src="https://arduinojson.org/images/2021/10/techexplorations.png" width="200">
|
||||
</a>
|
||||
<a href="https://www.programmingelectronics.com/" rel="sponsored">
|
||||
<img src="https://arduinojson.org/images/2021/10/programmingeleactronicsacademy.png" alt="Programming Electronics Academy" width="200">
|
||||
</a>
|
||||
</p>
|
||||
<p>
|
||||
<a href="https://github.com/1technophile" rel="sponsored">
|
||||
<img alt="1technophile" src="https://avatars.githubusercontent.com/u/12672732?s=40&v=4">
|
||||
</a>
|
||||
</p>
|
||||
|
||||
If you run a commercial project that embeds ArduinoJson, think about [sponsoring the library's development](https://github.com/sponsors/bblanchon): it ensures the code that your products rely on stays actively maintained. It can also give your project some exposure to the makers' community.
|
||||
|
||||
If you are an individual user and want to support the development (or give a sign of appreciation), consider purchasing the book [Mastering ArduinoJson](https://arduinojson.org/book/) ❤, or simply [cast a star](https://github.com/bblanchon/ArduinoJson/stargazers) ⭐.
|
||||
@@ -0,0 +1,27 @@
|
||||
# ArduinoJson Support
|
||||
|
||||
First off, thank you very much for using ArduinoJson.
|
||||
|
||||
We'll be very happy to help you, but first please read the following.
|
||||
|
||||
## Before asking for help
|
||||
|
||||
1. Read the [FAQ](https://arduinojson.org/faq/?utm_source=github&utm_medium=support)
|
||||
2. Search in the [API Reference](https://arduinojson.org/api/?utm_source=github&utm_medium=support)
|
||||
|
||||
If you did not find the answer, please create a [new issue on GitHub](https://github.com/bblanchon/ArduinoJson/issues/new).
|
||||
|
||||
It is OK to add a comment to a currently opened issue, but please avoid adding comments to a closed issue.
|
||||
|
||||
## Before hitting the Submit button
|
||||
|
||||
Please provide all the relevant information:
|
||||
|
||||
* Good title
|
||||
* Short description of the problem
|
||||
* Target platform
|
||||
* Compiler model and version
|
||||
* [MVCE](https://stackoverflow.com/help/mcve)
|
||||
* Compiler output
|
||||
|
||||
Good questions get fast answers!
|
||||
@@ -0,0 +1,37 @@
|
||||
version: 6.19.4.{build}
|
||||
environment:
|
||||
matrix:
|
||||
- APPVEYOR_BUILD_WORKER_IMAGE: Visual Studio 2022
|
||||
CMAKE_GENERATOR: Visual Studio 17 2022
|
||||
- APPVEYOR_BUILD_WORKER_IMAGE: Visual Studio 2019
|
||||
CMAKE_GENERATOR: Visual Studio 16 2019
|
||||
- APPVEYOR_BUILD_WORKER_IMAGE: Visual Studio 2017
|
||||
CMAKE_GENERATOR: Visual Studio 15 2017
|
||||
- APPVEYOR_BUILD_WORKER_IMAGE: Visual Studio 2015
|
||||
CMAKE_GENERATOR: Visual Studio 14 2015
|
||||
- CMAKE_GENERATOR: Visual Studio 12 2013
|
||||
- CMAKE_GENERATOR: Visual Studio 11 2012
|
||||
- CMAKE_GENERATOR: Visual Studio 10 2010
|
||||
- CMAKE_GENERATOR: Ninja
|
||||
MINGW: MinGW # MinGW 32-bit 5.3.0
|
||||
- CMAKE_GENERATOR: Ninja
|
||||
MINGW32: i686-5.3.0-posix-dwarf-rt_v4-rev0 # MinGW-w64 5.3.0
|
||||
- CMAKE_GENERATOR: Ninja
|
||||
MINGW32: i686-6.3.0-posix-dwarf-rt_v5-rev1 # MinGW-w64 6.3.0 i686
|
||||
- CMAKE_GENERATOR: Ninja
|
||||
MINGW64: x86_64-6.3.0-posix-seh-rt_v5-rev1 # MinGW-w64 6.3.0 x86_64
|
||||
- CMAKE_GENERATOR: Ninja
|
||||
MINGW64: x86_64-7.3.0-posix-seh-rt_v5-rev0 # MinGW-w64 7.3.0 x86_64
|
||||
- CMAKE_GENERATOR: Ninja
|
||||
MINGW64: x86_64-8.1.0-posix-seh-rt_v6-rev0 # MinGW-w64 8.1.0 x86_64
|
||||
configuration: Debug
|
||||
before_build:
|
||||
- set PATH=%PATH:C:\Program Files\Git\usr\bin;=% # Workaround for CMake not wanting sh.exe on PATH for MinGW
|
||||
- if defined MINGW set PATH=C:\%MINGW%\bin;%PATH%
|
||||
- if defined MINGW32 set PATH=C:\mingw-w64\%MINGW32%\mingw32\bin;%PATH%
|
||||
- if defined MINGW64 set PATH=C:\mingw-w64\%MINGW64%\mingw64\bin;%PATH%
|
||||
- cmake -DCMAKE_BUILD_TYPE=%CONFIGURATION% -G "%CMAKE_GENERATOR%" .
|
||||
build_script:
|
||||
- cmake --build . --config %CONFIGURATION%
|
||||
test_script:
|
||||
- ctest -C %CONFIGURATION% --output-on-failure .
|
||||
@@ -0,0 +1,367 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
width="1001"
|
||||
height="250"
|
||||
viewBox="0 0 264.84791 66.145837"
|
||||
version="1.1"
|
||||
id="svg8"
|
||||
inkscape:version="0.92.2 (5c3e80d, 2017-08-06)"
|
||||
sodipodi:docname="logo.svg">
|
||||
<defs
|
||||
id="defs2">
|
||||
<clipPath
|
||||
id="clipPath831"
|
||||
clipPathUnits="userSpaceOnUse">
|
||||
<path
|
||||
inkscape:connector-curvature="0"
|
||||
id="path829"
|
||||
d="M 0,638 H 1000 V 0 H 0 Z" />
|
||||
</clipPath>
|
||||
<clipPath
|
||||
id="clipPath839"
|
||||
clipPathUnits="userSpaceOnUse">
|
||||
<path
|
||||
inkscape:connector-curvature="0"
|
||||
id="path837"
|
||||
d="M 0,638 H 1000 V 0 H 0 Z" />
|
||||
</clipPath>
|
||||
</defs>
|
||||
<sodipodi:namedview
|
||||
id="base"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1.0"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:zoom="0.7"
|
||||
inkscape:cx="399.91304"
|
||||
inkscape:cy="242.52487"
|
||||
inkscape:document-units="mm"
|
||||
inkscape:current-layer="g823"
|
||||
showgrid="false"
|
||||
fit-margin-top="5"
|
||||
fit-margin-bottom="5"
|
||||
fit-margin-right="5"
|
||||
fit-margin-left="5"
|
||||
inkscape:window-width="1920"
|
||||
inkscape:window-height="1017"
|
||||
inkscape:window-x="-8"
|
||||
inkscape:window-y="-8"
|
||||
inkscape:window-maximized="1"
|
||||
units="px" />
|
||||
<metadata
|
||||
id="metadata5">
|
||||
<rdf:RDF>
|
||||
<cc:Work
|
||||
rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||
<dc:title></dc:title>
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<g
|
||||
inkscape:label="Layer 1"
|
||||
inkscape:groupmode="layer"
|
||||
id="layer1"
|
||||
transform="translate(-1.2883269,-86.047553)">
|
||||
<g
|
||||
transform="matrix(0.35277777,0,0,-0.35277777,-137.80575,256.55859)"
|
||||
inkscape:label="logo (2)"
|
||||
id="g823">
|
||||
<g
|
||||
id="g825"
|
||||
transform="matrix(0.44331091,0,0,0.44331091,559.35527,248.127)">
|
||||
<g
|
||||
clip-path="url(#clipPath831)"
|
||||
id="g827">
|
||||
<g
|
||||
transform="translate(246.9092,120.1406)"
|
||||
id="g847">
|
||||
<path
|
||||
inkscape:connector-curvature="0"
|
||||
id="path849"
|
||||
style="fill:#00868e;fill-opacity:1;fill-rule:nonzero;stroke:none"
|
||||
d="M 0,0 20.015,58.057 H 39.896 L 59.909,0 H 44.138 L 41.22,10.34 H 18.82 L 15.772,0 Z m 22.135,21.605 h 15.771 l -5.83,19.219 c -0.265,0.53 -0.53,1.457 -0.797,2.787 -0.398,1.323 -0.795,3.043 -1.192,5.034 -0.398,-1.461 -0.662,-2.784 -1.062,-4.11 -0.397,-1.322 -0.662,-2.519 -1.059,-3.711 z" />
|
||||
</g>
|
||||
<g
|
||||
transform="translate(311.457,120.1406)"
|
||||
id="g851">
|
||||
<path
|
||||
inkscape:connector-curvature="0"
|
||||
id="path853"
|
||||
style="fill:#00868e;fill-opacity:1;fill-rule:nonzero;stroke:none"
|
||||
d="m 0,0 v 40.029 h 13.387 v -9.41 c 1.326,3.447 3.182,5.965 5.566,7.69 2.387,1.72 5.304,2.648 8.616,2.648 0.661,0 1.192,-0.133 1.723,-0.133 0.53,0 1.192,-0.135 1.722,-0.135 L 29.689,27.836 c -0.928,0.266 -1.723,0.533 -2.517,0.664 -0.795,0.133 -1.592,0.133 -2.386,0.133 -3.446,0 -6.098,-1.065 -7.953,-3.182 C 14.978,23.332 14.05,20.414 14.05,16.568 V 0 Z" />
|
||||
</g>
|
||||
<g
|
||||
transform="translate(390.7183,181.6387)"
|
||||
id="g855">
|
||||
<path
|
||||
inkscape:connector-curvature="0"
|
||||
id="path857"
|
||||
style="fill:#00868e;fill-opacity:1;fill-rule:nonzero;stroke:none"
|
||||
d="m 0,0 v -61.498 h -12.989 v 6.494 c -1.724,-2.648 -3.844,-4.769 -5.966,-6.096 -2.253,-1.322 -4.771,-1.984 -7.554,-1.984 -5.567,0 -10.073,1.984 -13.387,5.83 -3.314,3.977 -5.036,9.147 -5.036,15.772 0,6.359 1.722,11.531 5.036,15.375 3.314,3.976 7.556,5.962 12.856,5.962 3.446,0 6.229,-0.664 8.35,-1.853 2.121,-1.191 3.977,-3.314 5.701,-6.098 -0.265,0.928 -0.265,1.989 -0.397,3.182 -0.135,1.193 -0.135,2.519 -0.135,3.975 V 0 Z m -12.459,-41.482 c 0,3.05 -0.795,5.566 -2.519,7.287 -1.591,1.859 -3.845,2.787 -6.76,2.787 -2.783,0 -5.036,-0.928 -6.761,-2.787 -1.588,-1.721 -2.385,-4.237 -2.385,-7.287 0,-3.182 0.797,-5.698 2.385,-7.426 1.725,-1.852 3.978,-2.647 6.761,-2.647 2.915,0 5.169,0.795 6.76,2.647 1.724,1.859 2.519,4.244 2.519,7.426" />
|
||||
</g>
|
||||
<g
|
||||
transform="translate(442.2769,160.1699)"
|
||||
id="g859">
|
||||
<path
|
||||
inkscape:connector-curvature="0"
|
||||
id="path861"
|
||||
style="fill:#00868e;fill-opacity:1;fill-rule:nonzero;stroke:none"
|
||||
d="m 0,0 v -20.412 c 0,-4.506 -0.265,-7.953 -1.061,-10.072 -0.663,-2.254 -1.989,-4.11 -3.71,-5.83 -1.724,-1.727 -3.978,-3.051 -6.496,-3.979 -2.65,-0.793 -5.831,-1.322 -9.41,-1.322 -3.578,0 -6.626,0.529 -9.277,1.322 -2.651,0.928 -4.771,2.252 -6.628,3.979 -1.724,1.587 -3.048,3.576 -3.712,5.83 -0.663,2.119 -1.06,5.566 -1.06,10.072 V 0 h 13.519 v -21.336 c 0,-3.316 0.532,-5.836 1.591,-7.293 1.195,-1.457 2.916,-2.252 5.434,-2.252 2.518,0 4.375,0.795 5.435,2.252 1.06,1.457 1.59,3.842 1.59,7.293 V 0 Z" />
|
||||
</g>
|
||||
<path
|
||||
inkscape:connector-curvature="0"
|
||||
id="path863"
|
||||
style="fill:#00868e;fill-opacity:1;fill-rule:nonzero;stroke:none"
|
||||
d="m 452.88,160.17 h 14.05 v -40.029 h -14.05 z m -0.795,12.461 c 0,2.119 0.795,3.974 2.386,5.435 1.457,1.588 3.313,2.25 5.434,2.25 2.254,0 4.108,-0.662 5.567,-2.119 1.458,-1.592 2.254,-3.316 2.254,-5.566 0,-2.123 -0.796,-3.977 -2.254,-5.57 -1.592,-1.457 -3.447,-2.25 -5.567,-2.25 -2.121,0 -3.977,0.793 -5.434,2.25 -1.591,1.593 -2.386,3.447 -2.386,5.57" />
|
||||
<g
|
||||
transform="translate(477.7983,120.1406)"
|
||||
id="g865">
|
||||
<path
|
||||
inkscape:connector-curvature="0"
|
||||
id="path867"
|
||||
style="fill:#00868e;fill-opacity:1;fill-rule:nonzero;stroke:none"
|
||||
d="m 0,0 v 40.029 h 12.856 v -8.088 c 1.988,3.186 4.109,5.567 6.496,7.163 2.517,1.455 5.434,2.25 8.881,2.25 2.648,0 4.904,-0.53 6.891,-1.325 1.99,-0.795 3.712,-1.984 4.903,-3.58 1.059,-1.322 1.854,-2.912 2.256,-4.902 0.529,-1.986 0.662,-4.774 0.662,-8.613 V 0 h -14.05 v 21.473 c 0,3.181 -0.53,5.433 -1.59,6.896 -1.062,1.455 -2.651,2.25 -5.035,2.25 -2.918,0 -5.04,-1.058 -6.365,-3.316 C 14.58,25.184 13.918,21.605 13.918,16.834 V 0 Z" />
|
||||
</g>
|
||||
<g
|
||||
transform="translate(573.2275,140.0254)"
|
||||
id="g869">
|
||||
<path
|
||||
inkscape:connector-curvature="0"
|
||||
id="path871"
|
||||
style="fill:#00868e;fill-opacity:1;fill-rule:nonzero;stroke:none"
|
||||
d="m 0,0 c 0,-2.918 -0.529,-5.566 -1.721,-8.221 -1.059,-2.515 -2.653,-4.904 -4.905,-6.89 -2.118,-2.125 -4.507,-3.711 -7.287,-4.774 -2.653,-1.056 -5.439,-1.586 -8.484,-1.586 -3.05,0 -5.963,0.53 -8.617,1.586 -2.653,1.063 -5.037,2.649 -7.155,4.774 -2.124,1.986 -3.845,4.242 -4.904,6.89 -1.064,2.522 -1.594,5.303 -1.594,8.221 0,2.918 0.53,5.697 1.594,8.35 1.059,2.515 2.648,4.904 4.904,6.892 1.986,2.117 4.37,3.578 7.155,4.639 2.654,1.058 5.567,1.588 8.617,1.588 3.045,0 5.962,-0.53 8.612,-1.588 2.652,-1.061 5.041,-2.522 7.159,-4.639 2.252,-2.119 3.846,-4.377 4.905,-7.025 C -0.529,5.697 0,2.918 0,0 m -14.05,0 c 0,3.049 -0.795,5.299 -2.252,7.021 -1.457,1.727 -3.579,2.655 -6.095,2.655 -2.654,0 -4.639,-0.928 -6.096,-2.655 -1.461,-1.722 -2.256,-3.972 -2.256,-7.021 0,-2.918 0.795,-5.303 2.256,-7.027 1.457,-1.725 3.442,-2.518 6.096,-2.518 2.516,0 4.638,0.793 6.095,2.518 1.457,1.724 2.252,4.109 2.252,7.027" />
|
||||
</g>
|
||||
<g
|
||||
transform="translate(593.7715,178.1973)"
|
||||
id="g873">
|
||||
<path
|
||||
inkscape:connector-curvature="0"
|
||||
id="path875"
|
||||
style="fill:#e5ad23;fill-opacity:1;fill-rule:nonzero;stroke:none"
|
||||
d="m 0,0 h 15.776 v -37.643 c 0,-4.109 -0.264,-7.287 -0.799,-9.545 -0.53,-2.117 -1.457,-4.109 -2.649,-5.832 -1.589,-2.115 -3.58,-3.843 -5.964,-4.902 -2.387,-1.191 -4.903,-1.721 -7.821,-1.721 -3.712,0 -7.022,0.793 -9.808,2.383 -2.914,1.588 -5.301,3.846 -7.289,6.889 l 9.676,8.75 c 0.133,-1.723 0.796,-3.045 1.589,-3.977 0.928,-0.927 1.987,-1.326 3.316,-1.326 1.456,0 2.516,0.797 3.178,2.524 0.529,1.586 0.795,5.83 0.795,12.455 z" />
|
||||
</g>
|
||||
<g
|
||||
transform="translate(616.4375,123.457)"
|
||||
id="g877">
|
||||
<path
|
||||
inkscape:connector-curvature="0"
|
||||
id="path879"
|
||||
style="fill:#e5ad23;fill-opacity:1;fill-rule:nonzero;stroke:none"
|
||||
d="M 0,0 3.051,10.734 C 5.036,9.01 7.288,7.553 9.809,6.627 c 2.519,-0.928 4.903,-1.459 7.424,-1.459 1.854,0 3.178,0.268 4.105,0.928 0.933,0.531 1.329,1.326 1.329,2.384 0,1.86 -1.858,3.182 -5.571,3.979 -1.192,0.264 -2.251,0.529 -2.913,0.793 -4.242,1.191 -7.292,2.652 -9.279,4.639 -1.853,1.859 -2.786,4.244 -2.786,7.293 0,3.839 1.462,7.021 4.375,9.277 2.917,2.385 6.895,3.576 11.8,3.576 2.648,0 5.168,-0.396 7.816,-0.928 2.654,-0.66 5.434,-1.588 8.22,-2.785 l -2.918,-9.67 c -1.853,1.192 -3.841,2.25 -5.698,2.912 -1.986,0.664 -3.846,0.928 -5.831,0.928 -1.589,0 -2.786,-0.264 -3.713,-0.793 -0.794,-0.533 -1.192,-1.193 -1.192,-2.119 0,-1.463 1.855,-2.654 5.566,-3.582 0.928,-0.264 1.457,-0.396 1.859,-0.529 4.904,-1.455 8.348,-3.051 10.205,-4.903 1.987,-1.988 2.914,-4.507 2.914,-7.691 0,-4.104 -1.589,-7.42 -4.771,-9.939 -3.047,-2.518 -7.289,-3.84 -12.457,-3.84 -3.448,0 -6.763,0.396 -9.809,1.191 C 5.566,-2.918 2.653,-1.592 0,0" />
|
||||
</g>
|
||||
<g
|
||||
transform="translate(701.9268,140.0254)"
|
||||
id="g881">
|
||||
<path
|
||||
inkscape:connector-curvature="0"
|
||||
id="path883"
|
||||
style="fill:#e5ad23;fill-opacity:1;fill-rule:nonzero;stroke:none"
|
||||
d="m 0,0 c 0,-2.918 -0.53,-5.566 -1.723,-8.221 -1.057,-2.515 -2.651,-4.904 -4.77,-6.89 -2.252,-2.125 -4.64,-3.711 -7.288,-4.774 -2.785,-1.056 -5.571,-1.586 -8.616,-1.586 -3.051,0 -5.964,0.53 -8.618,1.586 -2.647,1.063 -5.036,2.649 -7.155,4.774 -2.123,1.986 -3.712,4.242 -4.903,6.89 -1.065,2.522 -1.594,5.303 -1.594,8.221 0,2.918 0.529,5.697 1.594,8.35 1.191,2.515 2.78,4.904 4.903,6.892 2.119,2.117 4.508,3.578 7.155,4.639 2.654,1.058 5.567,1.588 8.618,1.588 3.177,0 5.962,-0.53 8.616,-1.588 C -11,18.82 -8.612,17.359 -6.493,15.242 -4.374,13.123 -2.78,10.865 -1.589,8.217 -0.53,5.697 0,2.918 0,0 m -14.05,0 c 0,3.049 -0.663,5.299 -2.252,7.021 -1.457,1.727 -3.447,2.655 -6.095,2.655 -2.521,0 -4.64,-0.928 -6.097,-2.655 -1.461,-1.722 -2.255,-3.972 -2.255,-7.021 0,-2.918 0.794,-5.303 2.255,-7.027 1.457,-1.725 3.576,-2.518 6.097,-2.518 2.648,0 4.638,0.793 6.095,2.518 1.589,1.724 2.252,4.109 2.252,7.027" />
|
||||
</g>
|
||||
<g
|
||||
transform="translate(710.0127,120.1406)"
|
||||
id="g885">
|
||||
<path
|
||||
inkscape:connector-curvature="0"
|
||||
id="path887"
|
||||
style="fill:#e5ad23;fill-opacity:1;fill-rule:nonzero;stroke:none"
|
||||
d="m 0,0 v 40.029 h 12.992 v -8.088 c 1.854,3.186 3.973,5.567 6.492,7.163 2.516,1.455 5.434,2.25 8.882,2.25 2.516,0 4.904,-0.53 6.89,-1.325 1.986,-0.795 3.58,-1.984 4.904,-3.58 1.06,-1.322 1.854,-2.912 2.257,-4.902 0.396,-1.986 0.661,-4.774 0.661,-8.613 V 0 h -14.05 v 21.473 c 0,3.181 -0.529,5.433 -1.589,6.896 -1.064,1.455 -2.786,2.25 -5.042,2.25 -2.913,0 -5.036,-1.058 -6.492,-3.316 -1.324,-2.119 -1.987,-5.698 -1.987,-10.469 L 13.918,0 Z" />
|
||||
</g>
|
||||
<g
|
||||
transform="translate(703.3916,417.1084)"
|
||||
id="g889">
|
||||
<path
|
||||
inkscape:connector-curvature="0"
|
||||
id="path891"
|
||||
style="fill:#e5ad23;fill-opacity:1;fill-rule:nonzero;stroke:none"
|
||||
d="m 0,0 -8.81,-3.271 c 5.363,-15.116 8.291,-31.384 8.291,-48.338 0,-4.616 -0.226,-9.177 -0.648,-13.683 l 9.375,-0.627 c 0.434,4.712 0.667,9.484 0.667,14.31 C 8.875,-33.505 5.739,-16.134 0,0" />
|
||||
</g>
|
||||
<g
|
||||
transform="translate(558.3203,220.9473)"
|
||||
id="g893">
|
||||
<path
|
||||
inkscape:connector-curvature="0"
|
||||
id="path895"
|
||||
style="fill:#e5ad23;fill-opacity:1;fill-rule:nonzero;stroke:none"
|
||||
d="m 0,0 c -15.705,0 -30.819,2.516 -44.978,7.146 -3.751,-1.978 -7.583,-3.82 -11.51,-5.49 -0.144,-0.06 -0.29,-0.117 -0.434,-0.177 C -39.313,-5.533 -20.108,-9.395 0,-9.395 c 48.132,0 91.098,22.094 119.328,56.688 l -8.571,4.381 C 84.24,20.088 44.469,0 0,0" />
|
||||
</g>
|
||||
<g
|
||||
transform="translate(441.6821,219.5352)"
|
||||
id="g897">
|
||||
<path
|
||||
inkscape:connector-curvature="0"
|
||||
id="path899"
|
||||
style="fill:#e47128;fill-opacity:1;fill-rule:nonzero;stroke:none"
|
||||
d="M 0,0 C -21.463,0 -41.833,4.674 -60.154,13.053 L -64.849,5.168 C -45.139,-4.002 -23.17,-9.131 0,-9.131 c 21.177,0 41.352,4.283 59.716,12.022 C 55.719,4.482 51.807,6.24 47.983,8.148 32.965,2.875 16.819,0 0,0" />
|
||||
</g>
|
||||
<g
|
||||
transform="translate(552.4727,271.1309)"
|
||||
id="g901">
|
||||
<path
|
||||
inkscape:connector-curvature="0"
|
||||
id="path903"
|
||||
style="fill:#e5ad23;fill-opacity:1;fill-rule:nonzero;stroke:none"
|
||||
d="M 0,0 C 0.009,0 0.017,-0.004 0.023,-0.004 0.017,-0.004 0.009,0 0,0" />
|
||||
</g>
|
||||
<g
|
||||
transform="translate(554.5205,271.0293)"
|
||||
id="g905">
|
||||
<path
|
||||
inkscape:connector-curvature="0"
|
||||
id="path907"
|
||||
style="fill:#e5ad23;fill-opacity:1;fill-rule:nonzero;stroke:none"
|
||||
d="M 0,0 C 1.26,-0.057 2.524,-0.098 3.8,-0.098 5.079,-0.098 6.349,-0.057 7.614,0 6.347,-0.051 5.076,-0.096 3.796,-0.096 2.523,-0.096 1.262,-0.051 0,0" />
|
||||
</g>
|
||||
<g
|
||||
transform="translate(554.3145,459.9658)"
|
||||
id="g909">
|
||||
<path
|
||||
inkscape:connector-curvature="0"
|
||||
id="path911"
|
||||
style="fill:#e5ad23;fill-opacity:1;fill-rule:nonzero;stroke:none"
|
||||
d="M 0,0 C 1.311,0.054 2.624,0.099 3.947,0.1 2.624,0.099 1.31,0.062 0,0" />
|
||||
</g>
|
||||
<g
|
||||
transform="translate(550.6484,459.731)"
|
||||
id="g913">
|
||||
<path
|
||||
inkscape:connector-curvature="0"
|
||||
id="path915"
|
||||
style="fill:#e5ad23;fill-opacity:1;fill-rule:nonzero;stroke:none"
|
||||
d="M 0,0 V 0 C 0.003,0 0.004,0 0.006,0 0.004,0 0.003,0 0,0" />
|
||||
</g>
|
||||
<g
|
||||
transform="translate(564.1133,271.125)"
|
||||
id="g917">
|
||||
<path
|
||||
inkscape:connector-curvature="0"
|
||||
id="path919"
|
||||
style="fill:#e5ad23;fill-opacity:1;fill-rule:nonzero;stroke:none"
|
||||
d="M 0,0 C 0.021,0.002 0.04,0.002 0.061,0.006 H 0.062 C 0.041,0.006 0.021,0.002 0,0" />
|
||||
</g>
|
||||
<g
|
||||
transform="translate(562.3379,459.9663)"
|
||||
id="g921">
|
||||
<path
|
||||
inkscape:connector-curvature="0"
|
||||
id="path923"
|
||||
style="fill:#e5ad23;fill-opacity:1;fill-rule:nonzero;stroke:none"
|
||||
d="M 0,0 C -1.334,0.056 -2.67,0.101 -4.018,0.101 H -4.02 c 1.349,0 2.686,-0.046 4.021,-0.102 C 0,0 0,0 0,0" />
|
||||
</g>
|
||||
<g
|
||||
transform="translate(595.6299,364.3545)"
|
||||
id="g925">
|
||||
<path
|
||||
inkscape:connector-curvature="0"
|
||||
id="path927"
|
||||
style="fill:#e5ad23;fill-opacity:1;fill-rule:nonzero;stroke:none"
|
||||
d="m 0,0 c 0,33.746 -10.866,64.951 -29.28,90.321 -2.649,0.236 -5.324,0.377 -8.033,0.377 -1.23,0 -2.45,-0.044 -3.667,-0.093 C -21.057,65.797 -9.131,34.293 -9.131,0 c 0,-33.273 -11.235,-63.909 -30.102,-88.358 0.64,-0.014 1.276,-0.049 1.92,-0.049 3.298,0 6.55,0.191 9.755,0.539 C -10.197,-62.95 0,-32.672 0,0" />
|
||||
</g>
|
||||
<g
|
||||
transform="translate(672.6201,365.4995)"
|
||||
id="g929">
|
||||
<path
|
||||
inkscape:connector-curvature="0"
|
||||
id="path931"
|
||||
style="fill:#e5ad23;fill-opacity:1;fill-rule:nonzero;stroke:none"
|
||||
d="m 0,0 c 0,63.125 -51.175,114.299 -114.3,114.299 -4.692,0 -9.316,-0.289 -13.861,-0.839 -3.962,-0.48 -7.86,-1.156 -11.687,-2.03 3.089,-2.502 6.077,-5.122 8.947,-7.866 3.831,0.61 7.731,1.013 11.688,1.195 1.629,0.076 3.266,0.119 4.913,0.119 57.922,0 104.878,-46.956 104.878,-104.878 0,-3.787 -0.206,-7.525 -0.597,-11.208 l 9.292,-1.675 C -0.253,-8.653 0,-4.357 0,0" />
|
||||
</g>
|
||||
<g
|
||||
transform="translate(383.3794,235.6973)"
|
||||
id="g933">
|
||||
<path
|
||||
inkscape:connector-curvature="0"
|
||||
id="path935"
|
||||
style="fill:#62aeb2;fill-opacity:1;fill-rule:nonzero;stroke:none"
|
||||
d="m 0,0 2.881,4.838 c -47.274,21.193 -80.206,68.66 -80.206,123.819 0,8.489 0.321,14.111 1.814,22.167 l -18.228,2.079 c -1.249,-7.899 -1.907,-15.995 -1.907,-24.246 0,-61.852 36.484,-115.178 89.1,-139.651 l 4.695,7.885 z" />
|
||||
</g>
|
||||
<g
|
||||
transform="translate(475.9927,495.5977)"
|
||||
id="g937">
|
||||
<path
|
||||
inkscape:connector-curvature="0"
|
||||
id="path939"
|
||||
style="fill:#62aeb2;fill-opacity:1;fill-rule:nonzero;stroke:none"
|
||||
d="m 0,0 c 4.099,-1.069 8.124,-2.323 12.062,-3.756 2.162,1.206 4.357,2.358 6.585,3.453 1.45,0.713 2.909,1.406 4.384,2.071 3.797,1.711 7.685,3.255 11.648,4.638 14.921,5.208 30.952,8.047 47.649,8.047 50.216,0 94.445,-25.61 120.356,-64.478 l 7.703,5.385 C 182.777,-3.349 135.73,23.848 82.328,23.848 61.15,23.848 40.972,19.57 22.608,11.833 18.644,10.163 14.768,8.325 10.98,6.34 9.503,5.566 8.039,4.77 6.59,3.949 -6.357,7.861 -20.087,9.974 -34.311,9.974 c -3.094,0 -6.163,-0.111 -9.207,-0.307 l -0.323,-5.623 c 3.149,0.219 6.326,0.339 9.53,0.339 C -22.457,4.383 -10.959,2.857 0,0" />
|
||||
</g>
|
||||
<g
|
||||
transform="translate(342.8081,347.3779)"
|
||||
id="g941">
|
||||
<path
|
||||
inkscape:connector-curvature="0"
|
||||
id="path943"
|
||||
style="fill:#62aeb2;fill-opacity:1;fill-rule:nonzero;stroke:none"
|
||||
d="m 0,0 4.169,2.559 c -0.711,4.703 -1.083,9.516 -1.083,14.418 0,51.248 40.26,93.047 90.872,95.611 1.237,1.59 2.509,3.154 3.806,4.693 1.986,2.356 4.035,4.656 6.155,6.887 -1.673,0.08 -3.352,0.136 -5.045,0.136 -59.276,0 -107.328,-48.053 -107.328,-107.327 0,-57.863 45.79,-105.023 103.111,-107.238 l -5.896,7.424 C 43.744,-78.327 7.513,-44.065 0,0" />
|
||||
</g>
|
||||
<g
|
||||
transform="translate(454.5591,264.8711)"
|
||||
id="g945">
|
||||
<path
|
||||
inkscape:connector-curvature="0"
|
||||
id="path947"
|
||||
style="fill:#62aeb2;fill-opacity:1;fill-rule:nonzero;stroke:none"
|
||||
d="m 0,0 c -1.312,1.354 -2.598,2.729 -3.857,4.131 -5.416,6.037 -10.324,12.535 -14.674,19.422 l -10.18,-1.379 c 3.833,-6.455 8.123,-12.606 12.825,-18.408 1.262,-1.559 2.553,-3.09 3.872,-4.596 2.034,-2.32 4.137,-4.578 6.305,-6.772 2.299,-2.324 4.68,-4.566 7.121,-6.742 l 10.99,2.959 C 10.322,-9.689 8.29,-7.939 6.311,-6.131 4.144,-4.154 2.042,-2.105 0,0" />
|
||||
</g>
|
||||
<g
|
||||
transform="translate(420.0469,407.7637)"
|
||||
id="g949">
|
||||
<path
|
||||
inkscape:connector-curvature="0"
|
||||
id="path951"
|
||||
style="fill:#62aeb2;fill-opacity:1;fill-rule:nonzero;stroke:none"
|
||||
d="m 0,0 -9.694,0.332 c -3.889,-13.529 -5.979,-27.818 -5.979,-42.596 0,-20.788 4.126,-40.609 11.596,-58.699 H 6.143 C -1.84,-83.025 -6.279,-63.164 -6.279,-42.264 -6.279,-27.56 -4.082,-13.369 0,0" />
|
||||
</g>
|
||||
<g
|
||||
transform="translate(458.4263,365.4995)"
|
||||
id="g953">
|
||||
<path
|
||||
inkscape:connector-curvature="0"
|
||||
id="path955"
|
||||
style="fill:#62aeb2;fill-opacity:1;fill-rule:nonzero;stroke:none"
|
||||
d="m 0,0 c 0,-17.657 4.593,-34.231 12.636,-48.619 l 4.824,2.261 c -0.085,0.15 -0.164,0.305 -0.247,0.456 -0.309,0.553 -0.611,1.11 -0.909,1.671 -0.37,0.698 -0.728,1.404 -1.082,2.114 -0.377,0.757 -0.743,1.519 -1.101,2.286 -0.322,0.692 -0.638,1.387 -0.944,2.088 -0.239,0.55 -0.476,1.1 -0.706,1.654 -0.434,1.049 -0.854,2.106 -1.251,3.173 -0.178,0.479 -0.342,0.965 -0.512,1.449 -0.281,0.795 -0.552,1.595 -0.813,2.401 -0.16,0.498 -0.323,0.994 -0.476,1.496 -0.35,1.149 -0.68,2.307 -0.986,3.474 -0.119,0.448 -0.225,0.9 -0.337,1.351 -0.232,0.939 -0.45,1.884 -0.654,2.834 -0.087,0.408 -0.181,0.812 -0.263,1.222 -0.251,1.249 -0.473,2.509 -0.674,3.776 -0.067,0.418 -0.126,0.838 -0.186,1.258 -0.161,1.109 -0.303,2.223 -0.425,3.343 -0.031,0.282 -0.069,0.562 -0.097,0.844 -0.133,1.337 -0.23,2.684 -0.307,4.037 -0.023,0.4 -0.041,0.801 -0.059,1.202 -0.058,1.316 -0.097,2.637 -0.1,3.965 0,0.088 -0.007,0.175 -0.007,0.264 0,0.173 0.012,0.344 0.013,0.517 0.007,1.306 0.039,2.606 0.099,3.898 0.025,0.564 0.069,1.123 0.105,1.685 0.058,0.914 0.124,1.826 0.208,2.733 0.058,0.626 0.125,1.249 0.195,1.872 0.094,0.837 0.202,1.671 0.318,2.502 0.089,0.638 0.178,1.276 0.28,1.91 0.132,0.823 0.284,1.64 0.438,2.456 0.116,0.616 0.222,1.236 0.349,1.848 0.271,1.292 0.566,2.576 0.888,3.849 0.192,0.756 0.41,1.501 0.62,2.251 0.166,0.593 0.328,1.188 0.505,1.777 0.243,0.808 0.505,1.608 0.77,2.407 0.164,0.498 0.33,0.995 0.503,1.489 0.297,0.848 0.604,1.692 0.924,2.531 0.146,0.38 0.298,0.759 0.449,1.138 0.369,0.93 0.743,1.856 1.14,2.771 0.007,0.016 0.015,0.032 0.022,0.048 1.999,4.596 4.359,8.995 7.034,13.177 0.004,0.009 0.01,0.017 0.015,0.025 1.34,2.093 2.745,4.138 4.24,6.115 l -5.094,2.048 C 7.191,42.509 0,22.1 0,0" />
|
||||
</g>
|
||||
<g
|
||||
transform="translate(581.1611,243.5664)"
|
||||
id="g957">
|
||||
<path
|
||||
inkscape:connector-curvature="0"
|
||||
id="path959"
|
||||
style="fill:#00868e;fill-opacity:1;fill-rule:nonzero;stroke:none"
|
||||
d="m 0,0 -2.615,9.418 -2.535,9.131 c 45.885,7.797 81.619,45.39 86.59,92.177 0.391,3.682 0.597,7.42 0.597,11.207 0,57.922 -46.956,104.878 -104.878,104.878 -1.647,0 -3.284,-0.043 -4.913,-0.119 -3.957,-0.182 -7.857,-0.585 -11.688,-1.194 -1.485,-0.237 -2.96,-0.499 -4.422,-0.797 -2.233,-0.454 -4.436,-0.984 -6.613,-1.578 -2.298,-0.625 -4.565,-1.328 -6.796,-2.103 -3.646,-1.267 -7.203,-2.727 -10.653,-4.372 -4.115,-1.962 -8.082,-4.187 -11.881,-6.648 -2.119,-1.375 -4.185,-2.822 -6.195,-4.34 -2.869,1.81 -5.835,3.481 -8.89,4.998 2.008,1.632 4.07,3.201 6.187,4.695 3.793,2.68 7.758,5.133 11.873,7.343 3.445,1.849 6.998,3.525 10.644,5.02 2.225,0.913 4.484,1.764 6.778,2.54 2.179,0.735 4.39,1.399 6.626,2.004 1.455,0.394 2.919,0.766 4.395,1.103 3.826,0.874 7.725,1.551 11.687,2.031 4.545,0.549 9.169,0.838 13.861,0.838 63.125,0 114.3,-51.173 114.3,-114.299 0,-4.357 -0.253,-8.653 -0.727,-12.883 l 9.607,-1.732 C 93.994,53.27 52.901,9.85 0,0 m -22.841,27.365 c -1.275,0 -2.54,0.041 -3.8,0.098 -0.676,0.025 -1.35,0.058 -2.024,0.098 -0.007,0 -0.015,0.003 -0.023,0.003 -19.058,1.165 -36.591,7.956 -50.95,18.756 -1.006,0.758 -1.994,1.537 -2.969,2.332 -0.391,0.323 -0.775,0.651 -1.161,0.975 -0.656,0.555 -1.306,1.115 -1.947,1.686 -0.425,0.38 -0.849,0.761 -1.269,1.148 -0.604,0.56 -1.197,1.131 -1.786,1.705 -0.4,0.389 -0.807,0.772 -1.2,1.168 -0.863,0.869 -1.708,1.756 -2.536,2.66 -0.474,0.516 -0.932,1.047 -1.394,1.574 -0.414,0.471 -0.827,0.942 -1.231,1.418 -0.487,0.58 -0.966,1.162 -1.439,1.752 -0.354,0.442 -0.701,0.885 -1.048,1.332 -0.48,0.621 -0.96,1.241 -1.424,1.869 -0.029,0.041 -0.06,0.079 -0.089,0.118 l -4.694,-2.575 -4.419,-2.419 c -12.258,17.162 -19.475,38.172 -19.475,60.87 0,25.55 9.142,48.961 24.325,67.153 l 4.675,-2.22 4.99,-2.369 c 7.854,8.892 17.353,16.287 28.026,21.725 0.266,0.137 0.532,0.277 0.803,0.411 0.77,0.384 1.547,0.755 2.33,1.118 0.419,0.194 0.841,0.382 1.263,0.571 9.537,4.253 19.904,6.968 30.794,7.843 0.003,0 0.004,0 0.006,0 1.216,0.097 2.434,0.183 3.66,0.234 1.31,0.062 2.624,0.099 3.948,0.101 0.018,0 0.036,0 0.054,0 h 0.002 0.002 c 1.348,0 2.684,-0.044 4.018,-0.1 0.008,-0.001 0.015,-0.001 0.024,-0.002 1.471,-0.062 2.93,-0.163 4.382,-0.292 0.116,-0.009 0.234,-0.015 0.352,-0.026 1.395,-0.128 2.778,-0.294 4.156,-0.483 0.171,-0.023 0.344,-0.041 0.514,-0.065 1.33,-0.189 2.645,-0.414 3.956,-0.657 0.213,-0.041 0.431,-0.075 0.646,-0.116 1.267,-0.245 2.521,-0.522 3.769,-0.817 0.252,-0.06 0.505,-0.113 0.756,-0.175 1.208,-0.295 2.404,-0.621 3.593,-0.963 0.283,-0.082 0.571,-0.158 0.853,-0.243 1.152,-0.343 2.293,-0.713 3.426,-1.099 0.31,-0.106 0.623,-0.207 0.933,-0.316 1.1,-0.387 2.186,-0.798 3.267,-1.225 0.332,-0.13 0.666,-0.257 0.997,-0.392 1.047,-0.427 2.082,-0.877 3.11,-1.339 0.354,-0.159 0.708,-0.315 1.059,-0.479 0.995,-0.462 1.978,-0.945 2.954,-1.441 0.371,-0.189 0.742,-0.376 1.109,-0.569 0.945,-0.495 1.876,-1.009 2.801,-1.535 0.387,-0.219 0.771,-0.439 1.154,-0.664 0.893,-0.524 1.775,-1.065 2.648,-1.617 0.399,-0.253 0.796,-0.506 1.192,-0.764 0.841,-0.55 1.672,-1.114 2.493,-1.69 0.411,-0.286 0.819,-0.577 1.224,-0.869 0.792,-0.573 1.573,-1.158 2.346,-1.754 0.417,-0.323 0.831,-0.648 1.242,-0.977 0.742,-0.593 1.473,-1.194 2.196,-1.808 0.424,-0.36 0.843,-0.725 1.261,-1.092 0.691,-0.608 1.375,-1.224 2.048,-1.852 0.426,-0.397 0.846,-0.802 1.267,-1.207 0.642,-0.622 1.279,-1.25 1.904,-1.89 0.426,-0.437 0.845,-0.881 1.263,-1.326 0.595,-0.633 1.184,-1.27 1.761,-1.918 0.425,-0.478 0.84,-0.964 1.255,-1.449 0.547,-0.641 1.091,-1.284 1.621,-1.939 0.419,-0.518 0.826,-1.045 1.235,-1.572 0.501,-0.647 1,-1.295 1.485,-1.954 0.41,-0.559 0.806,-1.128 1.205,-1.696 0.455,-0.649 0.91,-1.3 1.35,-1.962 0.4,-0.601 0.783,-1.212 1.168,-1.822 0.412,-0.652 0.825,-1.301 1.22,-1.963 0.384,-0.643 0.752,-1.297 1.121,-1.95 0.368,-0.65 0.738,-1.297 1.092,-1.956 0.367,-0.687 0.715,-1.386 1.065,-2.083 0.326,-0.644 0.655,-1.286 0.965,-1.939 0.348,-0.731 0.673,-1.474 1.002,-2.216 0.283,-0.637 0.573,-1.272 0.842,-1.917 0.324,-0.778 0.624,-1.567 0.929,-2.354 0.242,-0.627 0.493,-1.25 0.722,-1.883 0.299,-0.828 0.571,-1.668 0.848,-2.506 0.201,-0.61 0.413,-1.215 0.602,-1.83 0.272,-0.884 0.513,-1.781 0.759,-2.676 0.162,-0.585 0.335,-1.164 0.486,-1.753 0.242,-0.952 0.451,-1.916 0.665,-2.879 0.12,-0.546 0.256,-1.087 0.367,-1.638 0.211,-1.038 0.384,-2.089 0.56,-3.138 0.082,-0.488 0.18,-0.97 0.255,-1.461 0.178,-1.174 0.316,-2.361 0.451,-3.549 0.042,-0.376 0.1,-0.747 0.139,-1.124 0.148,-1.468 0.256,-2.947 0.337,-4.434 0.005,-0.103 0.018,-0.205 0.023,-0.307 0.082,-1.609 0.125,-3.227 0.125,-4.855 0,-1.619 -0.044,-3.227 -0.124,-4.826 -0.013,-0.244 -0.04,-0.484 -0.054,-0.728 -0.077,-1.336 -0.171,-2.667 -0.304,-3.987 -0.045,-0.459 -0.114,-0.911 -0.167,-1.368 -0.125,-1.096 -0.253,-2.193 -0.416,-3.278 -0.083,-0.549 -0.189,-1.089 -0.28,-1.634 -0.167,-0.983 -0.329,-1.966 -0.526,-2.937 -0.119,-0.594 -0.261,-1.179 -0.391,-1.769 -0.203,-0.911 -0.402,-1.823 -0.629,-2.724 -0.157,-0.62 -0.335,-1.231 -0.505,-1.847 -0.234,-0.855 -0.467,-1.711 -0.726,-2.557 -0.195,-0.639 -0.41,-1.267 -Line truncated
|
||||
</g>
|
||||
<g
|
||||
transform="translate(554.6494,454.96)"
|
||||
id="g961">
|
||||
<path
|
||||
inkscape:connector-curvature="0"
|
||||
id="path963"
|
||||
style="fill:#00868e;fill-opacity:1;fill-rule:nonzero;stroke:none"
|
||||
d="m 0,0 c -10.023,-0.405 -19.616,-2.459 -28.527,-5.896 21.724,-21.656 35.173,-51.611 35.173,-84.709 0,-13.873 -2.363,-27.191 -6.707,-39.58 l -12.005,2.938 c 4.153,11.435 6.425,23.771 6.425,36.642 0,31.303 -13.42,59.455 -34.8,79.071 -3.475,-1.972 -6.812,-4.159 -9.978,-6.562 20.346,-17.563 33.238,-43.523 33.238,-72.509 0,-28.309 -12.292,-53.734 -31.815,-71.269 14.298,-10.416 31.794,-16.689 50.743,-17.09 18.867,24.449 30.103,55.086 30.103,88.359 C 31.85,-56.312 19.924,-24.808 0,0" />
|
||||
</g>
|
||||
<g
|
||||
transform="translate(393.1943,500.7769)"
|
||||
id="g965">
|
||||
<path
|
||||
inkscape:connector-curvature="0"
|
||||
id="path967"
|
||||
style="fill:#e5ad23;fill-opacity:1;fill-rule:nonzero;stroke:none"
|
||||
d="M 0,0 -2.466,8.942 C -39.654,-4.079 -70.318,-30.977 -88.254,-65.542 l 9.666,-1.535 C -61.654,-36.083 -33.706,-11.967 0,0" />
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 32 KiB |
@@ -0,0 +1 @@
|
||||
COMPONENT_ADD_INCLUDEDIRS := src
|
||||
@@ -0,0 +1,160 @@
|
||||
// ArduinoJson - https://arduinojson.org
|
||||
// Copyright © 2014-2022, Benoit BLANCHON
|
||||
// MIT License
|
||||
//
|
||||
// This example shows how to store your project configuration in a file.
|
||||
// It uses the SD library but can be easily modified for any other file-system.
|
||||
//
|
||||
// The file contains a JSON document with the following content:
|
||||
// {
|
||||
// "hostname": "examples.com",
|
||||
// "port": 2731
|
||||
// }
|
||||
//
|
||||
// To run this program, you need an SD card connected to the SPI bus as follows:
|
||||
// * MOSI <-> pin 11
|
||||
// * MISO <-> pin 12
|
||||
// * CLK <-> pin 13
|
||||
// * CS <-> pin 4
|
||||
//
|
||||
// https://arduinojson.org/v6/example/config/
|
||||
|
||||
#include <ArduinoJson.h>
|
||||
#include <SD.h>
|
||||
#include <SPI.h>
|
||||
|
||||
// Our configuration structure.
|
||||
//
|
||||
// Never use a JsonDocument to store the configuration!
|
||||
// A JsonDocument is *not* a permanent storage; it's only a temporary storage
|
||||
// used during the serialization phase. See:
|
||||
// https://arduinojson.org/v6/faq/why-must-i-create-a-separate-config-object/
|
||||
struct Config {
|
||||
char hostname[64];
|
||||
int port;
|
||||
};
|
||||
|
||||
const char *filename = "/config.txt"; // <- SD library uses 8.3 filenames
|
||||
Config config; // <- global configuration object
|
||||
|
||||
// Loads the configuration from a file
|
||||
void loadConfiguration(const char *filename, Config &config) {
|
||||
// Open file for reading
|
||||
File file = SD.open(filename);
|
||||
|
||||
// Allocate a temporary JsonDocument
|
||||
// Don't forget to change the capacity to match your requirements.
|
||||
// Use https://arduinojson.org/v6/assistant to compute the capacity.
|
||||
StaticJsonDocument<512> doc;
|
||||
|
||||
// Deserialize the JSON document
|
||||
DeserializationError error = deserializeJson(doc, file);
|
||||
if (error)
|
||||
Serial.println(F("Failed to read file, using default configuration"));
|
||||
|
||||
// Copy values from the JsonDocument to the Config
|
||||
config.port = doc["port"] | 2731;
|
||||
strlcpy(config.hostname, // <- destination
|
||||
doc["hostname"] | "example.com", // <- source
|
||||
sizeof(config.hostname)); // <- destination's capacity
|
||||
|
||||
// Close the file (Curiously, File's destructor doesn't close the file)
|
||||
file.close();
|
||||
}
|
||||
|
||||
// Saves the configuration to a file
|
||||
void saveConfiguration(const char *filename, const Config &config) {
|
||||
// Delete existing file, otherwise the configuration is appended to the file
|
||||
SD.remove(filename);
|
||||
|
||||
// Open file for writing
|
||||
File file = SD.open(filename, FILE_WRITE);
|
||||
if (!file) {
|
||||
Serial.println(F("Failed to create file"));
|
||||
return;
|
||||
}
|
||||
|
||||
// Allocate a temporary JsonDocument
|
||||
// Don't forget to change the capacity to match your requirements.
|
||||
// Use https://arduinojson.org/assistant to compute the capacity.
|
||||
StaticJsonDocument<256> doc;
|
||||
|
||||
// Set the values in the document
|
||||
doc["hostname"] = config.hostname;
|
||||
doc["port"] = config.port;
|
||||
|
||||
// Serialize JSON to file
|
||||
if (serializeJson(doc, file) == 0) {
|
||||
Serial.println(F("Failed to write to file"));
|
||||
}
|
||||
|
||||
// Close the file
|
||||
file.close();
|
||||
}
|
||||
|
||||
// Prints the content of a file to the Serial
|
||||
void printFile(const char *filename) {
|
||||
// Open file for reading
|
||||
File file = SD.open(filename);
|
||||
if (!file) {
|
||||
Serial.println(F("Failed to read file"));
|
||||
return;
|
||||
}
|
||||
|
||||
// Extract each characters by one by one
|
||||
while (file.available()) {
|
||||
Serial.print((char)file.read());
|
||||
}
|
||||
Serial.println();
|
||||
|
||||
// Close the file
|
||||
file.close();
|
||||
}
|
||||
|
||||
void setup() {
|
||||
// Initialize serial port
|
||||
Serial.begin(9600);
|
||||
while (!Serial) continue;
|
||||
|
||||
// Initialize SD library
|
||||
const int chipSelect = 4;
|
||||
while (!SD.begin(chipSelect)) {
|
||||
Serial.println(F("Failed to initialize SD library"));
|
||||
delay(1000);
|
||||
}
|
||||
|
||||
// Should load default config if run for the first time
|
||||
Serial.println(F("Loading configuration..."));
|
||||
loadConfiguration(filename, config);
|
||||
|
||||
// Create configuration file
|
||||
Serial.println(F("Saving configuration..."));
|
||||
saveConfiguration(filename, config);
|
||||
|
||||
// Dump config file
|
||||
Serial.println(F("Print config file..."));
|
||||
printFile(filename);
|
||||
}
|
||||
|
||||
void loop() {
|
||||
// not used in this example
|
||||
}
|
||||
|
||||
// Performance issue?
|
||||
// ------------------
|
||||
//
|
||||
// File is an unbuffered stream, which is not optimal for ArduinoJson.
|
||||
// See: https://arduinojson.org/v6/how-to/improve-speed/
|
||||
|
||||
// See also
|
||||
// --------
|
||||
//
|
||||
// https://arduinojson.org/ contains the documentation for all the functions
|
||||
// used above. It also includes an FAQ that will help you solve any
|
||||
// serialization or deserialization problem.
|
||||
//
|
||||
// The book "Mastering ArduinoJson" contains a case study of a project that has
|
||||
// a complex configuration with nested members.
|
||||
// Contrary to this example, the project in the book uses the SPIFFS filesystem.
|
||||
// Learn more at https://arduinojson.org/book/
|
||||
// Use the coupon code TWENTY for a 20% discount ❤❤❤❤❤
|
||||
@@ -0,0 +1,63 @@
|
||||
// ArduinoJson - https://arduinojson.org
|
||||
// Copyright © 2014-2022, Benoit BLANCHON
|
||||
// MIT License
|
||||
//
|
||||
// This example shows how to use DeserializationOpion::Filter
|
||||
//
|
||||
// https://arduinojson.org/v6/example/filter/
|
||||
|
||||
#include <ArduinoJson.h>
|
||||
|
||||
void setup() {
|
||||
// Initialize serial port
|
||||
Serial.begin(9600);
|
||||
while (!Serial) continue;
|
||||
|
||||
// The huge input: an extract from OpenWeatherMap response
|
||||
const __FlashStringHelper* input_json = F(
|
||||
"{\"cod\":\"200\",\"message\":0,\"list\":[{\"dt\":1581498000,\"main\":{"
|
||||
"\"temp\":3.23,\"feels_like\":-3.63,\"temp_min\":3.23,\"temp_max\":4.62,"
|
||||
"\"pressure\":1014,\"sea_level\":1014,\"grnd_level\":1010,\"humidity\":"
|
||||
"58,\"temp_kf\":-1.39},\"weather\":[{\"id\":800,\"main\":\"Clear\","
|
||||
"\"description\":\"clear "
|
||||
"sky\",\"icon\":\"01d\"}],\"clouds\":{\"all\":0},\"wind\":{\"speed\":6."
|
||||
"19,\"deg\":266},\"sys\":{\"pod\":\"d\"},\"dt_txt\":\"2020-02-12 "
|
||||
"09:00:00\"},{\"dt\":1581508800,\"main\":{\"temp\":6.09,\"feels_like\":-"
|
||||
"1.07,\"temp_min\":6.09,\"temp_max\":7.13,\"pressure\":1015,\"sea_"
|
||||
"level\":1015,\"grnd_level\":1011,\"humidity\":48,\"temp_kf\":-1.04},"
|
||||
"\"weather\":[{\"id\":800,\"main\":\"Clear\",\"description\":\"clear "
|
||||
"sky\",\"icon\":\"01d\"}],\"clouds\":{\"all\":9},\"wind\":{\"speed\":6."
|
||||
"64,\"deg\":268},\"sys\":{\"pod\":\"d\"},\"dt_txt\":\"2020-02-12 "
|
||||
"12:00:00\"}],\"city\":{\"id\":2643743,\"name\":\"London\",\"coord\":{"
|
||||
"\"lat\":51.5085,\"lon\":-0.1257},\"country\":\"GB\",\"population\":"
|
||||
"1000000,\"timezone\":0,\"sunrise\":1581492085,\"sunset\":1581527294}}");
|
||||
|
||||
// The filter: it contains "true" for each value we want to keep
|
||||
StaticJsonDocument<200> filter;
|
||||
filter["list"][0]["dt"] = true;
|
||||
filter["list"][0]["main"]["temp"] = true;
|
||||
|
||||
// Deserialize the document
|
||||
StaticJsonDocument<400> doc;
|
||||
deserializeJson(doc, input_json, DeserializationOption::Filter(filter));
|
||||
|
||||
// Print the result
|
||||
serializeJsonPretty(doc, Serial);
|
||||
}
|
||||
|
||||
void loop() {
|
||||
// not used in this example
|
||||
}
|
||||
|
||||
// See also
|
||||
// --------
|
||||
//
|
||||
// https://arduinojson.org/ contains the documentation for all the functions
|
||||
// used above. It also includes an FAQ that will help you solve any
|
||||
// deserialization problem.
|
||||
//
|
||||
// The book "Mastering ArduinoJson" contains a tutorial on deserialization.
|
||||
// It begins with a simple example, like the one above, and then adds more
|
||||
// features like deserializing directly from a file or an HTTP request.
|
||||
// Learn more at https://arduinojson.org/book/
|
||||
// Use the coupon code TWENTY for a 20% discount ❤❤❤❤❤
|
||||
+77
@@ -0,0 +1,77 @@
|
||||
// ArduinoJson - https://arduinojson.org
|
||||
// Copyright © 2014-2022, Benoit BLANCHON
|
||||
// MIT License
|
||||
//
|
||||
// This example shows how to generate a JSON document with ArduinoJson.
|
||||
//
|
||||
// https://arduinojson.org/v6/example/generator/
|
||||
|
||||
#include <ArduinoJson.h>
|
||||
|
||||
void setup() {
|
||||
// Initialize Serial port
|
||||
Serial.begin(9600);
|
||||
while (!Serial) continue;
|
||||
|
||||
// Allocate the JSON document
|
||||
//
|
||||
// Inside the brackets, 200 is the RAM allocated to this document.
|
||||
// Don't forget to change this value to match your requirement.
|
||||
// Use https://arduinojson.org/v6/assistant to compute the capacity.
|
||||
StaticJsonDocument<200> doc;
|
||||
|
||||
// StaticJsonObject allocates memory on the stack, it can be
|
||||
// replaced by DynamicJsonDocument which allocates in the heap.
|
||||
//
|
||||
// DynamicJsonDocument doc(200);
|
||||
|
||||
// Add values in the document
|
||||
//
|
||||
doc["sensor"] = "gps";
|
||||
doc["time"] = 1351824120;
|
||||
|
||||
// Add an array.
|
||||
//
|
||||
JsonArray data = doc.createNestedArray("data");
|
||||
data.add(48.756080);
|
||||
data.add(2.302038);
|
||||
|
||||
// Generate the minified JSON and send it to the Serial port.
|
||||
//
|
||||
serializeJson(doc, Serial);
|
||||
// The above line prints:
|
||||
// {"sensor":"gps","time":1351824120,"data":[48.756080,2.302038]}
|
||||
|
||||
// Start a new line
|
||||
Serial.println();
|
||||
|
||||
// Generate the prettified JSON and send it to the Serial port.
|
||||
//
|
||||
serializeJsonPretty(doc, Serial);
|
||||
// The above line prints:
|
||||
// {
|
||||
// "sensor": "gps",
|
||||
// "time": 1351824120,
|
||||
// "data": [
|
||||
// 48.756080,
|
||||
// 2.302038
|
||||
// ]
|
||||
// }
|
||||
}
|
||||
|
||||
void loop() {
|
||||
// not used in this example
|
||||
}
|
||||
|
||||
// See also
|
||||
// --------
|
||||
//
|
||||
// https://arduinojson.org/ contains the documentation for all the functions
|
||||
// used above. It also includes an FAQ that will help you solve any
|
||||
// serialization problem.
|
||||
//
|
||||
// The book "Mastering ArduinoJson" contains a tutorial on serialization.
|
||||
// It begins with a simple example, like the one above, and then adds more
|
||||
// features like serializing directly to a file or an HTTP request.
|
||||
// Learn more at https://arduinojson.org/book/
|
||||
// Use the coupon code TWENTY for a 20% discount ❤❤❤❤❤
|
||||
@@ -0,0 +1,126 @@
|
||||
// ArduinoJson - https://arduinojson.org
|
||||
// Copyright © 2014-2022, Benoit BLANCHON
|
||||
// MIT License
|
||||
//
|
||||
// This example shows how to parse a JSON document in an HTTP response.
|
||||
// It uses the Ethernet library, but can be easily adapted for Wifi.
|
||||
//
|
||||
// It performs a GET resquest on https://arduinojson.org/example.json
|
||||
// Here is the expected response:
|
||||
// {
|
||||
// "sensor": "gps",
|
||||
// "time": 1351824120,
|
||||
// "data": [
|
||||
// 48.756080,
|
||||
// 2.302038
|
||||
// ]
|
||||
// }
|
||||
//
|
||||
// https://arduinojson.org/v6/example/http-client/
|
||||
|
||||
#include <ArduinoJson.h>
|
||||
#include <Ethernet.h>
|
||||
#include <SPI.h>
|
||||
|
||||
void setup() {
|
||||
// Initialize Serial port
|
||||
Serial.begin(9600);
|
||||
while (!Serial) continue;
|
||||
|
||||
// Initialize Ethernet library
|
||||
byte mac[] = {0xDE, 0xAD, 0xBE, 0xEF, 0xFE, 0xED};
|
||||
if (!Ethernet.begin(mac)) {
|
||||
Serial.println(F("Failed to configure Ethernet"));
|
||||
return;
|
||||
}
|
||||
delay(1000);
|
||||
|
||||
Serial.println(F("Connecting..."));
|
||||
|
||||
// Connect to HTTP server
|
||||
EthernetClient client;
|
||||
client.setTimeout(10000);
|
||||
if (!client.connect("arduinojson.org", 80)) {
|
||||
Serial.println(F("Connection failed"));
|
||||
return;
|
||||
}
|
||||
|
||||
Serial.println(F("Connected!"));
|
||||
|
||||
// Send HTTP request
|
||||
client.println(F("GET /example.json HTTP/1.0"));
|
||||
client.println(F("Host: arduinojson.org"));
|
||||
client.println(F("Connection: close"));
|
||||
if (client.println() == 0) {
|
||||
Serial.println(F("Failed to send request"));
|
||||
client.stop();
|
||||
return;
|
||||
}
|
||||
|
||||
// Check HTTP status
|
||||
char status[32] = {0};
|
||||
client.readBytesUntil('\r', status, sizeof(status));
|
||||
// It should be "HTTP/1.0 200 OK" or "HTTP/1.1 200 OK"
|
||||
if (strcmp(status + 9, "200 OK") != 0) {
|
||||
Serial.print(F("Unexpected response: "));
|
||||
Serial.println(status);
|
||||
client.stop();
|
||||
return;
|
||||
}
|
||||
|
||||
// Skip HTTP headers
|
||||
char endOfHeaders[] = "\r\n\r\n";
|
||||
if (!client.find(endOfHeaders)) {
|
||||
Serial.println(F("Invalid response"));
|
||||
client.stop();
|
||||
return;
|
||||
}
|
||||
|
||||
// Allocate the JSON document
|
||||
// Use https://arduinojson.org/v6/assistant to compute the capacity.
|
||||
const size_t capacity = JSON_OBJECT_SIZE(3) + JSON_ARRAY_SIZE(2) + 60;
|
||||
DynamicJsonDocument doc(capacity);
|
||||
|
||||
// Parse JSON object
|
||||
DeserializationError error = deserializeJson(doc, client);
|
||||
if (error) {
|
||||
Serial.print(F("deserializeJson() failed: "));
|
||||
Serial.println(error.f_str());
|
||||
client.stop();
|
||||
return;
|
||||
}
|
||||
|
||||
// Extract values
|
||||
Serial.println(F("Response:"));
|
||||
Serial.println(doc["sensor"].as<const char*>());
|
||||
Serial.println(doc["time"].as<long>());
|
||||
Serial.println(doc["data"][0].as<float>(), 6);
|
||||
Serial.println(doc["data"][1].as<float>(), 6);
|
||||
|
||||
// Disconnect
|
||||
client.stop();
|
||||
}
|
||||
|
||||
void loop() {
|
||||
// not used in this example
|
||||
}
|
||||
|
||||
// Performance issue?
|
||||
// ------------------
|
||||
//
|
||||
// EthernetClient is an unbuffered stream, which is not optimal for ArduinoJson.
|
||||
// See: https://arduinojson.org/v6/how-to/improve-speed/
|
||||
|
||||
// See also
|
||||
// --------
|
||||
//
|
||||
// https://arduinojson.org/ contains the documentation for all the functions
|
||||
// used above. It also includes an FAQ that will help you solve any
|
||||
// serialization problem.
|
||||
//
|
||||
// The book "Mastering ArduinoJson" contains a tutorial on deserialization
|
||||
// showing how to parse the response from GitHub's API. In the last chapter,
|
||||
// it shows how to parse the huge documents from OpenWeatherMap
|
||||
// and Reddit.
|
||||
// Learn more at https://arduinojson.org/book/
|
||||
// Use the coupon code TWENTY for a 20% discount ❤❤❤❤❤
|
||||
@@ -0,0 +1,80 @@
|
||||
// ArduinoJson - https://arduinojson.org
|
||||
// Copyright © 2014-2022, Benoit BLANCHON
|
||||
// MIT License
|
||||
//
|
||||
// This example shows how to deserialize a JSON document with ArduinoJson.
|
||||
//
|
||||
// https://arduinojson.org/v6/example/parser/
|
||||
|
||||
#include <ArduinoJson.h>
|
||||
|
||||
void setup() {
|
||||
// Initialize serial port
|
||||
Serial.begin(9600);
|
||||
while (!Serial) continue;
|
||||
|
||||
// Allocate the JSON document
|
||||
//
|
||||
// Inside the brackets, 200 is the capacity of the memory pool in bytes.
|
||||
// Don't forget to change this value to match your JSON document.
|
||||
// Use https://arduinojson.org/v6/assistant to compute the capacity.
|
||||
StaticJsonDocument<200> doc;
|
||||
|
||||
// StaticJsonDocument<N> allocates memory on the stack, it can be
|
||||
// replaced by DynamicJsonDocument which allocates in the heap.
|
||||
//
|
||||
// DynamicJsonDocument doc(200);
|
||||
|
||||
// JSON input string.
|
||||
//
|
||||
// Using a char[], as shown here, enables the "zero-copy" mode. This mode uses
|
||||
// the minimal amount of memory because the JsonDocument stores pointers to
|
||||
// the input buffer.
|
||||
// If you use another type of input, ArduinoJson must copy the strings from
|
||||
// the input to the JsonDocument, so you need to increase the capacity of the
|
||||
// JsonDocument.
|
||||
char json[] =
|
||||
"{\"sensor\":\"gps\",\"time\":1351824120,\"data\":[48.756080,2.302038]}";
|
||||
|
||||
// Deserialize the JSON document
|
||||
DeserializationError error = deserializeJson(doc, json);
|
||||
|
||||
// Test if parsing succeeds.
|
||||
if (error) {
|
||||
Serial.print(F("deserializeJson() failed: "));
|
||||
Serial.println(error.f_str());
|
||||
return;
|
||||
}
|
||||
|
||||
// Fetch values.
|
||||
//
|
||||
// Most of the time, you can rely on the implicit casts.
|
||||
// In other case, you can do doc["time"].as<long>();
|
||||
const char* sensor = doc["sensor"];
|
||||
long time = doc["time"];
|
||||
double latitude = doc["data"][0];
|
||||
double longitude = doc["data"][1];
|
||||
|
||||
// Print values.
|
||||
Serial.println(sensor);
|
||||
Serial.println(time);
|
||||
Serial.println(latitude, 6);
|
||||
Serial.println(longitude, 6);
|
||||
}
|
||||
|
||||
void loop() {
|
||||
// not used in this example
|
||||
}
|
||||
|
||||
// See also
|
||||
// --------
|
||||
//
|
||||
// https://arduinojson.org/ contains the documentation for all the functions
|
||||
// used above. It also includes an FAQ that will help you solve any
|
||||
// deserialization problem.
|
||||
//
|
||||
// The book "Mastering ArduinoJson" contains a tutorial on deserialization.
|
||||
// It begins with a simple example, like the one above, and then adds more
|
||||
// features like deserializing directly from a file or an HTTP request.
|
||||
// Learn more at https://arduinojson.org/book/
|
||||
// Use the coupon code TWENTY for a 20% discount ❤❤❤❤❤
|
||||
@@ -0,0 +1,117 @@
|
||||
// ArduinoJson - https://arduinojson.org
|
||||
// Copyright © 2014-2022, Benoit BLANCHON
|
||||
// MIT License
|
||||
//
|
||||
// This example shows how to implement an HTTP server that sends a JSON document
|
||||
// in the response.
|
||||
// It uses the Ethernet library but can be easily adapted for Wifi.
|
||||
//
|
||||
// The JSON document contains the values of the analog and digital pins.
|
||||
// It looks like that:
|
||||
// {
|
||||
// "analog": [0, 76, 123, 158, 192, 205],
|
||||
// "digital": [1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0]
|
||||
// }
|
||||
//
|
||||
// https://arduinojson.org/v6/example/http-server/
|
||||
|
||||
#include <ArduinoJson.h>
|
||||
#include <Ethernet.h>
|
||||
#include <SPI.h>
|
||||
|
||||
byte mac[] = {0xDE, 0xAD, 0xBE, 0xEF, 0xFE, 0xED};
|
||||
EthernetServer server(80);
|
||||
|
||||
void setup() {
|
||||
// Initialize serial port
|
||||
Serial.begin(9600);
|
||||
while (!Serial) continue;
|
||||
|
||||
// Initialize Ethernet libary
|
||||
if (!Ethernet.begin(mac)) {
|
||||
Serial.println(F("Failed to initialize Ethernet library"));
|
||||
return;
|
||||
}
|
||||
|
||||
// Start to listen
|
||||
server.begin();
|
||||
|
||||
Serial.println(F("Server is ready."));
|
||||
Serial.print(F("Please connect to http://"));
|
||||
Serial.println(Ethernet.localIP());
|
||||
}
|
||||
|
||||
void loop() {
|
||||
// Wait for an incomming connection
|
||||
EthernetClient client = server.available();
|
||||
|
||||
// Do we have a client?
|
||||
if (!client)
|
||||
return;
|
||||
|
||||
Serial.println(F("New client"));
|
||||
|
||||
// Read the request (we ignore the content in this example)
|
||||
while (client.available()) client.read();
|
||||
|
||||
// Allocate a temporary JsonDocument
|
||||
// Use https://arduinojson.org/v6/assistant to compute the capacity.
|
||||
StaticJsonDocument<500> doc;
|
||||
|
||||
// Create the "analog" array
|
||||
JsonArray analogValues = doc.createNestedArray("analog");
|
||||
for (int pin = 0; pin < 6; pin++) {
|
||||
// Read the analog input
|
||||
int value = analogRead(pin);
|
||||
|
||||
// Add the value at the end of the array
|
||||
analogValues.add(value);
|
||||
}
|
||||
|
||||
// Create the "digital" array
|
||||
JsonArray digitalValues = doc.createNestedArray("digital");
|
||||
for (int pin = 0; pin < 14; pin++) {
|
||||
// Read the digital input
|
||||
int value = digitalRead(pin);
|
||||
|
||||
// Add the value at the end of the array
|
||||
digitalValues.add(value);
|
||||
}
|
||||
|
||||
Serial.print(F("Sending: "));
|
||||
serializeJson(doc, Serial);
|
||||
Serial.println();
|
||||
|
||||
// Write response headers
|
||||
client.println(F("HTTP/1.0 200 OK"));
|
||||
client.println(F("Content-Type: application/json"));
|
||||
client.println(F("Connection: close"));
|
||||
client.print(F("Content-Length: "));
|
||||
client.println(measureJsonPretty(doc));
|
||||
client.println();
|
||||
|
||||
// Write JSON document
|
||||
serializeJsonPretty(doc, client);
|
||||
|
||||
// Disconnect
|
||||
client.stop();
|
||||
}
|
||||
|
||||
// Performance issue?
|
||||
// ------------------
|
||||
//
|
||||
// EthernetClient is an unbuffered stream, which is not optimal for ArduinoJson.
|
||||
// See: https://arduinojson.org/v6/how-to/improve-speed/
|
||||
|
||||
// See also
|
||||
// --------
|
||||
//
|
||||
// https://arduinojson.org/ contains the documentation for all the functions
|
||||
// used above. It also includes an FAQ that will help you solve any
|
||||
// serialization problem.
|
||||
//
|
||||
// The book "Mastering ArduinoJson" contains a tutorial on serialization.
|
||||
// It begins with a simple example, then adds more features like serializing
|
||||
// directly to a file or an HTTP client.
|
||||
// Learn more at https://arduinojson.org/book/
|
||||
// Use the coupon code TWENTY for a 20% discount ❤❤❤❤❤
|
||||
@@ -0,0 +1,106 @@
|
||||
// ArduinoJson - https://arduinojson.org
|
||||
// Copyright © 2014-2022, Benoit BLANCHON
|
||||
// MIT License
|
||||
//
|
||||
// This example shows how to send a JSON document to a UDP socket.
|
||||
// At regular interval, it sends a UDP packet that contains the status of
|
||||
// analog and digital pins.
|
||||
// It looks like that:
|
||||
// {
|
||||
// "analog": [0, 76, 123, 158, 192, 205],
|
||||
// "digital": [1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0]
|
||||
// }
|
||||
//
|
||||
// If you want to test this program, you need to be able to receive the UDP
|
||||
// packets.
|
||||
// For example, you can run netcat on your computer
|
||||
// $ ncat -ulp 8888
|
||||
// See https://nmap.org/ncat/
|
||||
//
|
||||
// https://arduinojson.org/v6/example/udp-beacon/
|
||||
|
||||
#include <ArduinoJson.h>
|
||||
#include <Ethernet.h>
|
||||
#include <SPI.h>
|
||||
|
||||
byte mac[] = {0xDE, 0xAD, 0xBE, 0xEF, 0xFE, 0xED};
|
||||
IPAddress remoteIp(192, 168, 0, 108); // <- EDIT!!!!
|
||||
unsigned short remotePort = 8888;
|
||||
unsigned short localPort = 8888;
|
||||
EthernetUDP udp;
|
||||
|
||||
void setup() {
|
||||
// Initialize serial port
|
||||
Serial.begin(9600);
|
||||
while (!Serial) continue;
|
||||
|
||||
// Initialize Ethernet libary
|
||||
if (!Ethernet.begin(mac)) {
|
||||
Serial.println(F("Failed to initialize Ethernet library"));
|
||||
return;
|
||||
}
|
||||
|
||||
// Enable UDP
|
||||
udp.begin(localPort);
|
||||
}
|
||||
|
||||
void loop() {
|
||||
// Allocate a temporary JsonDocument
|
||||
// Use https://arduinojson.org/v6/assistant to compute the capacity.
|
||||
StaticJsonDocument<500> doc;
|
||||
|
||||
// Create the "analog" array
|
||||
JsonArray analogValues = doc.createNestedArray("analog");
|
||||
for (int pin = 0; pin < 6; pin++) {
|
||||
// Read the analog input
|
||||
int value = analogRead(pin);
|
||||
|
||||
// Add the value at the end of the array
|
||||
analogValues.add(value);
|
||||
}
|
||||
|
||||
// Create the "digital" array
|
||||
JsonArray digitalValues = doc.createNestedArray("digital");
|
||||
for (int pin = 0; pin < 14; pin++) {
|
||||
// Read the digital input
|
||||
int value = digitalRead(pin);
|
||||
|
||||
// Add the value at the end of the array
|
||||
digitalValues.add(value);
|
||||
}
|
||||
|
||||
// Log
|
||||
Serial.print(F("Sending to "));
|
||||
Serial.print(remoteIp);
|
||||
Serial.print(F(" on port "));
|
||||
Serial.println(remotePort);
|
||||
serializeJson(doc, Serial);
|
||||
|
||||
// Send UDP packet
|
||||
udp.beginPacket(remoteIp, remotePort);
|
||||
serializeJson(doc, udp);
|
||||
udp.println();
|
||||
udp.endPacket();
|
||||
|
||||
// Wait
|
||||
delay(10000);
|
||||
}
|
||||
|
||||
// Performance issue?
|
||||
// ------------------
|
||||
//
|
||||
// EthernetUDP is an unbuffered stream, which is not optimal for ArduinoJson.
|
||||
// See: https://arduinojson.org/v6/how-to/improve-speed/
|
||||
|
||||
// See also
|
||||
// --------
|
||||
//
|
||||
// https://arduinojson.org/ contains the documentation for all the functions
|
||||
// used above. It also includes an FAQ that will help you solve any
|
||||
// serialization problem.
|
||||
//
|
||||
// The book "Mastering ArduinoJson" contains a tutorial on serialization.
|
||||
// It begins with a simple example, then adds more features like serializing
|
||||
// directly to a file or any stream.
|
||||
// Learn more at https://arduinojson.org/book/
|
||||
// Use the coupon code TWENTY for a 20% discount ❤❤❤❤❤
|
||||
@@ -0,0 +1,75 @@
|
||||
// ArduinoJson - https://arduinojson.org
|
||||
// Copyright © 2014-2022, Benoit BLANCHON
|
||||
// MIT License
|
||||
//
|
||||
// This example shows how to deserialize a MessagePack document with
|
||||
// ArduinoJson.
|
||||
//
|
||||
// https://arduinojson.org/v6/example/msgpack-parser/
|
||||
|
||||
#include <ArduinoJson.h>
|
||||
|
||||
void setup() {
|
||||
// Initialize serial port
|
||||
Serial.begin(9600);
|
||||
while (!Serial) continue;
|
||||
|
||||
// Allocate the JSON document
|
||||
//
|
||||
// Inside the brackets, 200 is the capacity of the memory pool in bytes.
|
||||
// Don't forget to change this value to match your JSON document.
|
||||
// Use https://arduinojson.org/v6/assistant to compute the capacity.
|
||||
StaticJsonDocument<200> doc;
|
||||
|
||||
// StaticJsonObject allocates memory on the stack, it can be
|
||||
// replaced by DynamicJsonObject which allocates in the heap.
|
||||
//
|
||||
// DynamicJsonObject doc(200);
|
||||
|
||||
// MessagePack input string.
|
||||
//
|
||||
// Using a char[], as shown here, enables the "zero-copy" mode. This mode uses
|
||||
// the minimal amount of memory because the JsonDocument stores pointers to
|
||||
// the input buffer.
|
||||
// If you use another type of input, ArduinoJson must copy the strings from
|
||||
// the input to the JsonDocument, so you need to increase the capacity of the
|
||||
// JsonDocument.
|
||||
uint8_t input[] = {131, 166, 115, 101, 110, 115, 111, 114, 163, 103, 112, 115,
|
||||
164, 116, 105, 109, 101, 206, 80, 147, 50, 248, 164, 100,
|
||||
97, 116, 97, 146, 203, 64, 72, 96, 199, 58, 188, 148,
|
||||
112, 203, 64, 2, 106, 146, 230, 33, 49, 169};
|
||||
// This MessagePack document contains:
|
||||
// {
|
||||
// "sensor": "gps",
|
||||
// "time": 1351824120,
|
||||
// "data": [48.75608, 2.302038]
|
||||
// }
|
||||
|
||||
DeserializationError error = deserializeMsgPack(doc, input);
|
||||
|
||||
// Test if parsing succeeded.
|
||||
if (error) {
|
||||
Serial.print("deserializeMsgPack() failed: ");
|
||||
Serial.println(error.f_str());
|
||||
return;
|
||||
}
|
||||
|
||||
// Fetch values.
|
||||
//
|
||||
// Most of the time, you can rely on the implicit casts.
|
||||
// In other case, you can do doc["time"].as<long>();
|
||||
const char* sensor = doc["sensor"];
|
||||
long time = doc["time"];
|
||||
double latitude = doc["data"][0];
|
||||
double longitude = doc["data"][1];
|
||||
|
||||
// Print values.
|
||||
Serial.println(sensor);
|
||||
Serial.println(time);
|
||||
Serial.println(latitude, 6);
|
||||
Serial.println(longitude, 6);
|
||||
}
|
||||
|
||||
void loop() {
|
||||
// not used in this example
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
// ArduinoJson - https://arduinojson.org
|
||||
// Copyright © 2014-2022, Benoit BLANCHON
|
||||
// MIT License
|
||||
//
|
||||
// This example shows the different ways you can use Flash strings with
|
||||
// ArduinoJson.
|
||||
//
|
||||
// Use Flash strings sparingly, because ArduinoJson duplicates them in the
|
||||
// JsonDocument. Prefer plain old char*, as they are more efficient in term of
|
||||
// code size, speed, and memory usage.
|
||||
//
|
||||
// https://arduinojson.org/v6/example/progmem/
|
||||
|
||||
#include <ArduinoJson.h>
|
||||
|
||||
void setup() {
|
||||
DynamicJsonDocument doc(1024);
|
||||
|
||||
// You can use a Flash String as your JSON input.
|
||||
// WARNING: the strings in the input will be duplicated in the JsonDocument.
|
||||
deserializeJson(doc, F("{\"sensor\":\"gps\",\"time\":1351824120,"
|
||||
"\"data\":[48.756080,2.302038]}"));
|
||||
JsonObject obj = doc.as<JsonObject>();
|
||||
|
||||
// You can use a Flash String to get an element of a JsonObject
|
||||
// No duplication is done.
|
||||
long time = obj[F("time")];
|
||||
|
||||
// You can use a Flash String to set an element of a JsonObject
|
||||
// WARNING: the content of the Flash String will be duplicated in the
|
||||
// JsonDocument.
|
||||
obj[F("time")] = time;
|
||||
|
||||
// You can set a Flash String to a JsonObject or JsonArray:
|
||||
// WARNING: the content of the Flash String will be duplicated in the
|
||||
// JsonDocument.
|
||||
obj["sensor"] = F("gps");
|
||||
|
||||
// It works with serialized() too:
|
||||
obj["sensor"] = serialized(F("\"gps\""));
|
||||
obj["sensor"] = serialized(F("\xA3gps"), 3);
|
||||
|
||||
// You can compare the content of a JsonVariant to a Flash String
|
||||
if (obj["sensor"] == F("gps")) {
|
||||
// ...
|
||||
}
|
||||
}
|
||||
|
||||
void loop() {
|
||||
// not used in this example
|
||||
}
|
||||
|
||||
// See also
|
||||
// --------
|
||||
//
|
||||
// https://arduinojson.org/ contains the documentation for all the functions
|
||||
// used above. It also includes an FAQ that will help you solve any memory
|
||||
// problem.
|
||||
//
|
||||
// The book "Mastering ArduinoJson" contains a quick C++ course that explains
|
||||
// how your microcontroller stores strings in memory. It also tells why you
|
||||
// should not abuse Flash strings with ArduinoJson.
|
||||
// Learn more at https://arduinojson.org/book/
|
||||
// Use the coupon code TWENTY for a 20% discount ❤❤❤❤❤
|
||||
@@ -0,0 +1,77 @@
|
||||
// ArduinoJson - https://arduinojson.org
|
||||
// Copyright © 2014-2022, Benoit BLANCHON
|
||||
// MIT License
|
||||
//
|
||||
// This example shows the different ways you can use String with ArduinoJson.
|
||||
//
|
||||
// Use String objects sparingly, because ArduinoJson duplicates them in the
|
||||
// JsonDocument. Prefer plain old char[], as they are more efficient in term of
|
||||
// code size, speed, and memory usage.
|
||||
//
|
||||
// https://arduinojson.org/v6/example/string/
|
||||
|
||||
#include <ArduinoJson.h>
|
||||
|
||||
void setup() {
|
||||
DynamicJsonDocument doc(1024);
|
||||
|
||||
// You can use a String as your JSON input.
|
||||
// WARNING: the string in the input will be duplicated in the JsonDocument.
|
||||
String input =
|
||||
"{\"sensor\":\"gps\",\"time\":1351824120,\"data\":[48.756080,2.302038]}";
|
||||
deserializeJson(doc, input);
|
||||
JsonObject obj = doc.as<JsonObject>();
|
||||
|
||||
// You can use a String to get an element of a JsonObject
|
||||
// No duplication is done.
|
||||
long time = obj[String("time")];
|
||||
|
||||
// You can use a String to set an element of a JsonObject
|
||||
// WARNING: the content of the String will be duplicated in the JsonDocument.
|
||||
obj[String("time")] = time;
|
||||
|
||||
// You can get a String from a JsonObject or JsonArray:
|
||||
// No duplication is done, at least not in the JsonDocument.
|
||||
String sensor = obj["sensor"];
|
||||
|
||||
// Unfortunately, the following doesn't work (issue #118):
|
||||
// sensor = obj["sensor"]; // <- error "ambiguous overload for 'operator='"
|
||||
// As a workaround, you need to replace by:
|
||||
sensor = obj["sensor"].as<String>();
|
||||
|
||||
// You can set a String to a JsonObject or JsonArray:
|
||||
// WARNING: the content of the String will be duplicated in the JsonDocument.
|
||||
obj["sensor"] = sensor;
|
||||
|
||||
// It works with serialized() too:
|
||||
obj["sensor"] = serialized(sensor);
|
||||
|
||||
// You can also concatenate strings
|
||||
// WARNING: the content of the String will be duplicated in the JsonDocument.
|
||||
obj[String("sen") + "sor"] = String("gp") + "s";
|
||||
|
||||
// You can compare the content of a JsonObject with a String
|
||||
if (obj["sensor"] == sensor) {
|
||||
// ...
|
||||
}
|
||||
|
||||
// Lastly, you can print the resulting JSON to a String
|
||||
String output;
|
||||
serializeJson(doc, output);
|
||||
}
|
||||
|
||||
void loop() {
|
||||
// not used in this example
|
||||
}
|
||||
|
||||
// See also
|
||||
// --------
|
||||
//
|
||||
// https://arduinojson.org/ contains the documentation for all the functions
|
||||
// used above. It also includes an FAQ that will help you solve any problem.
|
||||
//
|
||||
// The book "Mastering ArduinoJson" contains a quick C++ course that explains
|
||||
// how your microcontroller stores strings in memory. On several occasions, it
|
||||
// shows how you can avoid String in your program.
|
||||
// Learn more at https://arduinojson.org/book/
|
||||
// Use the coupon code TWENTY for a 20% discount ❤❤❤❤❤
|
||||
@@ -0,0 +1,4 @@
|
||||
@PACKAGE_INIT@
|
||||
|
||||
include("${CMAKE_CURRENT_LIST_DIR}/@PROJECT_NAME@Targets.cmake")
|
||||
check_required_components("@PROJECT_NAME@")
|
||||
@@ -0,0 +1,101 @@
|
||||
if(CMAKE_CXX_COMPILER_ID MATCHES "(GNU|Clang)")
|
||||
add_compile_options(
|
||||
-pedantic
|
||||
-Wall
|
||||
-Wcast-align
|
||||
-Wcast-qual
|
||||
-Wconversion
|
||||
-Wctor-dtor-privacy
|
||||
-Wdisabled-optimization
|
||||
-Werror
|
||||
-Wextra
|
||||
-Wformat=2
|
||||
-Winit-self
|
||||
-Wmissing-include-dirs
|
||||
-Wnon-virtual-dtor
|
||||
-Wold-style-cast
|
||||
-Woverloaded-virtual
|
||||
-Wparentheses
|
||||
-Wredundant-decls
|
||||
-Wshadow
|
||||
-Wsign-conversion
|
||||
-Wsign-promo
|
||||
-Wstrict-aliasing
|
||||
-Wundef
|
||||
)
|
||||
|
||||
if(${COVERAGE})
|
||||
set(CMAKE_CXX_FLAGS "-fprofile-arcs -ftest-coverage")
|
||||
endif()
|
||||
|
||||
endif()
|
||||
|
||||
if(CMAKE_CXX_COMPILER_ID STREQUAL "GNU")
|
||||
if((CMAKE_CXX_COMPILER_VERSION VERSION_GREATER 4.8) AND (NOT ${COVERAGE}))
|
||||
add_compile_options(-g -Og)
|
||||
else()
|
||||
add_compile_options(-g -O0)
|
||||
endif()
|
||||
|
||||
add_compile_options(
|
||||
-Wstrict-null-sentinel
|
||||
-Wno-vla # Allow VLA in tests
|
||||
)
|
||||
add_definitions(-DHAS_VARIABLE_LENGTH_ARRAY)
|
||||
|
||||
if(CMAKE_CXX_COMPILER_VERSION VERSION_GREATER 4.5)
|
||||
add_compile_options(-Wlogical-op) # the flag exists in 4.4 but is buggy
|
||||
endif()
|
||||
|
||||
if(CMAKE_CXX_COMPILER_VERSION VERSION_GREATER 4.6)
|
||||
add_compile_options(-Wnoexcept)
|
||||
endif()
|
||||
endif()
|
||||
|
||||
if(CMAKE_CXX_COMPILER_ID MATCHES "Clang")
|
||||
add_compile_options(
|
||||
-Wc++11-compat
|
||||
-Wdeprecated-register
|
||||
-Wno-vla-extension # Allow VLA in tests
|
||||
)
|
||||
add_definitions(
|
||||
-DHAS_VARIABLE_LENGTH_ARRAY
|
||||
-DSUBSCRIPT_CONFLICTS_WITH_BUILTIN_OPERATOR
|
||||
)
|
||||
endif()
|
||||
|
||||
if(CMAKE_CXX_COMPILER_ID STREQUAL "Clang")
|
||||
if((CMAKE_CXX_COMPILER_VERSION VERSION_GREATER 4.0) AND (NOT ${COVERAGE}))
|
||||
add_compile_options(-g -Og)
|
||||
else()
|
||||
add_compile_options(-g -O0)
|
||||
endif()
|
||||
endif()
|
||||
|
||||
if(CMAKE_CXX_COMPILER_ID STREQUAL "AppleClang")
|
||||
if((CMAKE_CXX_COMPILER_VERSION VERSION_GREATER 9.0) AND (NOT ${COVERAGE}))
|
||||
add_compile_options(-g -Og)
|
||||
else()
|
||||
add_compile_options(-g -O0)
|
||||
endif()
|
||||
endif()
|
||||
|
||||
if(MSVC)
|
||||
add_definitions(-D_CRT_SECURE_NO_WARNINGS)
|
||||
add_compile_options(
|
||||
/W4 # Set warning level
|
||||
/WX # Treats all compiler warnings as errors.
|
||||
)
|
||||
|
||||
if (NOT MSVC_VERSION LESS 1910) # >= Visual Studio 2017
|
||||
add_compile_options(
|
||||
/Zc:__cplusplus # Enable updated __cplusplus macro
|
||||
)
|
||||
endif()
|
||||
endif()
|
||||
|
||||
if(MINGW)
|
||||
# Static link on MinGW to avoid linking with the wrong DLLs when multiple
|
||||
# versions are installed.
|
||||
add_link_options(-static)
|
||||
endif()
|
||||
@@ -0,0 +1,8 @@
|
||||
# ArduinoJson - https://arduinojson.org
|
||||
# Copyright © 2014-2022, Benoit BLANCHON
|
||||
# MIT License
|
||||
|
||||
cmake_minimum_required(VERSION 3.5)
|
||||
|
||||
include($ENV{IDF_PATH}/tools/cmake/project.cmake)
|
||||
project(example)
|
||||
@@ -0,0 +1,6 @@
|
||||
# ArduinoJson - https://arduinojson.org
|
||||
# Copyright © 2014-2022, Benoit BLANCHON
|
||||
# MIT License
|
||||
|
||||
idf_component_register(SRCS "main.cpp"
|
||||
INCLUDE_DIRS "")
|
||||
@@ -0,0 +1,4 @@
|
||||
#
|
||||
# "main" pseudo-component makefile.
|
||||
#
|
||||
# (Uses default behaviour of compiling all source files in directory, adding 'include' to include path.)
|
||||
@@ -0,0 +1,16 @@
|
||||
// ArduinoJson - https://arduinojson.org
|
||||
// Copyright © 2014-2022, Benoit BLANCHON
|
||||
// MIT License
|
||||
|
||||
#include <ArduinoJson.h>
|
||||
|
||||
extern "C" void app_main() {
|
||||
char buffer[256];
|
||||
StaticJsonDocument<200> doc;
|
||||
|
||||
doc["hello"] = "world";
|
||||
serializeJson(doc, buffer);
|
||||
deserializeJson(doc, buffer);
|
||||
serializeMsgPack(doc, buffer);
|
||||
deserializeMsgPack(doc, buffer);
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
#!/bin/sh -ex
|
||||
|
||||
BOARD=$1
|
||||
|
||||
cd "$(dirname "$0")/../../"
|
||||
|
||||
cp extras/particle/src/smocktest.ino src/
|
||||
cp extras/particle/project.properties ./
|
||||
|
||||
particle compile "$BOARD"
|
||||
@@ -0,0 +1,16 @@
|
||||
#include <ArduinoJson.h>
|
||||
|
||||
static_assert(ARDUINOJSON_USE_LONG_LONG == 0, "ARDUINOJSON_USE_LONG_LONG");
|
||||
|
||||
static_assert(ARDUINOJSON_SLOT_OFFSET_SIZE == 1,
|
||||
"ARDUINOJSON_SLOT_OFFSET_SIZE");
|
||||
|
||||
static_assert(ARDUINOJSON_LITTLE_ENDIAN == 1, "ARDUINOJSON_LITTLE_ENDIAN");
|
||||
|
||||
static_assert(ARDUINOJSON_USE_DOUBLE == 1, "ARDUINOJSON_USE_DOUBLE");
|
||||
|
||||
static_assert(sizeof(ARDUINOJSON_NAMESPACE::VariantSlot) == 8,
|
||||
"sizeof(VariantSlot)");
|
||||
|
||||
void setup() {}
|
||||
void loop() {}
|
||||
@@ -0,0 +1,16 @@
|
||||
#include <ArduinoJson.h>
|
||||
|
||||
static_assert(ARDUINOJSON_USE_LONG_LONG == 1, "ARDUINOJSON_USE_LONG_LONG");
|
||||
|
||||
static_assert(ARDUINOJSON_SLOT_OFFSET_SIZE == 2,
|
||||
"ARDUINOJSON_SLOT_OFFSET_SIZE");
|
||||
|
||||
static_assert(ARDUINOJSON_LITTLE_ENDIAN == 1, "ARDUINOJSON_LITTLE_ENDIAN");
|
||||
|
||||
static_assert(ARDUINOJSON_USE_DOUBLE == 1, "ARDUINOJSON_USE_DOUBLE");
|
||||
|
||||
static_assert(sizeof(ARDUINOJSON_NAMESPACE::VariantSlot) == 16,
|
||||
"sizeof(VariantSlot)");
|
||||
|
||||
void setup() {}
|
||||
void loop() {}
|
||||
@@ -0,0 +1,15 @@
|
||||
#include <ArduinoJson.h>
|
||||
|
||||
static_assert(ARDUINOJSON_USE_LONG_LONG == 1, "ARDUINOJSON_USE_LONG_LONG");
|
||||
|
||||
static_assert(ARDUINOJSON_SLOT_OFFSET_SIZE == 4,
|
||||
"ARDUINOJSON_SLOT_OFFSET_SIZE");
|
||||
|
||||
static_assert(ARDUINOJSON_LITTLE_ENDIAN == 1, "ARDUINOJSON_LITTLE_ENDIAN");
|
||||
|
||||
static_assert(ARDUINOJSON_USE_DOUBLE == 1, "ARDUINOJSON_USE_DOUBLE");
|
||||
|
||||
static_assert(sizeof(ARDUINOJSON_NAMESPACE::VariantSlot) == 32,
|
||||
"sizeof(VariantSlot)");
|
||||
|
||||
int main() {}
|
||||
@@ -0,0 +1,15 @@
|
||||
#include <ArduinoJson.h>
|
||||
|
||||
static_assert(ARDUINOJSON_USE_LONG_LONG == 1, "ARDUINOJSON_USE_LONG_LONG");
|
||||
|
||||
static_assert(ARDUINOJSON_SLOT_OFFSET_SIZE == 2,
|
||||
"ARDUINOJSON_SLOT_OFFSET_SIZE");
|
||||
|
||||
static_assert(ARDUINOJSON_LITTLE_ENDIAN == 1, "ARDUINOJSON_LITTLE_ENDIAN");
|
||||
|
||||
static_assert(ARDUINOJSON_USE_DOUBLE == 1, "ARDUINOJSON_USE_DOUBLE");
|
||||
|
||||
static_assert(sizeof(ARDUINOJSON_NAMESPACE::VariantSlot) == 16,
|
||||
"sizeof(VariantSlot)");
|
||||
|
||||
int main() {}
|
||||
@@ -0,0 +1,59 @@
|
||||
# ArduinoJson - https://arduinojson.org
|
||||
# Copyright © 2014-2022, Benoit BLANCHON
|
||||
# MIT License
|
||||
|
||||
if(MSVC)
|
||||
add_compile_options(-D_CRT_SECURE_NO_WARNINGS)
|
||||
endif()
|
||||
|
||||
add_executable(msgpack_reproducer
|
||||
msgpack_fuzzer.cpp
|
||||
reproducer.cpp
|
||||
)
|
||||
target_link_libraries(msgpack_reproducer
|
||||
ArduinoJson
|
||||
)
|
||||
|
||||
add_executable(json_reproducer
|
||||
json_fuzzer.cpp
|
||||
reproducer.cpp
|
||||
)
|
||||
target_link_libraries(json_reproducer
|
||||
ArduinoJson
|
||||
)
|
||||
|
||||
macro(add_fuzzer name)
|
||||
set(FUZZER "${name}_fuzzer")
|
||||
set(CORPUS_DIR "${CMAKE_CURRENT_SOURCE_DIR}/${name}_corpus")
|
||||
set(SEED_CORPUS_DIR "${CMAKE_CURRENT_SOURCE_DIR}/${name}_seed_corpus")
|
||||
add_executable("${FUZZER}"
|
||||
"${name}_fuzzer.cpp"
|
||||
)
|
||||
target_link_libraries("${FUZZER}"
|
||||
ArduinoJson
|
||||
)
|
||||
set_target_properties("${FUZZER}"
|
||||
PROPERTIES
|
||||
COMPILE_FLAGS
|
||||
"-fprofile-instr-generate -fcoverage-mapping -fsanitize=fuzzer -fno-sanitize-recover=all"
|
||||
LINK_FLAGS
|
||||
"-fprofile-instr-generate -fcoverage-mapping -fsanitize=fuzzer -fno-sanitize-recover=all"
|
||||
)
|
||||
|
||||
add_test(
|
||||
NAME
|
||||
"${FUZZER}"
|
||||
COMMAND
|
||||
"${FUZZER}" "${CORPUS_DIR}" "${SEED_CORPUS_DIR}" -max_total_time=5 -timeout=1
|
||||
)
|
||||
|
||||
set_tests_properties("${FUZZER}"
|
||||
PROPERTIES
|
||||
LABELS "Fuzzing"
|
||||
)
|
||||
endmacro()
|
||||
|
||||
if (CMAKE_CXX_COMPILER_ID STREQUAL "Clang" AND CMAKE_CXX_COMPILER_VERSION VERSION_GREATER 6)
|
||||
add_fuzzer(json)
|
||||
add_fuzzer(msgpack)
|
||||
endif()
|
||||
@@ -0,0 +1,22 @@
|
||||
# CAUTION: this file is invoked by https://github.com/google/oss-fuzz
|
||||
|
||||
CXXFLAGS += -I../../src -DARDUINOJSON_DEBUG=1
|
||||
|
||||
all: \
|
||||
$(OUT)/json_fuzzer \
|
||||
$(OUT)/json_fuzzer_seed_corpus.zip \
|
||||
$(OUT)/json_fuzzer.options \
|
||||
$(OUT)/msgpack_fuzzer \
|
||||
$(OUT)/msgpack_fuzzer_seed_corpus.zip \
|
||||
$(OUT)/msgpack_fuzzer.options
|
||||
|
||||
$(OUT)/%_fuzzer: %_fuzzer.cpp $(shell find ../../src -type f)
|
||||
$(CXX) $(CXXFLAGS) $< -o$@ $(LIB_FUZZING_ENGINE)
|
||||
|
||||
$(OUT)/%_fuzzer_seed_corpus.zip: %_seed_corpus/*
|
||||
zip -j $@ $?
|
||||
|
||||
$(OUT)/%_fuzzer.options:
|
||||
@echo "[libfuzzer]" > $@
|
||||
@echo "max_len = 256" >> $@
|
||||
@echo "timeout = 10" >> $@
|
||||
@@ -0,0 +1,11 @@
|
||||
#include <ArduinoJson.h>
|
||||
|
||||
extern "C" int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) {
|
||||
DynamicJsonDocument doc(4096);
|
||||
DeserializationError error = deserializeJson(doc, data, size);
|
||||
if (!error) {
|
||||
std::string json;
|
||||
serializeJson(doc, json);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
//comment
|
||||
/*comment*/
|
||||
[ //comment
|
||||
/*comment*/"comment"/*comment*/,//comment
|
||||
/*comment*/{//comment
|
||||
/* comment*/"key"//comment
|
||||
: //comment
|
||||
"value"//comment
|
||||
}/*comment*/
|
||||
]//comment
|
||||
@@ -0,0 +1 @@
|
||||
[]
|
||||
@@ -0,0 +1 @@
|
||||
{}
|
||||
+1
@@ -0,0 +1 @@
|
||||
[1,[2,[3,[4,[5,[6,[7,[8,[9,[10,[11,[12,[13,[14,[15,[16,[17,[18,[19,[20,[21,[22,[23,[24,[25,[26,[27,[28,[29,[30,[31,[32,[33,[34,[35,[36,[37,[38,[39,[40,[41,[42,[43,[44,[45,[46,[47,[48,[49,[50,[51,[52,[53,[54,[55,[56,[57,[58,[59,[60,[61,[62,[63,[64,[65,[66,[67,[68,[69,[70,[71,[72,[73,[74,[75,[76,[77,[78,[79,[80,[81,[82,[83,[84,[85,[86,[87,[88,[89,[90,[91,[92,[93,[94,[95,[96,[97,[98,[99,[100,[101,[102,[103,[104,[105,[106,[107,[108,[109,[110,[111,[112,[113,[114,[115,[116,[117,[118,[119,[120]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]
|
||||
@@ -0,0 +1 @@
|
||||
9720730739393920739
|
||||
@@ -0,0 +1,24 @@
|
||||
[
|
||||
123,
|
||||
-123,
|
||||
123.456,
|
||||
-123.456,
|
||||
12e34,
|
||||
12e-34,
|
||||
12e+34,
|
||||
12E34,
|
||||
12E-34,
|
||||
12E+34,
|
||||
12.34e56,
|
||||
12.34e-56,
|
||||
12.34e+56,
|
||||
12.34E56,
|
||||
12.34E-56,
|
||||
12.34E+56,
|
||||
NaN,
|
||||
-NaN,
|
||||
+NaN,
|
||||
Infinity,
|
||||
+Infinity,
|
||||
-Infinity
|
||||
]
|
||||
@@ -0,0 +1,53 @@
|
||||
{
|
||||
"coord": {
|
||||
"lon": -0.13,
|
||||
"lat": 51.51
|
||||
},
|
||||
"weather": [
|
||||
{
|
||||
"id": 301,
|
||||
"main": "Drizzle",
|
||||
"description": "drizzle",
|
||||
"icon": "09n"
|
||||
},
|
||||
{
|
||||
"id": 701,
|
||||
"main": "Mist",
|
||||
"description": "mist",
|
||||
"icon": "50n"
|
||||
},
|
||||
{
|
||||
"id": 741,
|
||||
"main": "Fog",
|
||||
"description": "fog",
|
||||
"icon": "50n"
|
||||
}
|
||||
],
|
||||
"base": "stations",
|
||||
"main": {
|
||||
"temp": 281.87,
|
||||
"pressure": 1032,
|
||||
"humidity": 100,
|
||||
"temp_min": 281.15,
|
||||
"temp_max": 283.15
|
||||
},
|
||||
"visibility": 2900,
|
||||
"wind": {
|
||||
"speed": 1.5
|
||||
},
|
||||
"clouds": {
|
||||
"all": 90
|
||||
},
|
||||
"dt": 1483820400,
|
||||
"sys": {
|
||||
"type": 1,
|
||||
"id": 5091,
|
||||
"message": 0.0226,
|
||||
"country": "GB",
|
||||
"sunrise": 1483776245,
|
||||
"sunset": 1483805443
|
||||
},
|
||||
"id": 2643743,
|
||||
"name": "London",
|
||||
"cod": 200
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
[
|
||||
"hello",
|
||||
'hello',
|
||||
hello,
|
||||
{"hello":"world"},
|
||||
{'hello':'world'},
|
||||
{hello:world}
|
||||
]
|
||||
+90
@@ -0,0 +1,90 @@
|
||||
{
|
||||
"response": {
|
||||
"version": "0.1",
|
||||
"termsofService": "http://www.wunderground.com/weather/api/d/terms.html",
|
||||
"features": {
|
||||
"conditions": 1
|
||||
}
|
||||
},
|
||||
"current_observation": {
|
||||
"image": {
|
||||
"url": "http://icons-ak.wxug.com/graphics/wu2/logo_130x80.png",
|
||||
"title": "Weather Underground",
|
||||
"link": "http://www.wunderground.com"
|
||||
},
|
||||
"display_location": {
|
||||
"full": "San Francisco, CA",
|
||||
"city": "San Francisco",
|
||||
"state": "CA",
|
||||
"state_name": "California",
|
||||
"country": "US",
|
||||
"country_iso3166": "US",
|
||||
"zip": "94101",
|
||||
"latitude": "37.77500916",
|
||||
"longitude": "-122.41825867",
|
||||
"elevation": "47.00000000"
|
||||
},
|
||||
"observation_location": {
|
||||
"full": "SOMA - Near Van Ness, San Francisco, California",
|
||||
"city": "SOMA - Near Van Ness, San Francisco",
|
||||
"state": "California",
|
||||
"country": "US",
|
||||
"country_iso3166": "US",
|
||||
"latitude": "37.773285",
|
||||
"longitude": "-122.417725",
|
||||
"elevation": "49 ft"
|
||||
},
|
||||
"estimated": {},
|
||||
"station_id": "KCASANFR58",
|
||||
"observation_time": "Last Updated on June 27, 5:27 PM PDT",
|
||||
"observation_time_rfc822": "Wed, 27 Jun 2012 17:27:13 -0700",
|
||||
"observation_epoch": "1340843233",
|
||||
"local_time_rfc822": "Wed, 27 Jun 2012 17:27:14 -0700",
|
||||
"local_epoch": "1340843234",
|
||||
"local_tz_short": "PDT",
|
||||
"local_tz_long": "America/Los_Angeles",
|
||||
"local_tz_offset": "-0700",
|
||||
"weather": "Partly Cloudy",
|
||||
"temperature_string": "66.3 F (19.1 C)",
|
||||
"temp_f": 66.3,
|
||||
"temp_c": 19.1,
|
||||
"relative_humidity": "65%",
|
||||
"wind_string": "From the NNW at 22.0 MPH Gusting to 28.0 MPH",
|
||||
"wind_dir": "NNW",
|
||||
"wind_degrees": 346,
|
||||
"wind_mph": 22,
|
||||
"wind_gust_mph": "28.0",
|
||||
"wind_kph": 35.4,
|
||||
"wind_gust_kph": "45.1",
|
||||
"pressure_mb": "1013",
|
||||
"pressure_in": "29.93",
|
||||
"pressure_trend": "+",
|
||||
"dewpoint_string": "54 F (12 C)",
|
||||
"dewpoint_f": 54,
|
||||
"dewpoint_c": 12,
|
||||
"heat_index_string": "NA",
|
||||
"heat_index_f": "NA",
|
||||
"heat_index_c": "NA",
|
||||
"windchill_string": "NA",
|
||||
"windchill_f": "NA",
|
||||
"windchill_c": "NA",
|
||||
"feelslike_string": "66.3 F (19.1 C)",
|
||||
"feelslike_f": "66.3",
|
||||
"feelslike_c": "19.1",
|
||||
"visibility_mi": "10.0",
|
||||
"visibility_km": "16.1",
|
||||
"solarradiation": "",
|
||||
"UV": "5",
|
||||
"precip_1hr_string": "0.00 in ( 0 mm)",
|
||||
"precip_1hr_in": "0.00",
|
||||
"precip_1hr_metric": " 0",
|
||||
"precip_today_string": "0.00 in (0 mm)",
|
||||
"precip_today_in": "0.00",
|
||||
"precip_today_metric": "0",
|
||||
"icon": "partlycloudy",
|
||||
"icon_url": "http://icons-ak.wxug.com/i/c/k/partlycloudy.gif",
|
||||
"forecast_url": "http://www.wunderground.com/US/CA/San_Francisco.html",
|
||||
"history_url": "http://www.wunderground.com/history/airport/KCASANFR58/2012/6/27/DailyHistory.html",
|
||||
"ob_url": "http://www.wunderground.com/cgi-bin/findweather/getForecast?query=37.773285,-122.417725"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
#include <ArduinoJson.h>
|
||||
|
||||
extern "C" int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) {
|
||||
DynamicJsonDocument doc(4096);
|
||||
DeserializationError error = deserializeMsgPack(doc, data, size);
|
||||
if (!error) {
|
||||
std::string json;
|
||||
serializeMsgPack(doc, json);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
Binary file not shown.
Binary file not shown.
@@ -0,0 +1 @@
|
||||
�
|
||||
@@ -0,0 +1 @@
|
||||
’¥hello¥world
|
||||
@@ -0,0 +1 @@
|
||||
�
|
||||
@@ -0,0 +1 @@
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
‚£one£two
|
||||
@@ -0,0 +1 @@
|
||||
«hello world
|
||||
@@ -0,0 +1 @@
|
||||
ハ@H�
|
||||
@@ -0,0 +1 @@
|
||||
Л@ !КАѓo
|
||||
@@ -0,0 +1 @@
|
||||
拮�
|
||||
@@ -0,0 +1 @@
|
||||
Ҷi�.
|
||||
@@ -0,0 +1 @@
|
||||
モ4Vx埔゙�
|
||||
@@ -0,0 +1 @@
|
||||
��
|
||||
Binary file not shown.
Binary file not shown.
Loaded 100 of 843 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user