Skip to content

Commit ae407aa

Browse files
committed
Bugfixes: loop() polling; EEPROM serial debug; day/night-off day and time range checking; alarm toggle clear snooze and update display
1 parent cdb70b0 commit ae407aa

File tree

2 files changed

+83
-59
lines changed

2 files changed

+83
-59
lines changed

README.md

Lines changed: 8 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,9 @@
11
# arduino-nixie
22
**A digital clock with perpetual calendar, alarm, countdown timer, and day counter.** Written for the Arduino Nano at the heart of [RLB Designs'](http://rlb-designs.com/) Universal Nixie Driver Board (UNDB) v5.0, featuring a DS3231 thermocompensated battery-backed real-time clock, and driving up to 6 digits multiplexed in pairs via two SN74141 driver chips. Uses AdaEncoder and ooPinChangeInt (for rotary encoders, optional) and NorthernWidget DS3231 libraries.
33

4-
**This is an alternate codebase that is very much in progress!**
5-
64
## Instructions
75

8-
_In these instructions, **Select** is the main pushbutton, and **Adjust** can be a pair of up/down buttons (hold to set faster), or a knob (rotary encoder). Other variations may apply depending on the options selected in the code._
6+
_In these instructions, **Select** is the main pushbutton, and **Adjust** can be a pair of up/down buttons (hold to set faster), or a knob (rotary encoder). Other variations may apply depending on the options selected in the code, and functions may be reordered or left out._
97

108
### Clock Functions
119

@@ -24,29 +22,28 @@ _In these instructions, **Select** is the main pushbutton, and **Adjust** can be
2422

2523
### Options Menu
2624

27-
* Additional settings are available in the options menu.
2825
* To access this, hold **Select** for 3 seconds until you see a single `1` on the hour tubes. This indicates option number 1.
2926
* Use **Adjust** to go to the option number you want to set (see table below); press **Select** to open it for setting (display will flash); use **Adjust** to set; and **Select** to save.
3027
* When all done, hold **Select** to exit the options menu.
3128

3229
| Option | Settings |
3330
| --- | --- |
34-
| | **Timekeeping and display** |
31+
| **Timekeeping and display** ||
3532
| 1. Time format | 1 = 12-hour<br/>2 = 24-hour<br/>(time-of-day display only; setting times is always done in 24h) |
3633
| 2. Date format | 1 = month/date<br/>2 = date/month |
3734
| 3. Display date during time? | 0 = never<br/>1 = date instead of seconds<br/>2 = full date (as above) every minute at :30 seconds |
3835
| 4. Leading zero in hour, date, and month? | 0 = no<br/>1 = yes |
3936
| 5. Digit fade | _Not yet implemented._<br/>0–50 (in hundredths of a second) |
4037
| 6. Auto DST | Add 1h for daylight saving time between these dates (at 2am):<br/>0 = off<br/>1 = second Sunday in March to first Sunday in November (US/CA)<br/>2 = last Sunday in March to last Sunday in October (UK/EU)<br/>3 = first Sunday in April to last Sunday in October (MX)<br/>4 = last Sunday in September to first Sunday in April (NZ)<br/>5 = first Sunday in October to first Sunday in April (AU)<br/>6 = third Sunday in October to third Sunday in February (BZ) |
41-
| | **Alarms and sounds** |
38+
| **Alarms and sounds** ||
4239
| 7. Alarm days | 0 = every day<br/>1 = work week only (per settings below)<br/>2 = weekend only |
4340
| 8. Alarm snooze | 0–60 minutes. 0 disables snooze. |
44-
| 9. Alarm tone pitch | [Note number on a piano keyboard](https://en.wikipedia.org/wiki/Piano_key_frequencies), from 49 (A4) to 88 (C8). Some are louder than others! |
41+
| 9. Alarm signal pitch | [Note number on a piano keyboard](https://en.wikipedia.org/wiki/Piano_key_frequencies), from 49 (A4) to 88 (C8). Some are louder than others! |
4542
| 10. Timer interval mode | What happens when the timer reaches 0.<br/>0 = stop and sound continuously<br/>1 = restart and sound a single tone (interval timer) |
46-
| 11. Timer tone pitch | Set the same way as the alarm tone pitch, above. |
47-
| 12. Hourly strike | 0 = off<br/>1 = single beep<br/>2 = pips<br/>3 = strike the hour<br/>4 = ship's bell<br/>(Clocks without radio/timer control only. Will not sound during day-off or night-off.) |
48-
| 13. Hourly strike pitch | Set the same way as the alarm tone pitch, above. |
49-
| | **Night-off and day-off** |
43+
| 11. Timer signal pitch | Set the same way as the alarm pitch, above. |
44+
| 12. Hourly strike | 0 = off<br/>1 = single beep<br/>2 = pips<br/>3 = strike the hour<br/>4 = ship's bell<br/>Clocks without radio/timer control only. Will not sound during day-off/night-off (except when entering day-off/night-off at the top of the hour). |
45+
| 13. Hourly strike pitch | Set the same way as the alarm signal pitch, above. If using the pips, 63 (987 Hz) is closest to the real BBC pips frequency (1000 Hz). |
46+
| **Night-off and day-off** ||
5047
| 14. Night-off | To save tube life and/or preserve your sleep, dim or shut off tubes nightly when you're not around or sleeping.<br/>0 = none (tubes fully on at night)<br/>1 = dim tubes at night<br/>2 = shut off tubes at night<br/>When off, you can press **Select** to illuminate the tubes briefly. |
5148
| 15. Night starts at | Time of day. |
5249
| 16. Night ends at | Time of day. Set to 0:00 to use the alarm time. At this time (whether night-off/alarm is enabled or not), all tubes will briefly cycle through all digits at full brightness to help prevent [cathode poisoning](http://www.tube-tester.com/sites/nixie/different/cathode%20poisoning/cathode-poisoning.htm). |

sixtube_lm/sixtube_lm.ino

Lines changed: 75 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ const byte fnIsDayCount = 4;
3434
const byte fnIsTemp = 5;
3535
const byte fnIsTubeTester = 6; //cycles all digits on all tubes 1/second, similar to anti-cathode-poisoning cleaner
3636
// functions enabled in this clock, in their display order. Only fnIsTime is required
37-
const byte fnsEnabled[] = {fnIsTime, fnIsDate, fnIsAlarm, fnIsTimer, fnIsDayCount, fnIsTemp, fnIsTubeTester};
37+
const byte fnsEnabled[] = {fnIsTime, fnIsDate, fnIsAlarm, fnIsTimer, fnIsDayCount}; //, fnIsTemp, fnIsTubeTester
3838

3939
// These are the RLB board connections to Arduino analog input pins.
4040
// S1/PL13 = Reset
@@ -89,9 +89,9 @@ const byte displaySize = 6; //number of tubes
8989
// How long (in ms) are the button hold durations?
9090
const word btnShortHold = 1000; //for setting the displayed feataure
9191
const word btnLongHold = 3000; //for for entering options menu
92-
const byte velThreshold = 100; //ms
92+
const byte velThreshold = 150; //ms
9393
// When an adj up/down input (btn or rot) follows another in less than this time, value will change more (10 vs 1).
94-
// Recommend ~100 for rotaries. If you want to use this feature with buttons, extend to ~400.
94+
// Recommend ~150 for rotaries. If you want to use this feature with buttons, extend to ~400.
9595

9696

9797
////////// Other global consts and vars //////////
@@ -172,7 +172,7 @@ word unoffRemain = 0; //un-off (briefly turn on tubes during full night-off or d
172172
byte displayNext[6] = {15,15,15,15,15,15}; //Internal representation of display. Blank to start. Change this to change tubes.
173173
byte displayDim = 2; //dim per display or function: 2=normal, 1=dim, 0=off
174174
byte cleanRemain = 11; //anti-cathode-poisoning clean timeout counter, increments at cleanSpeed ms (see loop()). Start at 11 to run at clock startup
175-
word cleanSpeed = 300; //ms
175+
word cleanSpeed = 200; //ms
176176

177177

178178
////////// Main code control //////////
@@ -191,19 +191,18 @@ void loop(){
191191
unsigned long now = millis();
192192
//Handle tube cleaning. A special case since it runs at a speed outside the normal cycles as below
193193
if(cleanRemain) {
194-
if(pollCleanLast<now+cleanSpeed) {
194+
if(pollCleanLast+cleanSpeed<now) {
195195
pollCleanLast=now;
196196
cleanRemain--;
197197
updateDisplay();
198198
}
199199
}
200200
//Things done every 50ms - avoids overpolling(?) and switch bounce(?)
201-
if(pollLast<now+50) {
201+
if(pollLast+20<now) {
202202
pollLast=now;
203203
checkRTC(false); //if clock has ticked, decrement timer if running, and updateDisplay
204204
checkInputs(); //if inputs have changed, this will do things + updateDisplay as needed
205205
doSetHold(); //if inputs have been held, this will do more things + updateDisplay as needed
206-
//cycleBellTone(); //if beeper is making a bell tone noise, continue to "animate" its decay TODO
207206
}
208207
//Things done every loop cycle
209208
cycleDisplay(); //keeps the display hardware multiplexing cycle going
@@ -285,13 +284,15 @@ void checkRot(){
285284
AdaEncoder *thisEncoder=NULL;
286285
thisEncoder = AdaEncoder::genie();
287286
if(thisEncoder!=NULL) {
288-
inputLast2 = inputLast; inputLast = millis();
287+
unsigned long inputThis = millis();
288+
if(inputThis-inputLast < 70) return; //ignore inputs that come faster than a human could rotate
289289
int8_t clicks = thisEncoder->query(); //signed number of clicks it has moved
290290
byte dir = (clicks<0?0:1);
291291
clicks = abs(clicks);
292292
for(byte i=0; i<clicks; i++){ //in case of more than one click
293293
ctrlEvt((thisEncoder->getID()=='a'?(dir?mainAdjUp:mainAdjDn):(dir?altAdjUp:altAdjDn)),1);
294294
}
295+
inputLast2 = inputLast; inputLast = inputThis;
295296
}
296297
}
297298
}//end checkRot
@@ -399,6 +400,9 @@ void ctrlEvt(byte ctrl, byte evt){
399400
else { //fn setting
400401
if(evt==1) { //we respond only to press evts during fn setting
401402
//TODO could we do release/shorthold on mainSel so we can exit without making changes?
403+
//currently no, because we don't btnStop() when short hold goes into fn setting, in case long hold may go to options menu
404+
//so we can't handle a release because it would immediately save if releasing from the short hold.
405+
//Consider recording the btn start time when going into fn setting so we can distinguish its release from a future one
402406
if(ctrl==mainSel) { //mainSel push: go to next option or save and exit setting mode
403407
btnStop(); //not waiting for mainSelHold, so can stop listening here
404408
//We will set ds3231 time parts directly
@@ -574,18 +578,19 @@ word readEEPROM(int loc, bool isWord){
574578
void writeEEPROM(int loc, int val, bool isWord){
575579
if(isWord) {
576580
Serial.print(F("EEPROM write word:"));
581+
Serial.print(F(" loc ")); Serial.print(loc,DEC);
577582
if(EEPROM.read(loc)!=highByte(val)) {
578583
EEPROM.write(loc,highByte(val));
579-
Serial.print(F(" loc ")); Serial.print(loc,DEC);
580584
Serial.print(F(" val ")); Serial.print(highByte(val),DEC);
581-
} else Serial.print(F(" loc ")); Serial.print(loc,DEC); Serial.print(F(" unchanged (no write)."));
585+
} else Serial.print(F(" unchanged (no write)."));
586+
Serial.print(F(" loc ")); Serial.print(loc+1,DEC);
582587
if(EEPROM.read(loc+1)!=lowByte(val)) {
583588
EEPROM.write(loc+1,lowByte(val));
584-
Serial.print(F(" loc ")); Serial.print(loc+1,DEC);
585589
Serial.print(F(" val ")); Serial.print(lowByte(val),DEC);
586-
} else Serial.print(F(" loc ")); Serial.print(loc+1,DEC); Serial.print(F(" unchanged (no write)."));
590+
} else Serial.print(F(" unchanged (no write)."));
587591
} else {
588-
Serial.print(F("EEPROM write byte:")); Serial.print(F(" loc ")); Serial.print(loc,DEC);
592+
Serial.print(F("EEPROM write byte:"));
593+
Serial.print(F(" loc ")); Serial.print(loc,DEC);
589594
if(EEPROM.read(loc)!=val) { EEPROM.write(loc,val);
590595
Serial.print(F(" val ")); Serial.print(val,DEC);
591596
} else Serial.print(F(" unchanged (no write)."));
@@ -649,48 +654,50 @@ void checkRTC(bool force){
649654
if(force && rtcSecLast != tod.second()) force=false; //in the odd case it's BOTH, recognize the natural second
650655
rtcSecLast = tod.second();
651656

657+
//Things to do at specific times
652658
if(tod.second()==0) { //at top of minute
653659
//at 2am, check for DST change
654660
if(tod.minute()==0 && tod.hour()==2) autoDST();
655661
//check if we should trigger the alarm - if the time is right and the alarm is on...
656662
if(tod.hour()*60+tod.minute()==readEEPROM(0,true) && readEEPROM(2,false)) {
657663
if(readEEPROM(23,false)==0 || //any day of the week
658-
(readEEPROM(23,false)==1 && toddow>=readEEPROM(33,false) && toddow<=readEEPROM(34,false)) || //weekday only
659-
(readEEPROM(23,false)==2 && toddow<readEEPROM(33,false) && toddow>readEEPROM(34,false))) { //weekend only
664+
(readEEPROM(23,false)==1 && isDayInRange(readEEPROM(33,false),readEEPROM(34,false),toddow)) || //weekday only
665+
(readEEPROM(23,false)==2 && !isDayInRange(readEEPROM(33,false),readEEPROM(34,false),toddow)) ) { //weekend only
660666
fnSetPg = 0; fn = fnIsTime; signalPitch = getHz(readEEPROM(39,false)); signalRemain = signalDur*60;
661667
} //end toddow check
662668
} //end alarm trigger
663-
//if it's the top or bottom of the hour, maybe do some striking
664-
//TODO some of these could do with better timing than once per second (e.g. strike/2s and ship's bells in pairs)
665-
if((tod.minute()==0 || tod.minute()==30) && signalType<2 && readEEPROM(21,false)>0 && fn==fnIsTime && fnSetPg==0 && displayDim==2) {
666-
signalPitch = getHz(readEEPROM(41,false));
667-
byte hr; hr = tod.hour(); hr = (hr==0?12:(hr>12?hr-12:hr));
668-
switch(readEEPROM(21,false)) {
669-
case 1: //single beep via normal signal cycle
670-
if(tod.minute()==0) signalRemain = 1; break;
671-
case 2: //pips - directly play 500ms hour pip
672-
if(tod.minute()==0 && signalType==0) tone(signalPin, signalPitch, 500); break;
673-
case 3: //hour strike via normal signal cycle
674-
if(tod.minute()==0) {
675-
signalRemain = hr; break;
676-
}
677-
case 4: //ship's bell at :00 and :30 mins
678-
hr = ((hr%4)*2)+(tod.minute()==30?1:0);
679-
default: break;
680-
} //end strike type
681-
} //end striking
682669
//check if we should trigger the cleaner (at night end time, or alarm time if night end is 0:00)
683670
if(tod.hour()*60+tod.minute()==(readEEPROM(30,true)==0?readEEPROM(0,true):readEEPROM(30,true))) {
684671
cleanRemain = 11; //loop() will pick this up
685672
} //end cleaner check
673+
//
686674
}
687-
if(tod.second()==30 && fn==fnIsTime && fnSetPg==0) { //At bottom of minute, maybe show date
675+
if(tod.second()==30 && fn==fnIsTime && fnSetPg==0 && unoffRemain==0) { //At bottom of minute, maybe show date - not when unoffing
688676
if(readEEPROM(18,false)==2) { fn = fnIsDate; inputLast = pollLast; }
689677
}
690-
if(tod.minute()==59 && tod.second()>=55 && signalType<2 && readEEPROM(21,false)==2 && fn==fnIsTime && fnSetPg==0 && displayDim==2) {
691-
//In last 5 seconds of hour, maybe do pips - directly play 100ms seconds pip
692-
if(signalType==0) tone(signalPin, signalPitch, 100);
678+
679+
//Strikes - only if fn=clock, not setting, not night-off/day-off, and appropriate signal type.
680+
//Short pips before the top of the hour
681+
if(tod.minute()==59 && tod.second()>=55 && readEEPROM(21,false)==2 && signalType<2 && signalRemain==0 && snoozeRemain==0 && fn==fnIsTime && fnSetPg==0 && displayDim==2) {
682+
signalPitch = getHz(readEEPROM(41,false));
683+
tone(signalPin, signalPitch, (signalType==0?100:signalBeepDur));
693684
}
685+
//Strikes on/after the hour
686+
if(tod.second()==0 && (tod.minute()==0 || tod.minute()==30) && signalType<2 && signalRemain==0 && snoozeRemain==0 && fn==fnIsTime && fnSetPg==0 && displayDim==2){
687+
signalPitch = getHz(readEEPROM(41,false));
688+
byte hr; hr = tod.hour(); hr = (hr==0?12:(hr>12?hr-12:hr));
689+
switch(readEEPROM(21,false)) {
690+
case 1: //single beep via normal signal cycle
691+
if(tod.minute()==0) signalRemain = 1; break;
692+
case 2: //long pip
693+
tone(signalPin, signalPitch, (signalType==0?500:signalBeepDur));
694+
case 3: //hour strike via normal signal cycle
695+
if(tod.minute()==0) signalRemain = hr; break;
696+
case 4: //ship's bell at :00 and :30 mins
697+
signalRemain = ((hr%4)*2)+(tod.minute()==30?1:0); break;
698+
default: break;
699+
} //end strike type
700+
} //end strike
694701

695702
//Things to do every natural second (decrementing real-time counters)
696703
if(!force) {
@@ -724,7 +731,8 @@ void checkRTC(bool force){
724731
}
725732
} //end natural second
726733

727-
//Finally, whether natural tick or not, if we're not setting anything, update the display
734+
//Finally, whether natural tick or not, if we're not setting anything, update the display.
735+
//This also determines night-off/day-off, which is why chimes will happen if we go into off at top of hour TODO find a way to fix this
728736
if(fnSetPg==0) updateDisplay();
729737

730738
} //end if force or new second
@@ -775,6 +783,8 @@ void switchAlarm(char dir){
775783
if(dir==1) writeEEPROM(2,1,false);
776784
if(dir==-1) writeEEPROM(2,0,false);
777785
if(dir==0) writeEEPROM(2,!readEEPROM(2,false),false);
786+
snoozeRemain = 0;
787+
updateDisplay();
778788
}
779789
}
780790

@@ -801,6 +811,18 @@ word getHz(byte note){
801811
// }
802812
// }
803813

814+
bool isTimeInRange(word tstart, word tend, word ttest) {
815+
//Times are in minutes since midnight, 0-1439
816+
//if tstart < tend, ttest is in range if >= tstart AND < tend
817+
//if tstart > tend (range passes a midnight), ttest is in range if >= tstart OR < tend
818+
//if tstart == tend, no ttest is in range
819+
return ( (tstart<tend && ttest>=tstart && ttest<tend) || (tstart>tend && (ttest>=tstart || ttest<tend)) );
820+
}
821+
bool isDayInRange(byte dstart, byte dend, byte dtest) {
822+
//Similar to isTimeInRange, only the range is inclusive in both ends (always minimum 1 day match)
823+
return ( (dstart<=dend && dtest>=dstart && dtest<=dend) || (dstart>dend && (dtest>=dstart || dtest<=dend)) );
824+
}
825+
804826
////////// Display data formatting //////////
805827
void updateDisplay(){
806828
//Run as needed to update display when the value being shown on it has changed
@@ -836,17 +858,22 @@ void updateDisplay(){
836858
blankDisplay(2,5);
837859
}
838860
else { //fn running
861+
839862
//Set displayDim per night-off and day-off settings - fnIsAlarm may override this
863+
//issue: moving from off alarm to next fn briefly shows alarm in full brightness. I think because of the display delays. TODO
840864
word todmins = tod.hour()*60+tod.minute();
841-
if(unoffDur > 0) {
842-
displayDim = 2;
843-
} else if(readEEPROM(27,false) && (todmins>=readEEPROM(28,true) || todmins<(readEEPROM(30,true)==0?readEEPROM(0,true):readEEPROM(30,true)))) { //night-off
844-
displayDim = (readEEPROM(27,false)==1?1:0); //dim or off
845-
} else if(readEEPROM(32,false)==1 && toddow<readEEPROM(33,false) && toddow>readEEPROM(34,false)) { //day-off all day on weekends
846-
displayDim = 0;
847-
} else if(readEEPROM(32,false)==2 && toddow>=readEEPROM(33,false) && toddow<=readEEPROM(34,false) && todmins>=readEEPROM(35,true) && todmins<readEEPROM(37,true)) { //day-off during office hours
848-
displayDim = 0;
849-
} else displayDim = 2;
865+
//In order of precedence:
866+
//temporary unoff
867+
if(unoffRemain > 0) displayDim = 2;
868+
//day-off on weekends, all day
869+
else if( readEEPROM(32,false)==1 && !isDayInRange(readEEPROM(33,false),readEEPROM(34,false),toddow) ) displayDim = 0;
870+
//day-off on weekdays, during office hours only
871+
else if( readEEPROM(32,false)==2 && isDayInRange(readEEPROM(33,false),readEEPROM(34,false),toddow) && isTimeInRange(readEEPROM(35,true), readEEPROM(37,true), todmins) ) displayDim = 0;
872+
//night-off - if night end is 0:00, use alarm time instead
873+
else if( readEEPROM(27,false) && isTimeInRange(readEEPROM(28,true), (readEEPROM(30,true)==0?readEEPROM(0,true):readEEPROM(30,true)), todmins) ) displayDim = (readEEPROM(27,false)==1?1:0); //dim or off
874+
//normal
875+
else displayDim = 2;
876+
850877
switch(fn){
851878
case fnIsTime:
852879
byte hr; hr = tod.hour();

0 commit comments

Comments
 (0)