diff --git a/README.md b/README.md
index 7b67b22..55f6a4c 100644
--- a/README.md
+++ b/README.md
@@ -2,52 +2,64 @@

-**A digital clock with perpetual calendar, alarm, countdown timer/appliance timer, and day counter.** Written for the Arduino Nano at the heart of [RLB Designs'](http://rlb-designs.com/) Universal Nixie Driver Board (UNDB), featuring a DS3231 real-time clock, and driving up to 6 digits multiplexed in pairs via two SN74141 driver chips. Includes support for LED and relay control for UNDB v8+.
+**A digital clock for the Arduino Nano and a nixie tube display.**
-[The latest release can be downloaded here.](https://github.com/clockspot/arduino-nixie/releases/latest) Skip to [Hardware Configuration](#hardware-configuration) for details on how to tweak the sketch.
+* Features perpetual calendar with day counter/sunrise/sunset, alarm with skip/snooze, and countdown timer.
+* Supports four- or six-digit displays of Nixie tubes multiplexed in pairs via two SN74141 driver chips.
+* Supports auto DST change, tube shutoff, hourly chimes, LED lighting, piezo beeper, and switchable relay.
+* Timekeeping requires a DS3231 real-time clock via I2C, which is battery-backed and thermocompensated.
+* Written for [RLB Designs’](http://rlb-designs.com/) Universal Nixie Driver Board (UNDB), with LED control for v8+ and relay for v9+.
-## Clock Operating Instructions
+[The latest release can be downloaded here.](https://github.com/clockspot/arduino-nixie/releases/latest) Skip to [Hardware Configuration](#hardware-configuration) for details on tweaking the sketch.
-_Note: Many variations are possible, depending on your clock's hardware; but this covers most cases._
+## Operating Instructions, v1.6.0
-### Clock Functions
+The clock displays its software version when powered up (as of v1.6). [Instructions for earlier versions are here.](https://github.com/clockspot/arduino-nixie/releases)
-* Press **Select** to cycle through the clock's functions.
-* After a few seconds, the display will return to **Time**, except for the running timer and the preset function (see below).
-* To set, hold **Select** until the display flashes; use **Up/Down** to set, and **Select** to save.
+* Press **Select** to cycle through [Time of Day](#time-of-day), [Calendar](#calendar), [Alarm](#alarm), and [Countdown Timer](#countdown-timer).
+* To set anything, simply hold **Select** until the display flashes; use **Up/Down** to set, and **Select** to save. Additional settings are available in the [options menu](#options-menu).
-| Function | Looks like | Notes |
-| --- | --- | --- |
-| **Time** | `12 34 56` | The time of day. You can choose 12h or 24h format in the options menu (1). When setting, it's in 24h format (so you can tell AM from PM) and the seconds will reset to :00 when you save. The clock keeps time during power outages and compensates for temperature effects. |
-| **Date** | `_2 _4 _0`
(for Sun 2/4) | You can choose the date format in the options menu (2). Setting is done in three stages: first year, then month, then date.
Weekdays are: 0=Sun, 1=Mon, 2=Tue, 3=Wed, 4=Thu, 5=Fri, 6=Sat |
-| **Alarm** | `_7 00 1_` | Shows alarm time (always in 24hr format) and on/off status on 5th tube (1=on, 0=off) and by display brightness (bright=on, dim=off). Use **Up** to switch on, and **Down** to switch off. Hold **Select** to set time (same way as **Time**). When alarm sounds, press any button to snooze, or hold for 1sec (followed by a short beep) to silence the alarm for the day. Options menu lets you restrict the alarm to your workweek or weekend only. In a power outage, the alarm will remain set, but it will not sound if power is disconnected at alarm time. |
-| **Timer** | `__ __ _0` | A countdown timer, in hours, minutes, and seconds; or `0` when stopped. Can be set to the minute, up to 18 hours. Begins running as soon as you set it, and will continue to run in the background if you change to a different function. To cancel while running, hold **Select**. When timer runs out, press **Select** to silence. If power is lost, the timer will reset to `0`. Can be configured to work as an interval timer in the options menu (10). |
-| **Day counter** | `_1 23 __` | Shows the number of days until/since a date you specify. Set the same way as **Date.** |
-| **Temperature** | `__ 38 25` | Shows the temperature of the onboard DS3231 chip (e.g. 38.25°C – I think). May not be very useful as it tends to read higher than ambient temperature and its tolerance is low. Negative temperatures indicated with leading zeroes. |
-| **Tube tester** | `88 88 88` | (Disabled by default.) Cycles through all the digits on all the tubes. |
+### Time of Day
+
+The time of day is shown in 12h or 24h format per the [options menu](#options-menu), but when setting, it is shown in 24h so you can tell AM from PM. When exiting setting, seconds will reset to zero, unless the time was not changed.
+
+### Calendar
+
+The calendar cycles through several displays, before returning to the time of day:
+
+* **The date.** Several formats are available in the [options menu](#options-menu). When setting, it will ask for the year, then the month, then the date.
+* **Day counter.** This will count down to, or up from, a date of your choice, repeating every year. When setting, it will ask for the month, then the date, then the direction (0 = count down, 1 = count up).
+ * TIP: To display the day of the year, set it to count up from December 31.
+* **Sunrise/sunset.** These two displays show the previous and next apparent sunrise/sunset times (indicated by `1` or `0` on the seconds tubes – during the day, it shows sunrise then sunset; at night, sunset then sunrise). The times are calculated using the latitude, longitude, UTC offset, and auto DST rule specified in the [options menu](#options-menu), and shown in the same 12h/24h format as the time of day.
+ * NOTE: At this writing, the times may be incorrect by a few minutes, depending on [your longitude and time of year](https://docs.google.com/spreadsheets/d/1dYchVCJAuhvosrCdtEeHLT3ZXcLZK8X0UtENItZR32M/edit#gid=0). I believe this to be a rounding error(s) in the [Dusk2Dawn library](https://github.com/dmkishi/Dusk2Dawn) (compared to the [NOAA Solar Calculator](https://www.esrl.noaa.gov/gmd/grad/solcalc/) it’s based on) and plan to investigate.
+
+### Alarm
-### LEDs and Relay
+The alarm is always shown in 24h format so you can tell AM from PM.
-Later UNDBs (v8 with mods, or v9+) are equipped with controllable LEDs, as well as a relay, which can be enabled in the [hardware configuration](#hardware-configuration) in **switch mode** (such as for a radio) or **pulse mode** (such as for a bell striker). If you have one of these UNDBs:
+* Use **Up/Down** to switch the alarm between **on, skip, and off** (indicated by `1`/`01`/`0` on the seconds tubes, and/or high/medium/low beeps).
+* When the alarm sounds, press any button – once to snooze, and again to cancel the snooze / silence the alarm for the day (it will give a short low beep, and the display will blink once).
+* **Skip** silences the next alarm in advance – useful if you’re taking a day off, or you wake up before your alarm. In the [options menu](#options-menu), you can program the alarm to skip automatically during the work week or on weekends – and when this is active, you can also _unskip_ the next alarm by simply switching it back on. The [Alt button](#the-alt-button) can be set to toggle the skip setting.
-* The LED behavior is configurable in option 7, and can be set to switch on and off with the relay if enabled (great for a radio!)
-* The alarm, timer, and strike signals can be configured to use either the beeper or the relay if enabled (options 11, 21, and 31 – strike can only use the relay in pulse mode).
-* With the relay in switch mode:
- * **Alt** will switch it on and off at any time (except in options menu, and only if the soft power switch is enabled).
- * If the alarm is set to use the relay, it will switch on the relay at alarm time, and switch off two hours later. If **Alt** is used to switch it off, the alarm will be silenced for the day (skipping snooze).
- * If the timer is set to use the relay, it will switch on while the timer is running, and switch off when it runs out, like a clock radio's sleep function. If **Alt** is used to switch it off, the timer will be cancelled. (The interval timer option cannot be used with the relay in switch mode.)
+### Countdown Timer
-### Function Preset
+The countdown timer can be set up to 18 hours, and can be configured as an interval timer in the [options menu](#options-menu).
-If you are not using a switched relay and/or the soft power switch is not enabled, the **Alt** button acts as a function preset, to let you quickly access a function of your choice.
+* The timer will begin running as soon as you finish setting it. If you switch to another display, the timer will continue to run in the background. If power is lost, the timer will clear.
+* To cancel the running timer, hold **Select** while the timer is shown.
+* When the timer sounds, press any button to silence it.
-* Press **Alt** to jump immediately to the preset function (or back to **Time**).
-* To change the preset function, use **Select** to scroll to that function, then hold **Alt** until the display flashes and you hear two beeps.
-* Most functions return to **Time** after a few seconds, but the preset function will not. You can use this to make a function stay on display.
+### The Alt Button
+
+If your clock has an **Alt** button, it will do one of two things (depending on your [hardware configuration](#hardware-configuration)):
+
+* If your clock has a switched relay with soft power switch enabled (such as for a radio), **Alt** acts as that switch.
+* Otherwise, it works as a preset button. While viewing the display you want quick access to (such as the alarm or countdown timer), hold **Alt** until it beeps twice; then you can use **Alt** to jump straight there.
+ * TIP: If used with the alarm, the **Alt** button will also toggle the skip setting – so to skip (or unskip) the next alarm, you only need to press the **Alt** button twice: once to display it, and once to change it.
### Options Menu
-* To access this, hold **Select** for 3 seconds until you see a single `1` on the hour tubes. This indicates option number 1.
+* To enter the options menu, hold **Select** for 3 seconds until you see a single `1` on the hour tubes. This indicates option number 1.
* Use **Up/Down** to go to the option number you want to set (see table below); press **Select** to open it for setting (display will flash); use **Up/Down** to set; and **Select** to save.
* When all done, hold **Select** to exit the options menu.
@@ -55,58 +67,71 @@ If you are not using a switched relay and/or the soft power switch is not enable
| --- | --- | --- |
| | **General** | |
| 1 | Time format | 1 = 12-hour
2 = 24-hour
(time-of-day display only; setting times is always done in 24h) |
-| 2 | Date format | 1 = month/date/weekday
2 = date/month/weekday
3 = month/date/year
4 = date/month/year
5 = year/month/date
Note that four-tube clocks will display only the first two values in each of these options. |
-| 3 | Display date during time? | 0 = never
1 = date instead of seconds
2 = full date (as above) every minute at :30 seconds
3 = same as 2, but scrolls in and out |
+| 2 | Date format | 1 = month/date/weekday
2 = date/month/weekday
3 = month/date/year
4 = date/month/year
5 = year/month/date
The weekday is displayed as a number from 0 (Sunday) to 6 (Saturday).
Four-tube clocks will display only the first two values in each of these options. |
+| 3 | Display date during time? | 0 = never
1 = date instead of seconds
2 = full date each minute at :30 seconds
3 = same as 2, but scrolls in and out |
| 4 | Leading zero in hour, date, and month? | 0 = no
1 = yes |
| 5 | Digit fade | 0–20 (in hundredths of a second) |
-| 6 | Auto DST | Add 1h for daylight saving time between these dates (at 2am):
0 = off
1 = second Sunday in March to first Sunday in November (US/CA)
2 = last Sunday in March to last Sunday in October (UK/EU)
3 = first Sunday in April to last Sunday in October (MX)
4 = last Sunday in September to first Sunday in April (NZ)
5 = first Sunday in October to first Sunday in April (AU)
6 = third Sunday in October to third Sunday in February (BZ) |
-| 7 | LED behavior | 0 = always off
1 = always on
2 = on, but follow night/away modes if enabled
3 = off, but on when alarm/timer sounds4 = off, but on with switched relay (if equipped)
(Clocks with LED control only, UNDB v8+) |
-| 8 | Anti-cathode poisoning | Briefly cycles all digits to prevent [cathode poisoning](http://www.tube-tester.com/sites/nixie/different/cathode%20poisoning/cathode-poisoning.htm)
0 = once a day, either at midnight or at night mode start time (if enabled)
1 = at the top of every hour
2 = at the top of every minute
(Will not trigger during night/away modes) |
-| 9 | Temperature format | 0 = Celsius
1 = Fahrenheit
(Clocks with temperature function enabled only) |
+| 6 | Auto DST | Add 1h for daylight saving time between these dates (at 2am):
0 = off
1 = second Sunday in March to first Sunday in November (US/CA)
2 = last Sunday in March to last Sunday in October (UK/EU)
3 = first Sunday in April to last Sunday in October (MX)
4 = last Sunday in September to first Sunday in April (NZ)
5 = first Sunday in October to first Sunday in April (AU)
6 = third Sunday in October to third Sunday in February (BZ)
If the clock is not powered at the time, it will correct itself when powered up. |
+| 7 | LED behavior | 0 = always off
1 = always on
2 = on, but follow night/away shutoff if enabled
3 = off, but on when alarm/timer sounds4 = off, but on with switched relay (if equipped)
(Clocks with LED lighting only) |
+| 8 | Anti-cathode poisoning | Briefly cycles all digits to prevent [cathode poisoning](http://www.tube-tester.com/sites/nixie/different/cathode%20poisoning/cathode-poisoning.htm)
0 = once a day, either at midnight or when night shutoff starts (if enabled)
1 = at the top of every hour
2 = at the top of every minute
(Will not trigger during night/away shutoff) |
| | **Alarm** | |
-| 10 | Alarm days | 0 = every day
1 = work week only (per settings below)
2 = weekend only |
+| 10 | Alarm auto-skip | 0 = alarm triggers every day
1 = work week only, skipping weekends (per settings below)
2 = weekend only, skipping work week |
| 11 | Alarm signal | 0 = beeper
1 = relay (if in switch mode, will stay on for 2 hours)
(Clocks with both beeper and relay only) |
-| 12 | Alarm beeper 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!
(Clocks with beeper only) |
+| 12 | Alarm beeper pitch | [Note number](https://en.wikipedia.org/wiki/Piano_key_frequencies), from 49 (A4) to 88 (C8).
(Clocks with beeper only) |
| 13 | Alarm snooze | 0–60 minutes. 0 disables snooze. |
| | **Timer** | |
| 20 | Timer interval mode | 0 = count down and stop
1 = count down and restart (interval mode)
(Clocks with beeper and/or pulse relay only) |
| 21 | Timer signal | 0 = beeper
1 = relay (if in switch mode, will stay on until timer runs down)(Clocks with both beeper and relay only) |
-| 22 | Timer beeper pitch | Set the same way as the alarm pitch, above
(Clocks with beeper only) |
-| | **Strike** | |
-| 30 | Strike | Make noise on the hour:
0 = off
1 = single beep
2 = pips
3 = strike the hour (1 to 12)
4 = ship's bell (hour and half hour)
Will not sound during night/away modes (except when off starts at top of hour)
(Clocks with beeper or pulse relay only) |
-| 31 | Strike signal | 0 = beeper
1 = relay
(Clocks with both beeper and pulse relay only) |
-| 32 | Strike beeper 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).
(Clocks with beeper only) |
-| | **Night mode and away mode** | |
-| 40 | Night mode | To save tube life and/or preserve your sleep, dim or shut off tubes nightly when you're not around or sleeping.
0 = none (tubes fully on at night)
1 = dim tubes at night
2 = shut off tubes at night
When off, you can press **Select** to illuminate the tubes briefly. |
+| 22 | Timer beeper pitch | [Note number](https://en.wikipedia.org/wiki/Piano_key_frequencies), from 49 (A4) to 88 (C8).
(Clocks with beeper only) |
+| | **Chime** | |
+| 30 | Chime | Make noise on the hour:
0 = off
1 = single beep
2 = pips
3 = Chime the hour (1 to 12)
4 = ship’s bell (hour and half hour)
Will not sound during night/away shutoff (except when off starts at top of hour)
(Clocks with beeper or pulse relay only) |
+| 31 | Chime signal | 0 = beeper
1 = relay
(Clocks with both beeper and pulse relay only) |
+| 32 | Chime beeper pitch | [Note number](https://en.wikipedia.org/wiki/Piano_key_frequencies), from 49 (A4) to 88 (C8).
TIP: If using pips, 63 (987Hz) is closest to the real BBC pips frequency (1000Hz).
(Clocks with beeper only) |
+| | **Night/away shutoff** | |
+| 40 | Night shutoff | To save tube life and/or preserve your sleep, dim or shut off tubes nightly when you’re not around or sleeping.
0 = none (tubes fully on at night)
1 = dim tubes at night
2 = shut off tubes at night
When off, you can press **Select** to illuminate the tubes briefly. |
| 41 | Night starts at | Time of day. |
| 42 | Night ends at | Time of day. Set to 0:00 to use the alarm time. |
-| 43 | Away mode | To further save tube life, shut off tubes during the day when you're not around.
0 = none (tubes fully on during the day)
1 = clock at work (shut off all day on weekends)
2 = clock at home (shut off during work hours)
When off, you can press **Select** to illuminuate the tubes briefly. |
+| 43 | Away shutoff | To further save tube life, shut off tubes during daytime hours when you’re not around. This feature is designed to accommodate your work schedule.
0 = none (tubes on all day every day, except for night shutoff)
1 = clock at work (shut off all day on weekends)
2 = clock at home (shut off during work hours only)
When off, you can press **Select** to illuminate the tubes briefly. |
| 44 | First day of work week | 0–6 (Sunday–Saturday) |
| 45 | Last day of work week | 0–6 (Sunday–Saturday) |
| 46 | Work starts at | Time of day. |
| 47 | Work ends at | Time of day. |
+| | **Geography** | |
+| 50 | Latitude | Your latitude, in tenths of a degree; negative (south) values are indicated with leading zeroes. (Example: Dallas is at 32.8°N, set as `328`.) |
+| 51 | Longitude | Your longitude, in tenths of a degree; negative (west) values are indicated with leading zeroes. (Example: Dallas is at 96.7°W, set as `00967`.) |
+| 52 | UTC offset | Your time zone’s offset from UTC (non-DST), in hours and minutes; negative (west) values are indicated with leading zeroes. (Example: Dallas is UTC–6, set as `0600`.) |
-To reset the options menu settings to "factory" defaults, hold **Select** while connecting the clock to power.
+To reset the clock to “factory” defaults, hold **Select** while powering up the clock.
## Hardware Configuration
-A number of hardware-related settings are specified in config files, one of which is included at the top of the sketch (so you can easily maintain multiple clocks with different hardware). These settings include:
+A number of hardware-related settings are specified in config files, so you can easily maintain multiple clocks with different hardware, by including the correct config file at the top of the sketch before compiling.
+
+These settings include:
-* **How many tubes** in the display module. Default is 6; small display adjustments are made for 4-tube clocks.
-* **Which functions** are enabled. Default is all but temperature and tube tester.
-* **Which pins** are associated with the inputs (controls) and outputs (LED, relay, anodes, cathodes).
-* **What type of Up/Down controls** are equipped: pushbuttons (default) or rotary encoder (unimplemented).
+* **Number of digits** in the display module. Default is 6; small display adjustments are made for 4-tube clocks.
+* **Which functions** are enabled (calendar, alarm, etc).
+* **Which pins** are associated with the inputs (controls) and outputs (displays and signals).
+ * If your clock includes LED backlighting (e.g. UNDB v8+), specifying an LED pin will enable the LED-related options in the options menu. LEDs should be connected to a PWM pin.
+* **What type of Up/Down controls** are equipped: pushbuttons (default) or rotary encoder (TBD).
* **What type of signal outputs** are equipped: a piezo beeper (default) and/or a relay.
* **Signal duration** (default 3min) and **piezo pulse duration** (default 500ms)
- * If relay is equipped, **relay mode**:
- * In switched mode (default), the relay will be switched to control an appliance like a radio or light fixture. If used with timer, it will switch on while timer is running (like a "sleep" function). If used with alarm, it will switch on when alarm trips; specify **relay switch duration** (default 2 hours).
+ * If your clock includes a relay (e.g. UNDB v9+), specifying a relay pin will enable the relay-related options in the options menu. You can also specify the **relay mode**:
+ * In switched mode (default), the relay will be switched to control an appliance like a radio or lamp. If used with timer, it will switch on while timer is running (like the “sleep” function on a clock radio). If used with alarm, it will switch on when alarm trips and stay on for **relay switch duration** (default 2 hours). In this case, the **Alt** button (if equipped) will shut it off immediately, skipping snooze. This mode also enables the option for the LED backlighting, if equipped, to switch with the relay (great for a radio!).
* In pulse mode, the relay will be pulsed, like the beeper is, to control an intermittent signaling device like a solenoid or indicator lamp; specify **relay pulse duration** (default 200ms).
-* **Soft alarm switch** enabled: default is yes; it is switched with **Up** (on) and **Down** (off) while viewing the alarm time. Change to no if the signal output/appliance has its own switch on this relay circuit; the clock's alarm will be permanently on.
+* **Soft alarm switch** enabled: default is yes; it is switched with **Up** (on) and **Down** (off) while viewing the alarm time. Change to no if the signal output/appliance has its own switch on this relay circuit; the software alarm will be permanently on.
* **Soft power switch** enabled (switched relay only): default is yes; appliance can be toggled on/off with **Alt**. Change to no if the appliance has its own power switch (independent of this relay circuit) or does not need to be manually switched. (If set to no, or not using a switched relay, **Alt** acts as a function preset, as above.)
-* **Various other durations** for things like scrolling speed, set mode timeouts, short and long button holds, "hold to set faster" thresholds, etc.
+* **Various other durations** for things like scrolling speed, set function/display timeouts, short and long button holds, “hold to set faster” thresholds, etc.
+
+You can also set the **defaults for the options menu** (in main code, currently) to better suit the clock’s intended use.
+
+### Compiling the sketch
-You can also set the **defaults for the options menu** (in main code, currently) to better suit the clock's intended use.
+**To compile the sketch,** ensure these libraries are added and enabled in your Arduino IDE, via the Library Manager:
-**To compile the edited sketch:** You will need to add the [NorthernWidget DS3231](https://github.com/NorthernWidget/DS3231) libraries to your Arduino IDE.
+* Wire (Arduino built-in)
+* EEPROM (Arduino built-in)
+* DS3231 ([NorthernWidget](https://github.com/NorthernWidget/DS3231))
+* Dusk2Dawn ([dmkishi](https://github.com/dmkishi/Dusk2Dawn))
-**To upload the sketch to the UNDB:** if it doesn't appear in the IDE's Ports menu (as a USB port), your UNDB may be equipped with an Arduino clone that requires [drivers for the CH340 chipset](https://sparks.gogo.co.nz/ch340.html).
\ No newline at end of file
+**To upload the sketch to your clock,** if it doesn’t appear in the IDE’s Ports menu (as a USB port), your UNDB may be equipped with an Arduino clone that requires [drivers for the CH340 chipset](https://sparks.gogo.co.nz/ch340.html).
\ No newline at end of file
diff --git a/TODO.md b/TODO.md
index 023775d..0750e8e 100644
--- a/TODO.md
+++ b/TODO.md
@@ -2,14 +2,20 @@
* Timer count up, ending options - maybe separate chrono and timer à la Timex?
* Different setting option for pushbutton (à la Timex) vs. rotary (à la microwave ovens) - external file?
-* Set current DST state in EEPROM so when the clock is connected to power, it can determine whether correction is needed
* Option to display weekdays as Sun=0 or Sun=1 (per Portuguese!)
* When setting times of day, make 1439 (minutes) roll over to 0 and vice versa
* Implement options for full date every 5 minutes
-* Is it possible to trip the chime *after* determining if we're in night mode or not
+* Is it possible to trip the chime *after* determining if we're in night shutoff or not
* Reenable rotary encoder with libraries with workable licenses
* In display code, consider using `delayMicroseconds()` which, with its tighter resolution, may give better control over fades and dim levels
* in `checkInputs()`, can all this if/else business be defined at load instead of evaluated every sample? OR is it compiled that way? maybe use `#ifdef`
* in `ctrlEvt()`, could we do release/shorthold on mainSel so we can exit without making changes?
+* Should functions be modular in the code, and have a reserved memory location / 100 per each?
+* Should functions have their own options menu?
+* I2C display to help with setting?
+* I2C multicolor LED to indicate which function we're in?
+* Metronome function
+* Alarm option should be beeping patterns, including a slow wake which defeats the 2 minute delay
+* Signalstart should create a situation where there's time on the counter, but doesn't make sound since the rtc can do that. Other beepable actions would probably cancel that counter anyway
See other TODOs throughout code.
\ No newline at end of file
diff --git a/arduino-nixie/Song.h b/arduino-nixie/Song.h
new file mode 100644
index 0000000..dadf03c
--- /dev/null
+++ b/arduino-nixie/Song.h
@@ -0,0 +1,58 @@
+//Try to make the beeper play Woody's chorus part from "1612"
+class Song {
+ int bpm = 80;
+ int pin = 10; //that piezo is connected to
+ int dur = 10000; //max note length in millis
+ //This'll be like a proto-MIDI with events that represent sounds (notes, chords) or to stop sound.
+ //Need to represent chords as a single event since the beeper is mono and has to cycle through the notes.
+ //Event timing is in centibeats (e.g. in */4, quarters are 100, eights 50, sixteenths 25)
+ //First index is a placeholder. eventtimes[] has an extra time, at which it loops back to index 1.
+ int eventtimes[30] = {0, 0, 15, 75, 90,100,115,175,190,225,240,275,290,300,315,400,415,475,490,500,515,575,590,625,640,675,690,700,715,800};
+ int eventtypes[29] = {0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 2, 0, 2, 0, 2, 0, 2, 0, 2, 0, 2, 0, 2, 0};
+ int curevent = 0; //Current event (index in arrays above)
+ int cureventnote = 0; //Current note in the event (to support cycling through a chord). 1 for single note. 0 when not playing a note presently.
+ float transpose = 1;
+ unsigned long eventstart = 0; //0 when not playing a song presently.
+ //we will only keep track of time since last event, thus bpm may vary due to rounding errors, but millis() rollover will only cause one event skip (I think?).
+public:
+ Song(int zero) {}
+ void play(){
+ check(true); //the true means this starts the song
+ } //end void play
+ void check(bool start){ if(start || curevent){ //if song starting or playing
+ unsigned long mils = millis();
+ //Are we moving to a new event?
+ int neweventdiff = (eventtimes[curevent+1]-eventtimes[curevent])*(600/bpm); //*(600/bpm) converts diff in centibeats to milliseconds
+ if(start || mils-eventstart >= neweventdiff){
+ if(start) eventstart = mils; //true start of event
+ else eventstart = eventstart + neweventdiff; //set start of event relative to start of last, for better timekeeping
+ curevent++; //Next event
+ if(curevent+1 == 30){ curevent=1; } //Have we looped? Go back to first event //eventtimes.size
+ }
+ //Should we do anything about the current event?
+ int neweventnote = (((mils-eventstart)%120)/40)+1; //a cycle of 3 every 120 ms (40ms/ea), based on current time
+ switch(eventtypes[curevent]){ //The type of note/chord/stop
+ case 1: //C Major chord, cycle of 523.25, 659.25, 784 Hz
+ if(cureventnote!=neweventnote) {
+ cureventnote = neweventnote;
+ tone(pin, (cureventnote==1? 523.25*transpose: (cureventnote==2? 659.25*transpose: 784*transpose)), dur);
+ }
+ break;
+ case 2: //F Major chord, cycle of 523.25, 698.45, 880 Hz
+ if(cureventnote!=neweventnote) {
+ cureventnote = neweventnote;
+ tone(pin, (cureventnote==1? 523.25*transpose: (cureventnote==2? 698.45*transpose: 880*transpose)), dur);
+ }
+ break;
+ case 0: //Stop - turn off sound, if playing
+ if(cureventnote!=0){ cureventnote=0; noTone(pin); } break;
+ default: break;
+ }
+ }} //end void check if song playing
+ void stop(){
+ noTone(pin);
+ curevent = 0;
+ cureventnote = 0;
+ eventstart = 0;
+ } //end void stop
+}; //end class Song
\ No newline at end of file
diff --git a/sixtube_lm/sixtube_lm.ino b/arduino-nixie/arduino-nixie.ino
similarity index 63%
rename from sixtube_lm/sixtube_lm.ino
rename to arduino-nixie/arduino-nixie.ino
index d58a020..aa2cc51 100644
--- a/sixtube_lm/sixtube_lm.ino
+++ b/arduino-nixie/arduino-nixie.ino
@@ -7,29 +7,47 @@
////////// Hardware configuration //////////
//Include the config file that matches your hardware setup. If needed, duplicate an existing one.
-#include "configs/v8c-6tube-relayswitch-pwm-top.h"
+#include "configs/v8c-6tube.h"
+////////// Software version //////////
+const byte vMajor = 1;
+const byte vMinor = 6;
+const byte vPatch = 0;
////////// Other includes, global consts, and vars //////////
+#include //Arduino - GNU LPGL
#include //Arduino - GNU LPGL
-#include //Arduino - GNU LPGL
-#include //NorthernWidget - The Unlicense
+#include //NorthernWidget - The Unlicense
+#include //TODO not needed unless whatever
/*EEPROM locations for non-volatile clock settings
Don't change which location is associated with which setting; they should remain permanent to avoid the need for EEPROM initializations after code upgrades; and they are used directly in code.
-Most values in the clock are 1-byte; if 2-byte, high byte is loc, low byte is loc+1.
+Setting values are char (1-byte unsigned, 0 to 255) or int (2-byte signed, -32768 to 32767) where high byte is loc and low byte is loc+1.
+In updateDisplay(), special setting formats are deduced from the option max value (max 1439 is a time of day, max 156 is a UTC offset, etc)
+and whether a value is an int or char is deduced from whether the max value > 255.
+TODO consider having volatile values for all these things, using EEPROM just to recover after a power loss - so any degradation of EEPROM would not be noticeable during running. This can be integrated into a storage module accommodating flash memory in the Arduino IoT
These ones are set outside the options menu (defaults defined in initEEPROM()):
0-1 Alarm time, mins
- 2 Alarm on
- 3-4 Day count year
+ 2 Alarm on (mirrors volatile var)
+ 3 [free]
+ 4 Day count direction
5 Day count month
6 Day count date
7 Function preset (done by Alt when not power-switching)
-( 8-15 are available )
+ 8 Functions/pages enabled (bitmask, dynamic) TODO
+ const unsigned int FN_TIMER = 1<<0; //1
+ const unsigned int FN_DAYCOUNT = 1<<1; //2
+ const unsigned int FN_SUN = 1<<2; //4
+ const unsigned int FN_WEATHER = 1<<3; //8
+ 9 [free]
+ 15 DST on (mirrors volatile var)
These ones are set inside the options menu (defaults defined in arrays below).
Some are skipped when they wouldn't apply to a given clock's hardware config, see fnOptScroll(); these ones will also be set at startup to the start= values, see setup(). Otherwise, make sure these ones' defaults work for all configs.
+ 10-11 Latitude
+ 12-13 Longitude
+ 14 UTC offset in quarter-hours plus 100 - range is 52 (-12h or -48qh, US Minor Outlying Islands) to 156 (+14h or +56qh, Kiribati)
16 Time format
17 Date format
18 Display date during time
@@ -41,10 +59,10 @@ Some are skipped when they wouldn't apply to a given clock's hardware config, se
24 Alarm snooze
25 Timer interval mode - skipped when no piezo and relay is switch (start=0)
26 LED circuit behavior - skipped when no led pin
- 27 Night mode
+ 27 Night shutoff
28-29 Night start, mins
30-31 Night end, mins
- 32 Away mode
+ 32 Away shutoff
33 First day of workweek
34 Last day of workweek
35-36 Work starts at, mins
@@ -62,12 +80,12 @@ Some are skipped when they wouldn't apply to a given clock's hardware config, se
//Options menu numbers (displayed in UI and readme), EEPROM locations, and default/min/max values.
//Option numbers/order can be changed (though try to avoid for user convenience);
//but option locs should be maintained so EEPROM doesn't have to be reset after an upgrade.
-// General Alarm Timer Strike Night and away mode
-const byte optsNum[] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10,11,12,13, 20,21,22, 30,31,32, 40, 41, 42,43,44,45, 46, 47};
-const byte optsLoc[] = {16,17,18,19,20,22,26,46,45, 23,42,39,24, 25,43,40, 21,44,41, 27, 28, 30,32,33,34, 35, 37};
-const word optsDef[] = { 2, 1, 0, 0, 5, 0, 1, 0, 0, 0, 0,61, 9, 0, 0,61, 0, 0,61, 0,1320, 360, 0, 1, 5, 480,1020};
-const word optsMin[] = { 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0,49, 0, 0, 0,49, 0, 0,49, 0, 0, 0, 0, 0, 0, 0, 0};
-const word optsMax[] = { 2, 5, 3, 1,20, 6, 4, 2, 1, 2, 1,88,60, 1, 1,88, 4, 1,88, 2,1439,1439, 2, 6, 6,1439,1439};
+// General Alarm Timer Strike Night and away shutoff Lat Long UTC±*
+const byte optsNum[] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10,11,12,13, 20,21,22, 30,31,32, 40, 41, 42,43,44,45, 46, 47, 50, 51, 52};
+const byte optsLoc[] = {16,17,18,19,20,22,26,46,45, 23,42,39,24, 25,43,40, 21,44,41, 27, 28, 30,32,33,34, 35, 37, 10, 12, 14};
+const int optsDef[] = { 2, 1, 0, 0, 5, 0, 1, 0, 0, 0, 0,61, 9, 0, 0,61, 0, 0,61, 0,1320, 360, 0, 1, 5, 480,1020, 0, 0,100};
+const int optsMin[] = { 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0,49, 0, 0, 0,49, 0, 0,49, 0, 0, 0, 0, 0, 0, 0, 0, -900,-1800, 52};
+const int optsMax[] = { 2, 5, 3, 1,20, 6, 4, 2, 1, 2, 1,88,60, 1, 1,88, 4, 1,88, 2,1439,1439, 2, 6, 6,1439,1439, 900, 1800,156};
//RTC objects
DS3231 ds3231; //an object to access the ds3231 specifically (temp, etc)
@@ -81,27 +99,42 @@ byte btnCurHeld = 0; //Button hold thresholds: 0=none, 1=unused, 2=short, 3=long
unsigned long inputLast = 0; //When a button was last pressed
unsigned long inputLast2 = 0; //Second-to-last of above
//TODO the math between these two may fail very rarely due to millis() rolling over while setting. Need to find a fix. I think it only applies to the rotary encoder though.
+int inputLastTODMins = 0; //time of day, in minutes past midnight, when button was pressed. Used in paginated functions so they all reflect the same TOD.
const byte fnOpts = 201; //fn values from here to 255 correspond to options in the options menu
byte fn = fnIsTime; //currently displayed fn (per fnsEnabled)
+byte fnPg = 0; //allows a function to have multiple pages
byte fnSetPg = 0; //whether this function is currently being set, and which option/page it's on
-word fnSetVal; //the value currently being set, if any - unsigned int 0-65535
-word fnSetValMin; //min possible - unsigned int
-word fnSetValMax; //max possible - unsigned int
+ int fnSetVal; //the value currently being set, if any
+ int fnSetValMin; //min possible
+ int fnSetValMax; //max possible
bool fnSetValVel; //whether it supports velocity setting (if max-min > 30)
-word fnSetValDate[3]; //holder for newly set date, so we can set it in 3 stages but set the RTC only once
+ int fnSetValDate[3]; //holder for newly set date, so we can set it in 3 stages but set the RTC only once
+bool fnSetValDid; //false when starting a set; true when value is changed - to detect if value was not changed
+
+//the calendar function page numbers, depending on which ones are enabled. See findFnAndPageNumbers
+byte fnDatePages = 1;
+byte fnDateCounter = 255;
+byte fnDateSunlast = 255;
+byte fnDateWeathernow = 255;
+byte fnDateSunnext = 255;
+byte fnDateWeathernext = 255;
// Volatile running values used throughout the code. (Others are defined right above the method that uses them)
+bool dstOn = 0; //this is stored in EEPROM too, but only for correction after power loss – this var hedges against EEPROM failure in normal use
+bool alarmOn = 0; //this is stored in EEPROM too, but only for power loss recovery – this var hedges against EEPROM failure in normal use
+bool alarmSkip = 0;
byte signalSource = 0;
word signalRemain = 0; //alarm/timer signal timeout counter, seconds
word snoozeRemain = 0; //snooze timeout counter, seconds
word timerInitial = 0; //timer original setting, seconds - up to 18 hours (64,800 seconds - fits just inside a word)
word timerRemain = 0; //timer actual counter
unsigned long signalPulseStartTime = 0; //to keep track of individual pulses so we can stop them
-word unoffRemain = 0; //un-off (briefly turn on tubes during full night or away modes) timeout counter, seconds
+word unoffRemain = 0; //un-off (briefly turn on tubes during full night/away shutoff) timeout counter, seconds
byte displayDim = 2; //dim per display or function: 2=normal, 1=dim, 0=off
-byte cleanRemain = 11; //anti-cathode-poisoning clean timeout counter, increments at cleanSpeed ms (see loop()). Start at 11 to run at clock startup
+byte cleanRemain = 0; //anti-cathode-poisoning clean timeout counter, increments at cleanSpeed ms (see loop()). Start at 11 to run at clock startup
char scrollRemain = 0; //"frames" of scroll – 0=not scrolling, >0=coming in, <0=going out, -128=scroll out at next change
+byte versionRemain = 3; //display version at start
////////// Main code control //////////
@@ -126,8 +159,11 @@ void setup(){
writeEEPROM(21,0,false); //turn off strike
writeEEPROM(25,0,false); //turn off timer interval mode
}
- if(!enableSoftAlarmSwitch) writeEEPROM(2,1,false); //force alarm on if software switch is disabled
+ if(!enableSoftAlarmSwitch) alarmOn = 1; //force alarm on if software switch is disabled
+ else alarmOn = (readEEPROM(2,false)>0); //otherwise set alarm per EEPROM backup
+ dstOn = (readEEPROM(15,false)>0); //set last known DST state per EEPROM backup
//if LED circuit is not switched (v5.0 board), the LED menu setting (eeprom 26) doesn't matter
+ findFnAndPageNumbers(); //initial values
initOutputs(); //depends on some EEPROM settings
}
@@ -139,6 +175,7 @@ void loop(){
if(cleanRemain && (unsigned long)(now-pollCleanLast)>=cleanSpeed) { //account for rollover
pollCleanLast=now;
cleanRemain--;
+ if(cleanRemain<1) calcSun(tod.year(),tod.month(),tod.day()); //take this opportunity to perform a calculation that blanks the display for a bit
updateDisplay();
}
//If we're scrolling an animation, advance it every scrollSpeed ms.
@@ -154,7 +191,7 @@ void loop(){
//Every loop cycle, check the RTC and inputs (previously polled, but works fine without and less flicker)
checkRTC(false); //if clock has ticked, decrement timer if running, and updateDisplay
checkInputs(); //if inputs have changed, this will do things + updateDisplay as needed
- doSetHold(); //if inputs have been held, this will do more things + updateDisplay as needed
+ doSetHold(false); //if inputs have been held, this will do more things + updateDisplay as needed
cycleDisplay(); //keeps the display hardware multiplexing cycle going
cycleLEDs();
}
@@ -193,7 +230,7 @@ void checkBtn(byte btn){
unsigned long now = millis();
//If the button has just been pressed, and no other buttons are in use...
if(btnCur==0 && bnow==LOW) {
- btnCur = btn; btnCurHeld = 0; inputLast2 = inputLast; inputLast = now;
+ btnCur = btn; btnCurHeld = 0; inputLast2 = inputLast; inputLast = now; inputLastTODMins = tod.hour()*60+tod.minute();
ctrlEvt(btn,1); //hey, the button has been pressed
}
//If the button is being held...
@@ -227,36 +264,32 @@ void checkRot(){
////////// Input handling and value setting //////////
-bool stoppingSignal = false; //Special stuff (snooze canceling) happens right after a press that silences the signal
void ctrlEvt(byte ctrl, byte evt){
//Handle control events (from checkBtn or checkRot), based on current fn and set state.
//evt: 1=press, 2=short hold, 3=long hold, 0=release.
//We only handle press evts for adj ctrls, as that's the only evt encoders generate.
//But we can handle short and long holds and releases for the sel ctrls (always buttons).
-
- //Before all else, is it a press to stop the signal? Silence it
+
+ //If the signal is going, any press should silence it
if(signalRemain>0 && evt==1){
- stoppingSignal = true;
signalStop();
if(signalSource==fnIsAlarm) { //If this was the alarm
//If the alarm is using the switched relay and this is the Alt button, don't set the snooze
if(relayMode==0 && readEEPROM(42,false)==1 && altSel!=0 && ctrl==altSel) {
- quickBeep(); //Short signal to indicate the alarm has been silenced until tomorrow
+ quickBeep(64); //Short signal to indicate the alarm has been silenced until tomorrow
delay(250); //to flash the display to indicate this as well
} else { //start snooze
snoozeRemain = readEEPROM(24,false)*60; //snoozeRemain is seconds, but snooze duration is minutes
}
}
+ btnStop();
return;
}
- //After pressing to silence, short hold cancels a snooze; ignore other btn evts
- if(stoppingSignal){
- stoppingSignal = false;
- if(evt==2 && snoozeRemain>0) {
- snoozeRemain = 0;
- quickBeep(); //Short signal to indicate the alarm has been silenced until tomorrow
- delay(250); //to flash the display
- }
+ //If the snooze is going, any press should cancel it, with a signal
+ if(snoozeRemain>0 && evt==1){
+ snoozeRemain = 0;
+ quickBeep(64); //Short signal to indicate the alarm has been silenced until tomorrow
+ delay(250); //to flash the display
btnStop();
return;
}
@@ -283,15 +316,22 @@ void ctrlEvt(byte ctrl, byte evt){
switch(fn){
case fnIsTime: //set mins
startSet((tod.hour()*60)+tod.minute(),0,1439,1); break;
- case fnIsDate: //set year
- fnSetValDate[1]=tod.month(), fnSetValDate[2]=tod.day(); startSet(tod.year(),0,9999,1); break;
+ case fnIsDate: //depends what page we're on
+ if(fnPg==0){ //regular date display: set year
+ fnSetValDate[1]=tod.month(), fnSetValDate[2]=tod.day(); startSet(tod.year(),0,9999,1);
+ } else if(fnPg==fnDateCounter){ //month, date, direction
+ startSet(readEEPROM(5,false),1,12,1);
+ } else if(fnPg==fnDateSunlast || fnPg==fnDateSunnext){ //lat and long
+ //TODO
+ } else if(fnPg==fnDateWeathernow || fnDateWeathernext){ //temperature units??
+ //TODO
+ } break;
case fnIsAlarm: //set mins
startSet(readEEPROM(0,true),0,1439,1); break;
case fnIsTimer: //set mins, up to 18 hours (64,800 seconds - fits just inside a word)
if(timerRemain>0) { timerRemain = 0; btnStop(); updateDisplay(); break; } //If the timer is running, zero it out.
startSet(timerInitial/60,0,1080,1); break;
- case fnIsDayCount: //set year like date, but from eeprom like startOpt
- startSet(readEEPROM(3,true),2000,9999,1); break;
+ //fnIsDayCount removed in favor of paginated calendar
case fnIsTemp: //could do calibration here if so inclined
case fnIsTubeTester:
default: break;
@@ -302,6 +342,7 @@ void ctrlEvt(byte ctrl, byte evt){
//we can't handle sel press here because, if attempting to enter setting mode, it would switch the fn first
if(ctrl==mainSel){
fnScroll(1); //Go to next fn in the cycle
+ fnPg = 0; //reset page counter in case we were in a paged display
checkRTC(true); //updates display
}
else if(ctrl==mainAdjUp || ctrl==mainAdjDn) {
@@ -310,7 +351,6 @@ void ctrlEvt(byte ctrl, byte evt){
}
}
else if(altSel>0 && ctrl==altSel) { //alt sel press
- //TODO switch I2C radio
//if switched relay, and soft switch enabled, we'll switch power.
if(enableSoftPowerSwitch && relayPin>=0 && relayMode==0) { switchPower(0); btnStop(); }
//Otherwise, this becomes our function preset.
@@ -321,15 +361,20 @@ void ctrlEvt(byte ctrl, byte evt){
if(readEEPROM(7,false)!=fn) {
btnStop();
writeEEPROM(7,fn,false);
- quickBeep(); //beep 1
+ quickBeep(76); //beep 1
delay(250); //to flash the display and delay beep 2
- quickBeep(); //beep 2
+ quickBeep(76); //beep 2
}
}
- //On short release, toggle between fnIsTime and the preset fn.
+ //On short release, jump to the preset fn.
else if(evt==0) {
btnStop();
- if(fn==readEEPROM(7,false)) fn=fnIsTime; else fn=readEEPROM(7,false);
+ if(fn!=readEEPROM(7,false)) fn=readEEPROM(7,false);
+ else {
+ //Special case: if this is the alarm and it's on, toggle the skip state
+ if(fn==fnIsAlarm && alarmOn) switchAlarm(alarmSkip);
+ }
+ fnPg = 0; //reset page counter in case we were in a paged display
updateDisplay();
}
}
@@ -337,7 +382,7 @@ void ctrlEvt(byte ctrl, byte evt){
} //end fn running
else { //fn setting
- if(evt==1) { //we respond only to press evts during fn setting
+ if(evt==1) { //press
//TODO could we do release/shorthold on mainSel so we can exit without making changes?
//currently no, because we don't btnStop() when short hold goes into fn setting, in case long hold may go to options menu
//so we can't handle a release because it would immediately save if releasing from the short hold.
@@ -348,28 +393,57 @@ void ctrlEvt(byte ctrl, byte evt){
//con: potential for very rare clock rollover while setting; pro: can set date separate from time
switch(fn){
case fnIsTime: //save in RTC
- ds3231.setHour(fnSetVal/60);
- ds3231.setMinute(fnSetVal%60);
- ds3231.setSecond(0);
+ if(fnSetValDid){ //but only if the value was actually changed
+ ds3231.setHour(fnSetVal/60);
+ ds3231.setMinute(fnSetVal%60);
+ ds3231.setSecond(0);
+ calcSun(tod.year(),tod.month(),tod.day());
+ isDSTByHour(tod.year(),tod.month(),tod.day(),fnSetVal/60,true);
+ }
clearSet(); break;
- case fnIsDate: //save in RTC
- switch(fnSetPg){
- case 1: //save year, set month
- delay(300); //blink display to indicate save. Safe b/c we've btnStopped. See below for why
- fnSetValDate[0]=fnSetVal;
- startSet(fnSetValDate[1],1,12,2); break;
- case 2: //save month, set date
- delay(300); //blink display to indicate save. Needed if set month == date: without blink, nothing changes.
- fnSetValDate[1]=fnSetVal;
- startSet(fnSetValDate[2],1,daysInMonth(fnSetValDate[0],fnSetValDate[1]),3); break;
- case 3: //write year/month/date to RTC
- ds3231.setYear(fnSetValDate[0]%100); //TODO: do we store century on our end? Per ds3231 docs, "The century bit (bit 7 of the month register) is toggled when the years register overflows from 99 to 00."
- ds3231.setMonth(fnSetValDate[1]);
- ds3231.setDate(fnSetVal);
- ds3231.setDoW(dayOfWeek(fnSetValDate[0],fnSetValDate[1],fnSetVal)+1); //ds3231 weekday is 1-index
- clearSet(); break;
- default: break;
- } break;
+ case fnIsDate: //depends what page we're on
+ if(fnPg==0){ //regular date display: save in RTC
+ switch(fnSetPg){
+ case 1: //save year, set month
+ delay(300); //blink display to indicate save. Safe b/c we've btnStopped. See below for why
+ fnSetValDate[0]=fnSetVal;
+ startSet(fnSetValDate[1],1,12,2); break;
+ case 2: //save month, set date
+ delay(300); //blink display to indicate save. Needed if set month == date: without blink, nothing changes.
+ fnSetValDate[1]=fnSetVal;
+ startSet(fnSetValDate[2],1,daysInMonth(fnSetValDate[0],fnSetValDate[1]),3); break;
+ case 3: //write year/month/date to RTC
+ ds3231.setYear(fnSetValDate[0]%100); //TODO: do we store century on our end? Per ds3231 docs, "The century bit (bit 7 of the month register) is toggled when the years register overflows from 99 to 00."
+ ds3231.setMonth(fnSetValDate[1]);
+ ds3231.setDate(fnSetVal);
+ ds3231.setDoW(dayOfWeek(fnSetValDate[0],fnSetValDate[1],fnSetVal)+1); //ds3231 weekday is 1-index
+ calcSun(fnSetValDate[0],fnSetValDate[1],fnSetVal);
+ isDSTByHour(fnSetValDate[0],fnSetValDate[1],fnSetVal,tod.hour(),true);
+ clearSet(); break;
+ default: break;
+ }
+ } else if(fnPg==fnDateCounter){ //set like date, save in eeprom like finishOpt
+ switch(fnSetPg){
+ case 1: //save month, set date
+ delay(300); //blink display to indicate save. Safe b/c we've btnStopped.
+ //Needed if set month == date: without blink, nothing changes. Also just good feedback.
+ writeEEPROM(5,fnSetVal,false);
+ startSet(readEEPROM(6,false),1,daysInMonth(fnSetValDate[0],fnSetValDate[1]),2); break;
+ case 2: //save date, set direction
+ delay(300);
+ writeEEPROM(6,fnSetVal,false);
+ startSet(readEEPROM(4,false),0,1,3); break;
+ case 3: //save date
+ writeEEPROM(4,fnSetVal,false);
+ clearSet(); break;
+ default: break;
+ }
+ } else if(fnPg==fnDateSunlast || fnPg==fnDateSunnext){ //lat and long
+ //TODO
+ } else if(fnPg==fnDateWeathernow || fnPg==fnDateWeathernext){ //temperature units??
+ //TODO
+ }
+ break;
case fnIsAlarm:
writeEEPROM(0,fnSetVal,true);
clearSet(); break;
@@ -383,22 +457,7 @@ void ctrlEvt(byte ctrl, byte evt){
//TODO will this cancel properly? especially if alarm interrupts?
}
clearSet(); break;
- case fnIsDayCount: //set like date, save in eeprom like finishOpt
- switch(fnSetPg){
- case 1: //save year, set month
- delay(300); //blink display to indicate save. Safe b/c we've btnStopped. See below for why
- writeEEPROM(3,fnSetVal,true);
- startSet(readEEPROM(5,false),1,12,2); break;
- case 2: //save month, set date
- delay(300); //blink display to indicate save. Needed if set month == date: without blink, nothing changes.
- writeEEPROM(5,fnSetVal,false);
- startSet(readEEPROM(6,false),1,daysInMonth(fnSetValDate[0],fnSetValDate[1]),3); break;
- case 3: //save date
- writeEEPROM(6,fnSetVal,false);
- clearSet(); break;
- default: break;
- } break;
- break;
+ //fnIsDayCount removed in favor of paginated calendar
case fnIsTemp:
break;
default: break;
@@ -407,6 +466,9 @@ void ctrlEvt(byte ctrl, byte evt){
if(ctrl==mainAdjUp) doSet(inputLast-inputLast2255?true:false);
fn = fnIsTime;
+ //we may have changed lat/long/GMT/DST settings so recalc those
+ calcSun(tod.year(),tod.month(),tod.day());
+ isDSTByHour(tod.year(),tod.month(),tod.day(),tod.hour(),true);
clearSet();
return;
}
@@ -477,22 +542,26 @@ void fnOptScroll(char dir){
}
}
-void startSet(word n, word m, word x, byte p){ //Enter set state at page p, and start setting a value
- fnSetVal=n; fnSetValMin=m; fnSetValMax=x; fnSetValVel=(x-m>30?1:0); fnSetPg=p;
+void startSet(int n, int m, int x, byte p){ //Enter set state at page p, and start setting a value
+ fnSetVal=n; fnSetValMin=m; fnSetValMax=x; fnSetValVel=(x-m>30?1:0); fnSetPg=p; fnSetValDid=false;
updateDisplay();
}
void doSet(int delta){
//Does actual setting of fnSetVal, as told to by ctrlEvt or doSetHold. Makes sure it stays within range.
if(delta>0) if(fnSetValMax-fnSetVal=250) {
+ //The interval used to be 250, but in order to make the value change by a full 9 values between btnShortHold and btnLongHold,
+ //the interval is now that difference divided by 9. TODO divisor may need to be a bit higher in case btnLongHold ever fires before 9th.
+ //It may be weird not being exactly quarter-seconds, but it doesn't line up with the blinking anyway.
+ if(start || (unsigned long)(now-doSetHoldLast)>=((btnLongHold-btnShortHold)/9)) {
doSetHoldLast = now;
if(fnSetPg!=0 && (mainAdjType==1 && (btnCur==mainAdjUp || btnCur==mainAdjDn)) ){ //if we're setting, and this is an adj btn
bool dir = (btnCur==mainAdjUp ? 1 : 0);
@@ -505,6 +574,7 @@ void doSetHold(){
}
void clearSet(){ //Exit set state
startSet(0,0,0,0);
+ fnSetValDid=false;
updateLEDs(); //in case LED setting was changed
checkRTC(true); //force an update to tod and updateDisplay()
}
@@ -525,77 +595,68 @@ void initEEPROM(bool hard){
ds3231.setMinute(0);
ds3231.setSecond(0);
}
- //Set the default values that aren't part of the options menu
- //only on hard init, or if the set value is outside of range
- if(hard || readEEPROM(0,true)>1439) writeEEPROM(0,420,true); //alarm at 7am
- if(hard || readEEPROM(2,false)>1) writeEEPROM(2,enableSoftAlarmSwitch==0?1:0,false); //alarm is off, or on if no software switch
- if(hard || readEEPROM(3,true)<2018) writeEEPROM(3,2018,true); //day counter target: 2018...
- if(hard || readEEPROM(5,false)<1 || readEEPROM(5,false)>12) writeEEPROM(5,1,false); //...January...
- if(hard || readEEPROM(6,false)<1 || readEEPROM(6,false)>31) writeEEPROM(6,1,false); //...first.
+ if(hard || readEEPROM(0,true)>1439) writeEEPROM(0,420,true); //0-1: alarm at 7am
+ if(hard){ alarmOn = !enableSoftAlarmSwitch; writeEEPROM(2,alarmOn,false); } //2: alarm is off, or on if no software switch
+ //3 is free
+ if(hard || readEEPROM(4,false)<0 || readEEPROM(4,false)>1) writeEEPROM(4,1,false); //4: day counter direction: count up...
+ if(hard || readEEPROM(5,false)<1 || readEEPROM(5,false)>12) writeEEPROM(5,12,false); //5: ...December...
+ if(hard || readEEPROM(6,false)<1 || readEEPROM(6,false)>31) writeEEPROM(6,31,false); //6: ...31st. (This gives the day of the year)
bool foundAltFn = false;
for(byte fni=0; fni255?true:false); //TODO shouldn't this be optsMax
- if(hard || readEEPROM(optsLoc[opt],isWord)optsMax[opt])
- writeEEPROM(optsLoc[opt],optsDef[opt],isWord);
+ isInt = (optsMax[opt]>255?true:false);
+ if(hard || readEEPROM(optsLoc[opt],isInt)optsMax[opt])
+ writeEEPROM(optsLoc[opt],optsDef[opt],isInt);
} //end for
} //end initEEPROM()
-word readEEPROM(int loc, bool isWord){
- if(isWord) {
- //TODO why was it done this way
- //word w = (EEPROM.read(loc)<<8)+EEPROM.read(loc+1); return w; //word / unsigned int, 0-65535
+int readEEPROM(int loc, bool isInt){
+ if(isInt) {
+ // if(loc==[value under test]) {
+ // Serial.print(F("EEPROM read 2 bytes:"));
+ // Serial.print(F(" loc ")); Serial.print(loc,DEC); Serial.print(F(" val ")); Serial.print(EEPROM.read(loc),DEC);
+ // Serial.print(F(" loc ")); Serial.print(loc+1,DEC); Serial.print(F(" val ")); Serial.print(EEPROM.read(loc+1),DEC);
+ // Serial.print(F(" sum ")); Serial.print((EEPROM.read(loc)<<8)+EEPROM.read(loc+1)); Serial.println();
+ // }
return (EEPROM.read(loc)<<8)+EEPROM.read(loc+1);
} else {
- //byte b = EEPROM.read(loc); return b; //byte, 0-255
+ // if(loc==[value under test]) {
+ // Serial.print(F("EEPROM read 1 byte:"));
+ // Serial.print(F(" loc ")); Serial.print(loc,DEC); Serial.print(F(" val ")); Serial.print(EEPROM.read(loc),DEC); Serial.println();
+ // }
return EEPROM.read(loc);
}
}
-void writeEEPROM(int loc, int val, bool isWord){
- if(isWord) {
- // Serial.print(F("EEPROM write word:"));
+void writeEEPROM(int loc, int val, bool is2Byte){
+ if(is2Byte) {
+ // Serial.print(F("EEPROM write 2 bytes (")); Serial.print(val,DEC); Serial.print(F("):"));
// Serial.print(F(" loc ")); Serial.print(loc,DEC); Serial.print(F(" val "));
- // if(EEPROM.read(loc)!=highByte(val)) { Serial.print(highByte(val),DEC); } else { Serial.print(F("no change")); }
- EEPROM.update(loc,highByte(val));
+ // if(EEPROM.read(loc)!=highByte(val)) { Serial.print(highByte(val),DEC); } else { Serial.print(F("no change")); } Serial.print(F(";"));
// Serial.print(F(" loc ")); Serial.print(loc+1,DEC); Serial.print(F(" val "));
- // if(EEPROM.read(loc+1)!=lowByte(val)) { Serial.print(lowByte(val),DEC); } else { Serial.print(F("no change")); }
+ // if(EEPROM.read(loc+1)!=lowByte(val)) { Serial.println(lowByte(val),DEC); } else { Serial.println(F("no change")); }
+ EEPROM.update(loc,highByte(val));
EEPROM.update(loc+1,lowByte(val));
} else {
- // Serial.print(F("EEPROM write byte:"));
+ // Serial.print(F("EEPROM write byte (")); Serial.print(val,DEC); Serial.print(F("):"));
// Serial.print(F(" loc ")); Serial.print(loc,DEC); Serial.print(F(" val "));
- // if(EEPROM.read(loc)!=val) { Serial.print(val,DEC); } else { Serial.print(F("no change")); }
+ // if(EEPROM.read(loc)!=val) { Serial.println(val,DEC); } else { Serial.println(F("no change")); }
EEPROM.update(loc,val);
}
- //Serial.println();
}
-byte daysInMonth(word y, byte m){
- if(m==2) return (y%4==0 && (y%100!=0 || y%400==0) ? 29 : 28);
- //https://cmcenroe.me/2014/12/05/days-in-month-formula.html
- else return (28 + ((m + (m/8)) % 2) + (2 % m) + (2 * (1/m)));
-}
-//The following are for use with the Day Counter feature - find number of days between now and target date
-//I don't know how reliable the unixtime() is from RTClib past 2038, plus there might be timezone complications
-//so we'll just calculate the number of days since 2000-01-01
-long dateToDayCount(word y, byte m, byte d){
- long dc = (y-2000)*365; //365 days for every full year since 2000
- word i; for(i=0; i=fnOpts){
if((unsigned long)(now-inputLast)>=timeoutSet*1000) { fnSetPg = 0; fn = fnIsTime; force=true; } //Time out after 2 mins
}
- //Temporary-display mode timeout: if we're *not* in a permanent one (time, Alt preset, or running/signaling timer)
- else if(fn!=fnIsTime && fn!=readEEPROM(7,false) && !(fn==fnIsTimer && (timerRemain>0 || signalRemain>0))){
+ //Paged-display function timeout //TODO change fnIsDate to consts? //TODO timeoutPageFn var
+ else if(fn==fnIsDate && (unsigned long)(now-inputLast)>=2500) {
+ //Here we just have to increment the page and decide when to reset. updateDisplay() will do the rendering
+ fnPg++; inputLast+=2500; //but leave inputLastTODMins alone so the subsequent page displays will be based on the same TOD
+ if(fnPg >= fnDatePages){ fnPg = 0; fn = fnIsTime; }
+ force=true;
+ }
+ //Temporary-display function timeout: if we're *not* in a permanent one (time, or running/signaling timer)
+ else if(fn!=fnIsTime && !(fn==fnIsTimer && (timerRemain>0 || signalRemain>0))){ // && fn!=readEEPROM(7,false)
if((unsigned long)(now-inputLast)>=timeoutTempFn*1000) { fnSetPg = 0; fn = fnIsTime; force=true; }
}
//Stop a signal pulse if it's time to
@@ -628,30 +696,40 @@ void checkRTC(bool force){
if(rtcSecLast != tod.second() || force) { //If it's a new RTC second, or we are forcing it
+ //First run things
+ if(rtcSecLast==61) { autoDST(); calcSun(tod.year(),tod.month(),tod.day()); }
+
//Things to do at specific times
if(tod.second()==0) { //at top of minute
//at 2am, check for DST change
- if(tod.minute()==0 && tod.hour()==2) autoDST();
- //check if we should trigger the alarm - if the time is right and the alarm is on...
- if(tod.hour()*60+tod.minute()==readEEPROM(0,true) && readEEPROM(2,false)) {
- if(readEEPROM(23,false)==0 || //any day of the week
- (readEEPROM(23,false)==1 && isDayInRange(readEEPROM(33,false),readEEPROM(34,false),toddow)) || //weekday only
- (readEEPROM(23,false)==2 && !isDayInRange(readEEPROM(33,false),readEEPROM(34,false),toddow)) ) { //weekend only
- fnSetPg = 0; fn = fnIsTime; signalStart(fnIsAlarm,1,0);
- } //end toddow check
+ if((tod.minute()==0 && tod.hour()==2)) autoDST();
+ //at the alarm trigger time
+ if(tod.hour()*60+tod.minute()==readEEPROM(0,true)){
+ if(alarmOn && !alarmSkip) { //if the alarm is on and not skipped, sound it!
+ fnSetPg = 0; fn = fnIsTime; signalStart(fnIsAlarm,1,0);
+ }
+ //set alarmSkip for the next instance of the alarm
+ alarmSkip =
+ //if alarm is any day of the week
+ (readEEPROM(23,false)==0 ||
+ //or if alarm is weekday only, and tomorrow is a weekday
+ (readEEPROM(23,false)==1 && isDayInRange(readEEPROM(33,false),readEEPROM(34,false),(toddow==6?0:toddow+1))) ||
+ //or if alarm is weekend only, and tomorrow is a weekend
+ (readEEPROM(23,false)==2 && !isDayInRange(readEEPROM(33,false),readEEPROM(34,false),(toddow==6?0:toddow+1)))
+ ? 0: 1); //then don't skip the next alarm; else skip it
} //end alarm trigger
}
//At bottom of minute, see if we should show the date
if(tod.second()==30 && fn==fnIsTime && fnSetPg==0 && unoffRemain==0) {
- if(readEEPROM(18,false)>=2) { fn = fnIsDate; inputLast = now; updateDisplay(); }
+ if(readEEPROM(18,false)>=2) { fn = fnIsDate; inputLast = now; inputLastTODMins = tod.hour()*60+tod.minute(); fnPg = 254; updateDisplay(); }
if(readEEPROM(18,false)==3) { startScroll(); }
}
//Anti-poisoning routine triggering: start when applicable, and not at night, during setting, or after a button press (unoff)
if(tod.second()<2 && displayDim==2 && fnSetPg==0 && unoffRemain==0) {
switch(readEEPROM(46,false)) { //how often should the routine run?
case 0: //every day
- if(readEEPROM(27,false)>0? //is night mode enabled?
- tod.second()==0 && tod.hour()*60+tod.minute()==readEEPROM(28,true): //if so, at start of night mode (at second :00 before dim is in effect)
+ if(readEEPROM(27,false)>0? //is night shutoff enabled?
+ tod.second()==0 && tod.hour()*60+tod.minute()==readEEPROM(28,true): //if so, at start of night shutoff (at second :00 before dim is in effect)
tod.second()==1 && tod.hour()*60+tod.minute()==0) //if not, at 00:00:01
cleanRemain = 51; //run routine for five cycles
break;
@@ -700,15 +778,16 @@ void checkRTC(bool force){
if(readEEPROM(25,false)) { //interval timer: a short signal and restart; don't change to timer fn
signalStart(fnIsTimer,0,0); timerRemain = timerInitial;
} else {
- fnSetPg = 0; fn = fnIsTimer; inputLast = now; signalStart(fnIsTimer,signalDur,0);
+ fnSetPg = 0; fn = fnIsTimer; inputLast = now; inputLastTODMins = tod.hour()*60+tod.minute(); signalStart(fnIsTimer,signalDur,0);
}
} //end not switched relay
} //end timer elapsed
}
//If alarm snooze has time on it, decrement and trigger signal if we reach zero (and alarm is still on)
+ //Won't check alarm skip status here, as it reflects tomorrow
if(snoozeRemain>0) {
snoozeRemain--;
- if(snoozeRemain<=0 && readEEPROM(2,false)) {
+ if(snoozeRemain<=0 && alarmOn) {
fnSetPg = 0; fn = fnIsTime; signalStart(fnIsAlarm,1,0);
}
}
@@ -724,67 +803,140 @@ void checkRTC(bool force){
if(unoffRemain>0) {
unoffRemain--; //updateDisplay will naturally put it back to off state if applicable
}
+ if(versionRemain>0) {
+ versionRemain--;
+ }
} //end natural second
//Finally, update the display, whether natural tick or not, as long as we're not setting or on a scrolled display (unless forced eg. fn change)
- //This also determines night/away mode, which is why strikes will happen if we go into off at top of hour, and not when we come into on at the top of the hour TODO find a way to fix this
- if(fnSetPg==0 && (scrollRemain==0 || force)) updateDisplay();
+ //This also determines night/away shutoff, which is why strikes will happen if we go into off at top of hour, and not when we come into on at the top of the hour TODO find a way to fix this
+ //Also skip updating the display if this is date and not being forced, since its pages take some calculating that cause it to flicker
+ if(fnSetPg==0 && (scrollRemain==0 || force) && !(fn==fnIsDate && !force)) updateDisplay();
rtcSecLast = tod.second();
} //end if force or new second
} //end checkRTC()
-bool fellBack = false;
void autoDST(){
- //Call daily when clock reaches 2am.
- //If rule matches, will set to 3am in spring, 1am in fall (and set fellBack so it only happens once)
- if(fellBack) { fellBack=false; return; } //If we fell back at last 2am, do nothing.
- if(toddow==0) { //is it sunday? currently all these rules fire on Sundays only
- switch(readEEPROM(22,false)){
- case 1: //second Sunday in March to first Sunday in November (US/CA)
- if(tod.month()==3 && tod.day()>=8 && tod.day()<=14) setDST(1);
- if(tod.month()==11 && tod.day()<=7) setDST(-1);
- break;
- case 2: //last Sunday in March to last Sunday in October (UK/EU)
- if(tod.month()==3 && tod.day()>=25) setDST(1);
- if(tod.month()==10 && tod.day()>=25) setDST(-1);
- break;
- case 3: //first Sunday in April to last Sunday in October (MX)
- if(tod.month()==4 && tod.day()<=7) setDST(1);
- if(tod.month()==10 && tod.day()>=25) setDST(-1);
- break;
- case 4: //last Sunday in September to first Sunday in April (NZ)
- if(tod.month()==9 && tod.day()>=24) setDST(1); //30 days hath September: last Sun will be 24-30
- if(tod.month()==4 && tod.day()<=7) setDST(-1);
- break;
- case 5: //first Sunday in October to first Sunday in April (AU)
- if(tod.month()==10 && tod.day()<=7) setDST(1);
- if(tod.month()==4 && tod.day()<=7) setDST(-1);
- break;
- case 6: //third Sunday in October to third Sunday in February (BZ)
- if(tod.month()==10 && tod.day()>=15 && tod.day()<=21) setDST(1);
- if(tod.month()==2 && tod.day()>=15 && tod.day()<=21) setDST(-1);
- break;
- default: break;
- } //end setting switch
- } //end is it sunday
+ //Change the clock if the current DST differs from the dstOn flag.
+ //Call daily when clock reaches 2am, and at first run.
+ bool dstNow = isDSTByHour(tod.year(),tod.month(),tod.day(),tod.hour(),false);
+ if(readEEPROM(15,false)>1){ //dstOn unreliable probably due to software update to 1.6.0
+ dstOn = dstNow; writeEEPROM(15,dstOn,false); }
+ if(dstNow!=dstOn){ ds3231.setHour(dstNow>dstOn? 3: 1); dstOn = dstNow; writeEEPROM(15,dstOn,false); }
+}
+bool isDST(int y, char m, char d){
+ //returns whether DST is in effect on this date (after 2am shift)
+ switch(readEEPROM(22,false)){ //local DST ruleset
+ case 1: //second Sunday in March to first Sunday in November (US/CA)
+ return (m==3 && d>=nthSunday(y,3,2)) || (m>3 && m<11) || (m==11 && d=nthSunday(y,3,-1)) || (m>3 && m<10) || (m==10 && d=nthSunday(y,4,1)) || (m>4 && m<10) || (m==10 && d=nthSunday(y,9,-1)) || (m>9 || m<4) || (m==4 && d=nthSunday(y,10,1)) || (m>10 || m<4) || (m==4 && d=nthSunday(y,10,3)) || (m>10 || m<2) || (m==2 && d0) return (((7-dayOfWeek(y,m,1))%7)+1+((nth-1)*7));
+ if(nth<0) return (dayOfWeek(y,m,1)==0 && daysInMonth(y,m)>28? 29: nthSunday(y,m,1)+21+((nth+1)*7));
+ return 0;
+}
+byte daysInMonth(word y, byte m){
+ if(m==2) return (y%4==0 && (y%100!=0 || y%400==0) ? 29 : 28);
+ //https://cmcenroe.me/2014/12/05/days-in-month-formula.html
+ else return (28 + ((m + (m/8)) % 2) + (2 % m) + (2 * (1/m)));
+}
+int daysInYear(word y){
+ return 337 + daysInMonth(y,2);
+}
+int dateToDayCount(word y, byte m, byte d){
+ int dc = 0;
+ for(byte i=1; i=m*100+d+(countUp&&!(mt==12&&dt==31)?1:0); //if count up from 12/31 (day of year), show 365/366 instead of 0
+ int targetYear = (countUp && targetDir? y-1: (!countUp && !targetDir? y+1: y));
+ int targetDayCount; targetDayCount = dateToDayCount(targetYear, mt, dt);
+ if(targetYeary) targetDayCount += daysInYear(y);
+ long currentDayCount; currentDayCount = dateToDayCount(y,m,d);
+ return abs(targetDir? currentDayCount-targetDayCount: targetDayCount-currentDayCount);
+ //For now, this does not indicate negative (eg with leading zeros bc I don't like how it looks here)
+ //and since the direction is specified, it's always going to be either negative or positive
+ // Serial.print("Today is ");
+ // serialPrintDate(y,m,d);
+ // Serial.print(" and ");
+ // serialPrintDate(targetYear,mt,dt);
+ // Serial.print(" is ");
+ // Serial.print(abs(targetDir? currentDayCount-targetDayCount: targetDayCount-currentDayCount),DEC);
+ // Serial.print(" day(s) ");
+ // Serial.println(countUp?"ago":"away");
}
void switchAlarm(char dir){
if(enableSoftAlarmSwitch){
signalStop(); //snoozeRemain = 0;
- byte itWas = readEEPROM(2,false);
- if(dir==1) writeEEPROM(2,1,false);
- if(dir==-1) writeEEPROM(2,0,false);
- if(dir==0) writeEEPROM(2,!readEEPROM(2,false),false);
- if(readEEPROM(2,false) && itWas==false) quickBeep(); //Short signal to indicate we just turned the alarm on
+ //There are three alarm states - on, on with skip (skips the next alarm trigger), and off.
+ //Currently we use up/down buttons or a rotary control, rather than a binary switch, so we can cycle up/down through these states.
+ //On/off is stored in EEPROM to survive power loss; skip is volatile, not least because it can change automatically and I don't like making automated writes to EEPROM if I can help it. Skip state doesn't matter when alarm is off.
+ if(dir==0) dir=(alarmOn?-1:1); //If alarm is off, cycle button goes up; otherwise down.
+ if(dir==1){
+ if(!alarmOn){ //if off, go straight to on, no skip
+ alarmOn=1; writeEEPROM(2,alarmOn,false); alarmSkip=0; quickBeep(76); //C7
+ }
+ else if(alarmSkip){ //else if skip, go to on
+ alarmSkip=0; quickBeep(76); //C7
+ }
+ }
+ if(dir==-1){
+ if(alarmOn){ //if on
+ if(!alarmSkip){ //if not skip, go to skip
+ alarmSkip=1; quickBeep(71); //G6
+ }
+ else { //if skip, go to off
+ alarmOn=0; writeEEPROM(2,alarmOn,false); quickBeep(64); //C6
+ }
+ }
+ }
updateDisplay();
}
+ //TODO don't make alarm permanent until leaving setting to minimize writes to eeprom as user cycles through options?
}
void switchPower(char dir){
signalRemain = 0; snoozeRemain = 0; //in case alarm is going now - alternatively use signalStop()?
@@ -871,6 +1023,11 @@ void updateDisplay(){
displayNext[i] = (isrc>=displaySize? 15: scrollDisplay[isrc]); //allow to fade
}
}
+ else if(versionRemain>0) {
+ editDisplay(vMajor, 0, 1, false, false);
+ editDisplay(vMinor, 2, 3, false, false);
+ editDisplay(vPatch, 4, 5, false, false);
+ }
else if(fnSetPg) { //setting value, for either fn or option
displayDim = 2;
blankDisplay(4, 5, false);
@@ -884,7 +1041,14 @@ void updateDisplay(){
} else if(fnSetValMax==88) { //A piezo pitch. Play a short demo beep.
editDisplay(fnSetVal, 0, 3, false, false);
if(piezoPin>=0) { signalStop(); tone(piezoPin, getHz(fnSetVal), 100); } //One exception to using signalStart since we need to specify pitch directly
- } else editDisplay(fnSetVal, 0, 3, false, false); //some other type of value
+ } else if(fnSetValMax==156) { //Timezone offset from UTC in quarter hours plus 100 (since we're not set up to support signed bytes)
+ editDisplay((abs(fnSetVal-100)*25)/100, 0, 1, fnSetVal<100, false); //hours, leading zero for negatives
+ editDisplay((abs(fnSetVal-100)%4)*15, 2, 3, true, false); //minutes, leading zero always
+ } else if(fnSetValMax==900 || fnSetValMax==1800) { //Lat/long in tenths of a degree
+ //If 6 tubes (0-5), display degrees on 0-3 and tenths on 4, with 5 blank
+ //If 4 tubes (0-3), display degrees on 0-2 and tenths on 3
+ editDisplay(abs(fnSetVal), 0, (displaySize>4? 4: 3), fnSetVal<0, false);
+ } else editDisplay(abs(fnSetVal), 0, 3, fnSetVal<0, false); //some other type of value - leading zeros for negatives
}
else if(fn >= fnOpts){ //options menu, but not setting a value
displayDim = 2;
@@ -902,7 +1066,7 @@ void updateDisplay(){
displayDim = (unoffRemain>0? 2: 0); //unoff overrides this
//clock at home: away on weekdays, during office hours only
else if( readEEPROM(32,false)==2 && isDayInRange(readEEPROM(33,false),readEEPROM(34,false),toddow) && isTimeInRange(readEEPROM(35,true), readEEPROM(37,true), todmins) ) displayDim = (unoffRemain>0? 2: 0);
- //night mode - if night end is 0:00, use alarm time instead
+ //night shutoff - if night end is 0:00, use alarm time instead
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:(unoffRemain>0?2:0)); //dim or (unoff? bright: off)
//normal
else displayDim = 2;
@@ -917,37 +1081,42 @@ void updateDisplay(){
if(readEEPROM(18,false)==1) editDisplay(tod.day(), 4, 5, readEEPROM(19,false), true); //date
else editDisplay(tod.second(), 4, 5, true, true); //seconds
break;
- case fnIsDate:
- byte df; df = readEEPROM(17,false); //1=m/d/w, 2=d/m/w, 3=m/d/y, 4=d/m/y, 5=y/m/d
- if(df<=4) {
- editDisplay((df==1||df==3?tod.month():tod.day()),0,1,readEEPROM(19,false),true); //month or date first
- editDisplay((df==1||df==3?tod.day():tod.month()),2,3,readEEPROM(19,false),true); //date or month second
- editDisplay((df<=2?toddow:tod.year()),4,5,(df<=2?false:true),true); //dow or year third - dow never leading zero, year always
+ case fnIsDate: //a paged display
+ if(fnPg==0 || fnPg==254){ //plain ol' date - 0 will continue to other pages, 254 will only display date then return to time (e.g. at half minute)
+ byte df; df = readEEPROM(17,false); //1=m/d/w, 2=d/m/w, 3=m/d/y, 4=d/m/y, 5=y/m/d
+ if(df<=4) {
+ editDisplay((df==1||df==3?tod.month():tod.day()),0,1,readEEPROM(19,false),true); //month or date first
+ editDisplay((df==1||df==3?tod.day():tod.month()),2,3,readEEPROM(19,false),true); //date or month second
+ editDisplay((df<=2?toddow:tod.year()),4,5,(df<=2?false:true),true); //dow or year third - dow never leading zero, year always
+ }
+ else { //df==5
+ editDisplay(tod.year(),0,1,true,true); //year always has leading zero
+ editDisplay(tod.month(),2,3,readEEPROM(19,false),true);
+ editDisplay(tod.day(),4,5,readEEPROM(19,false),true);
+ }
}
- else { //df==5
- editDisplay(tod.year(),0,1,true,true); //year always has leading zero
- editDisplay(tod.month(),2,3,readEEPROM(19,false),true);
- editDisplay(tod.day(),4,5,readEEPROM(19,false),true);
+ else if(fnPg==fnDateCounter){
+ editDisplay(dateComp(tod.year(),tod.month(),tod.day(),readEEPROM(5,false),readEEPROM(6,false),readEEPROM(4,false)),0,3,false,true);
+ blankDisplay(4,5,true);
}
- break;
- case fnIsDayCount:
- long targetDayCount; targetDayCount = dateToDayCount(
- readEEPROM(3,true),
- readEEPROM(5,false),
- readEEPROM(6,false)
- );
- long currentDayCount; currentDayCount = dateToDayCount(tod.year(),tod.month(),tod.day());
- editDisplay(abs(targetDayCount-currentDayCount),0,3,false,true);
- //TODO for now don't indicate negative. Elsewhere we use leading zeros to represent negative but I don't like how that looks here
- blankDisplay(4,5,true);
- break;
+ //The sun and weather displays are based on a snapshot of the time of day when the function display was triggered, just in case it's triggered a few seconds before a sun event (sunrise/sunset) and the "prev/now" and "next" displays fall on either side of that event, they'll both display data from before it. If triggered just before midnight, the date could change as well – not such an issue for sun, but might be for weather - TODO create date snapshot also
+ else if(fnPg==fnDateSunlast) displaySun(0,tod.day(),inputLastTODMins);
+ else if(fnPg==fnDateWeathernow) displayWeather(0);
+ else if(fnPg==fnDateSunnext) displaySun(1,tod.day(),inputLastTODMins);
+ else if(fnPg==fnDateWeathernext) displayWeather(1);
+ break; //end fnIsDate
+ //fnIsDayCount removed in favor of paginated calendar
case fnIsAlarm: //alarm
word almTime; almTime = readEEPROM(0,true);
editDisplay(almTime/60, 0, 1, readEEPROM(19,false), true); //hours with leading zero
editDisplay(almTime%60, 2, 3, true, true);
- editDisplay(readEEPROM(2,false),4,4,false,true); //status 1/0
- displayDim = (readEEPROM(2,false)?2:1); //status bright/dim
- blankDisplay(5,5,true);
+ if(alarmOn && alarmSkip){ //alarm on+skip
+ editDisplay(1,4,5,true,true); //01 to indicate off now, on maybe later
+ } else { //alarm fully on or off
+ editDisplay(alarmOn,4,4,false,true);
+ blankDisplay(5,5,true);
+ }
+ displayDim = (alarmOn?2:1); //status bright/dim
break;
case fnIsTimer: //timer - display time left.
//Relative unit positioning: when t <1h, display min/sec in place of hr/min on 4-tube displays
@@ -981,7 +1150,7 @@ void updateDisplay(){
}//end switch
} //end if fn running
- if(false) { //DEBUG MODE: when display's not working, just write it to the console, with time
+ if(false) { //DEBUG MODE: when display's not working, just write it to the console, with time. TODO create dummy display handler
if(tod.hour()<10) Serial.print(F("0"));
Serial.print(tod.hour(),DEC);
Serial.print(F(":"));
@@ -1005,6 +1174,97 @@ void updateDisplay(){
} //end updateDisplay()
+//A snapshot of sun times, in minutes past midnight, calculated at clean time and when the date or time is changed.
+//Need to capture this many, as we could be displaying these values at least through end of tomorrow depending on when cleaning happens.
+
+char sunDate = 0; //date of month when calculated ("today")
+int sunSet0 = -1; //yesterday's set
+int sunRise1 = -1; //today rise
+int sunSet1 = -1; //today set
+int sunRise2 = -1; //tomorrow rise
+int sunSet2 = -1; //tomorrow set
+int sunRise3 = -1; //day after tomorrow rise
+void calcSun(int y, char m, char d){
+ //Calculates sun times and stores them in the values above
+ blankDisplay(0,5,false); //immediately blank display so we can fade in from it elegantly
+ Dusk2Dawn here(readEEPROM(10,true)/10, readEEPROM(12,true)/10, (float(readEEPROM(14,false))-100)/4);
+ //Today
+ sunDate = d;
+ sunRise1 = here.sunrise(y,m,d,isDST(y,m,d)); //TODO: unreliable if event is before time change on DST change day. Optionally if isDSTChangeDay() and event is <2h de-correct for it - maybe modify the library to do this - as when 2h overlaps in fall, we don't know whether the output has been precorrected.
+ sunSet1 = here.sunset(y,m,d,isDST(y,m,d));
+ // serialPrintDate(y,m,d);
+ // Serial.print(F(" Rise ")); serialPrintTime(sunRise1);
+ // Serial.print(F(" Set ")); serialPrintTime(sunSet1); Serial.println();
+ //Yesterday
+ d--; if(d<1){ m--; if(m<1){ y--; m=12; } d=daysInMonth(y,m); }
+ sunSet0 = here.sunset(y,m,d,isDST(y,m,d));
+ // serialPrintDate(y,m,d);
+ // Serial.print(F(" "));
+ // Serial.print(F(" Set ")); serialPrintTime(sunSet0); Serial.println();
+ //Tomorrow
+ d+=2; if(d>daysInMonth(y,m)){ d-=daysInMonth(y,m); m++; if(m>12){ m=1; y++; }}
+ sunRise2 = here.sunrise(y,m,d,isDST(y,m,d));
+ sunSet2 = here.sunset(y,m,d,isDST(y,m,d));
+ // serialPrintDate(y,m,d);
+ // Serial.print(F(" Rise ")); serialPrintTime(sunRise2);
+ // Serial.print(F(" Set ")); serialPrintTime(sunSet2); Serial.println();
+ //Day after tomorrow
+ d++; if(d>daysInMonth(y,m)){ d=1; m++; if(m>12){ m=1; y++; }}
+ sunRise3 = here.sunrise(y,m,d,isDST(y,m,d));
+ // serialPrintDate(y,m,d);
+ // Serial.print(F(" Rise ")); serialPrintTime(sunRise3); Serial.println();
+}
+void serialPrintDate(int y, char m, char d){
+ Serial.print(y,DEC); Serial.print(F("-"));
+ if(m<10) Serial.print(F("0")); Serial.print(m,DEC); Serial.print(F("-"));
+ if(d<10) Serial.print(F("0")); Serial.print(d,DEC);
+}
+void serialPrintTime(int todMins){
+ if(todMins/60<10) Serial.print(F("0")); Serial.print(todMins/60,DEC); Serial.print(F(":"));
+ if(todMins%60<10) Serial.print(F("0")); Serial.print(todMins%60,DEC);
+}
+
+void displaySun(char which, int d, int tod){
+ //Displays sun times from previously calculated values
+ //Old code to calculate sun at display time, with test serial output, is in commit 163ca33
+ //which is 0=prev, 1=next
+ int evtTime = 0; bool evtIsRise = 0;
+ if(d==sunDate){ //displaying same day as calc
+ //before sunrise: prev is calcday-1 sunset, next is calcday sunrise
+ //daytime: prev is calcday sunrise, next is calcday sunset
+ //after sunset: prev is calcday sunset, next is calcday+1 sunrise
+ if(tod12?hr-12:hr)); //12/24h per settings
+ editDisplay(hr, 0, 1, readEEPROM(19,false), true); //leading zero per settings
+ editDisplay(evtTime%60, 2, 3, true, true);
+ }
+ blankDisplay(4, 4, true);
+ editDisplay(evtIsRise, 5, 5, false, true);
+}
+
+void displayWeather(char which){
+ //shows high/low temp (for day/night respectively) on main tubes, and precipitation info on seconds tubes
+ //which==0: display for current sun period (after last sun event)
+ //which==1: display for next sun period (after next sun event)
+ //IoT: Show the weather for the current period (after last sun event): high (day) or low (night) and precip info//IoT: Show the weather for the period after the next sun event
+}
+
void editDisplay(word n, byte posStart, byte posEnd, bool leadingZeros, bool fade){
//Splits n into digits, sets them into displayNext in places posSt-posEnd (inclusive), with or without leading zeros
//If there are blank places (on the left of a non-leading-zero number), uses value 15 to blank tube
@@ -1016,6 +1276,8 @@ void editDisplay(word n, byte posStart, byte posEnd, bool leadingZeros, bool fad
case 1: place=10; break;
case 2: place=100; break;
case 3: place=1000; break;
+ case 4: place=10000; break;
+ case 5: place=100000; break;
default: break;
}
displayNext[posEnd-i] = (i==0&&n==0 ? 0 : (n>=place ? (n/place)%10 : (leadingZeros?0:15)));
@@ -1056,7 +1318,7 @@ void initOutputs() {
if(piezoPin>=0) pinMode(piezoPin, OUTPUT);
if(relayPin>=0) {
pinMode(relayPin, OUTPUT); digitalWrite(relayPin, HIGH); //LOW = device on
- quickBeep(); //"primes" the beeper, seems necessary when relay pin is spec'd, otherwise first intentional beep doesn't happen
+ quickBeep(76); //"primes" the beeper, seems necessary when relay pin is spec'd, otherwise first intentional beep doesn't happen TODO still true?
}
if(ledPin>=0) pinMode(ledPin, OUTPUT);
updateLEDs(); //set to initial value
@@ -1074,6 +1336,7 @@ void cycleDisplay(){
} else {
if(setStartLast>0) setStartLast=0;
}
+ //TODO if we want to flash certain elements, we might do it similarly here
fadeLastDur = fadeDur-(dim?dimDur:0); //by default, last digit displays for entire fadeDur minus dim time
@@ -1138,7 +1401,7 @@ void cycleDisplay(){
if(dim) delay(dimDur);
} //end if displayDim>0
-
+ //TODO why does it sometimes flicker while in the setting mode
} //end cycleDisplay()
void setCathodes(byte decValA, byte decValB){
@@ -1221,10 +1484,11 @@ word getHz(byte note){
char getSignalOutput(){ //for current signal: time, timer, or (default) alarm: 0=piezo, 1=relay
return readEEPROM((signalSource==fnIsTime?44:(signalSource==fnIsTimer?43:42)),false);
}
-void quickBeep(){
- //Short beeper signal at alarm pitch (if equipped), even if relay is switched. Used when silencing snooze or switching alarm on.
- if(getSignalOutput()==1 && relayMode==0) { if(piezoPin>=0) { signalSource=fnIsAlarm; tone(piezoPin, getSignalPitch(), 100); }}
- else signalStart(fnIsAlarm,0,100);
+void quickBeep(int pitch){
+ //C6 = 64
+ //G6 = 71
+ //C7 = 76
+ if(piezoPin>=0) { signalStop(); tone(piezoPin, getHz(pitch), 100); }
}
const byte ledFadeStep = 10; //fade speed – with every loop() we'll increment/decrement the LED brightness (between 0-255) by this amount
@@ -1242,7 +1506,7 @@ void updateLEDs(){
ledStateTarget = 255;
//Serial.println(F("LEDs on always"));
break;
- case 2: //on, but follow night/away modes
+ case 2: //on, but follow night/away shutoff
ledStateTarget = (displayDim==2? 255: (displayDim==1? 127: 0));
//Serial.print(displayDim==2? F("LEDs on"): (displayDim==1? F("LEDs dim"): F("LEDs off"))); Serial.println(F(" per dim state"));
break;
diff --git a/sixtube_lm/configs/v5-4tube.h b/arduino-nixie/configs/v5-4tube.h
similarity index 86%
rename from sixtube_lm/configs/v5-4tube.h
rename to arduino-nixie/configs/v5-4tube.h
index 9fe8ef2..2964d0d 100644
--- a/sixtube_lm/configs/v5-4tube.h
+++ b/arduino-nixie/configs/v5-4tube.h
@@ -7,12 +7,11 @@ const byte fnIsTime = 0;
const byte fnIsDate = 1;
const byte fnIsAlarm = 2;
const byte fnIsTimer = 3;
-const byte fnIsDayCount = 4;
-const byte fnIsTemp = 5;
-const byte fnIsTubeTester = 6; //cycles all digits on all tubes 1/second, similar to anti-cathode-poisoning cleaner
+const byte fnIsTemp = 4;
+const byte fnIsTubeTester = 5; //cycles all digits on all tubes 1/second, similar to anti-cathode-poisoning cleaner
// functions enabled in this clock, in their display order. Only fnIsTime is required
-const byte fnsEnabled[] = {fnIsTime, fnIsDate, fnIsAlarm, fnIsTimer, fnIsDayCount}; //, fnIsTemp, fnIsTubeTester
-// To control which of these display persistently vs. switch back to Time after a few seconds, search "Temporary-display mode timeout"
+const byte fnsEnabled[] = {fnIsTime, fnIsDate, fnIsAlarm, fnIsTimer}; //, fnIsTemp, fnIsTubeTester
+// To control which of these display persistently vs. switch back to Time after a few seconds, search "Temporary-display function timeout"
// These are the UNDB v5 board connections to Arduino analog input pins.
// S1/PL13 = Reset
@@ -40,7 +39,7 @@ const char relayPin = -1; //don't change - not available until UNDB v8
const byte relayMode = 0; //don't change - not available until UNDB v8
const word signalDur = 180; //sec - when pulsed signal is going, pulses are sent once/sec for this period (e.g. 180 = 3min)
const word switchDur = 7200; //sec - when alarm triggers switched relay, it's switched on for this period (e.g. 7200 = 2hr)
-const word piezoPulse = 500; //ms - used with piezo via tone()
+const word piezoPulse = 250; //ms - used with piezo via tone()
const word relayPulse = 200; //ms - used with pulsed relay
//Soft power switches
@@ -56,11 +55,11 @@ const char ledPin = -1; //don't change - not available until UNDB v8
const byte unoffDur = 10; //sec
// How long (in ms) are the button hold durations?
-const word btnShortHold = 1000; //for setting the displayed feataure
-const word btnLongHold = 3000; //for for entering options menu
-const byte velThreshold = 150; //ms
+const word btnShortHold = 1000; //for entering setting mode, or hold-setting at low velocity
+const word btnLongHold = 3000; //for entering options menu, or hold-setting at high velocity
+const word velThreshold = 0; //ms
// When an adj up/down input (btn or rot) follows another in less than this time, value will change more (10 vs 1).
-// Recommend ~150 for rotaries. If you want to use this feature with buttons, extend to ~400.
+// 0 to disable. Recommend ~150 for rotaries. If you want to use this feature with buttons, extend to ~300.
// What is the "frame rate" of the tube cleaning and display scrolling? up to 65535 ms
const word cleanSpeed = 200; //ms
diff --git a/sixtube_lm/configs/v5-6tube.h b/arduino-nixie/configs/v5-6tube.h
similarity index 88%
rename from sixtube_lm/configs/v5-6tube.h
rename to arduino-nixie/configs/v5-6tube.h
index 67afa33..67880bd 100644
--- a/sixtube_lm/configs/v5-6tube.h
+++ b/arduino-nixie/configs/v5-6tube.h
@@ -7,12 +7,11 @@ const byte fnIsTime = 0;
const byte fnIsDate = 1;
const byte fnIsAlarm = 2;
const byte fnIsTimer = 3;
-const byte fnIsDayCount = 4;
const byte fnIsTemp = 5;
const byte fnIsTubeTester = 6; //cycles all digits on all tubes 1/second, similar to anti-cathode-poisoning cleaner
// functions enabled in this clock, in their display order. Only fnIsTime is required
-const byte fnsEnabled[] = {fnIsTime, fnIsDate, fnIsAlarm, fnIsTimer, fnIsDayCount}; //, fnIsTemp, fnIsTubeTester
-// To control which of these display persistently vs. switch back to Time after a few seconds, search "Temporary-display mode timeout"
+const byte fnsEnabled[] = {fnIsTime, fnIsDate, fnIsAlarm, fnIsTimer}; //, fnIsTemp, fnIsTubeTester
+// To control which of these display persistently vs. switch back to Time after a few seconds, search "Temporary-display function timeout"
// These are the UNDB v5 board connections to Arduino analog input pins.
// S1/PL13 = Reset
@@ -40,7 +39,7 @@ const char relayPin = -1; //don't change - not available until UNDB v8
const byte relayMode = 0; //don't change - not available until UNDB v8
const word signalDur = 180; //sec - when pulsed signal is going, pulses are sent once/sec for this period (e.g. 180 = 3min)
const word switchDur = 7200; //sec - when alarm triggers switched relay, it's switched on for this period (e.g. 7200 = 2hr)
-const word piezoPulse = 500; //ms - used with piezo via tone()
+const word piezoPulse = 250; //ms - used with piezo via tone()
const word relayPulse = 200; //ms - used with pulsed relay
//Soft power switches
@@ -56,11 +55,11 @@ const char ledPin = -1; //don't change - not available until UNDB v8
const byte unoffDur = 10; //sec
// How long (in ms) are the button hold durations?
-const word btnShortHold = 1000; //for setting the displayed feataure
-const word btnLongHold = 3000; //for for entering options menu
-const byte velThreshold = 150; //ms
+const word btnShortHold = 1000; //for entering setting mode, or hold-setting at low velocity
+const word btnLongHold = 3000; //for entering options menu, or hold-setting at high velocity
+const word velThreshold = 0; //ms
// When an adj up/down input (btn or rot) follows another in less than this time, value will change more (10 vs 1).
-// Recommend ~150 for rotaries. If you want to use this feature with buttons, extend to ~400.
+// 0 to disable. Recommend ~150 for rotaries. If you want to use this feature with buttons, extend to ~300.
// What is the "frame rate" of the tube cleaning and display scrolling? up to 65535 ms
const word cleanSpeed = 200; //ms
diff --git a/sixtube_lm/configs/v8-4tube.h b/arduino-nixie/configs/v8-4tube.h
similarity index 89%
rename from sixtube_lm/configs/v8-4tube.h
rename to arduino-nixie/configs/v8-4tube.h
index 4b5566c..6088f89 100644
--- a/sixtube_lm/configs/v8-4tube.h
+++ b/arduino-nixie/configs/v8-4tube.h
@@ -7,12 +7,11 @@ const byte fnIsTime = 0;
const byte fnIsDate = 1;
const byte fnIsAlarm = 2;
const byte fnIsTimer = 3;
-const byte fnIsDayCount = 4;
-const byte fnIsTemp = 5;
-const byte fnIsTubeTester = 6; //cycles all digits on all tubes 1/second, similar to anti-cathode-poisoning cleaner
+const byte fnIsTemp = 4;
+const byte fnIsTubeTester = 5; //cycles all digits on all tubes 1/second, similar to anti-cathode-poisoning cleaner
// functions enabled in this clock, in their display order. Only fnIsTime is required
-const byte fnsEnabled[] = {fnIsTime, fnIsDate, fnIsAlarm, fnIsTimer, fnIsDayCount}; //, fnIsTemp, fnIsTubeTester
-// To control which of these display persistently vs. switch back to Time after a few seconds, search "Temporary-display mode timeout"
+const byte fnsEnabled[] = {fnIsTime, fnIsDate, fnIsAlarm, fnIsTimer}; //, fnIsTemp, fnIsTubeTester
+// To control which of these display persistently vs. switch back to Time after a few seconds, search "Temporary-display function timeout"
// These are the RLB board connections to Arduino analog input pins.
// S1/PL13 = Reset
@@ -43,7 +42,7 @@ const byte relayMode = 0; //If relay is equipped, what does it do?
// 1 = pulsed mode: the relay will be pulsed, like the beeper is, to control an intermittent signaling device like a solenoid or indicator lamp. Specify pulse duration in relayPulse.
const word signalDur = 180; //sec - when pulsed signal is going, pulses are sent once/sec for this period (e.g. 180 = 3min)
const word switchDur = 7200; //sec - when alarm triggers switched relay, it's switched on for this period (e.g. 7200 = 2hr)
-const word piezoPulse = 500; //ms - used with piezo via tone()
+const word piezoPulse = 250; //ms - used with piezo via tone()
const word relayPulse = 200; //ms - used with pulsed relay
//Soft power switches
@@ -62,11 +61,11 @@ const char ledPin = -1;
const byte unoffDur = 10; //sec
// How long (in ms) are the button hold durations?
-const word btnShortHold = 1000; //for setting the displayed feataure
-const word btnLongHold = 3000; //for for entering options menu
-const byte velThreshold = 150; //ms
+const word btnShortHold = 1000; //for entering setting mode, or hold-setting at low velocity
+const word btnLongHold = 3000; //for entering options menu, or hold-setting at high velocity
+const word velThreshold = 0; //ms
// When an adj up/down input (btn or rot) follows another in less than this time, value will change more (10 vs 1).
-// Recommend ~150 for rotaries. If you want to use this feature with buttons, extend to ~400.
+// 0 to disable. Recommend ~150 for rotaries. If you want to use this feature with buttons, extend to ~300.
// What is the "frame rate" of the tube cleaning and display scrolling? up to 65535 ms
const word cleanSpeed = 200; //ms
diff --git a/sixtube_lm/configs/v8-6tube.h b/arduino-nixie/configs/v8-6tube.h
similarity index 89%
rename from sixtube_lm/configs/v8-6tube.h
rename to arduino-nixie/configs/v8-6tube.h
index 5281b71..98d76c1 100644
--- a/sixtube_lm/configs/v8-6tube.h
+++ b/arduino-nixie/configs/v8-6tube.h
@@ -7,12 +7,11 @@ const byte fnIsTime = 0;
const byte fnIsDate = 1;
const byte fnIsAlarm = 2;
const byte fnIsTimer = 3;
-const byte fnIsDayCount = 4;
-const byte fnIsTemp = 5;
-const byte fnIsTubeTester = 6; //cycles all digits on all tubes 1/second, similar to anti-cathode-poisoning cleaner
+const byte fnIsTemp = 4;
+const byte fnIsTubeTester = 5; //cycles all digits on all tubes 1/second, similar to anti-cathode-poisoning cleaner
// functions enabled in this clock, in their display order. Only fnIsTime is required
-const byte fnsEnabled[] = {fnIsTime, fnIsDate, fnIsAlarm, fnIsTimer, fnIsDayCount}; //, fnIsTemp, fnIsTubeTester
-// To control which of these display persistently vs. switch back to Time after a few seconds, search "Temporary-display mode timeout"
+const byte fnsEnabled[] = {fnIsTime, fnIsDate, fnIsAlarm, fnIsTimer}; //, fnIsTemp, fnIsTubeTester
+// To control which of these display persistently vs. switch back to Time after a few seconds, search "Temporary-display function timeout"
// These are the RLB board connections to Arduino analog input pins.
// S1/PL13 = Reset
@@ -43,7 +42,7 @@ const byte relayMode = 0; //If relay is equipped, what does it do?
// 1 = pulsed mode: the relay will be pulsed, like the beeper is, to control an intermittent signaling device like a solenoid or indicator lamp. Specify pulse duration in relayPulse.
const word signalDur = 180; //sec - when pulsed signal is going, pulses are sent once/sec for this period (e.g. 180 = 3min)
const word switchDur = 7200; //sec - when alarm triggers switched relay, it's switched on for this period (e.g. 7200 = 2hr)
-const word piezoPulse = 500; //ms - used with piezo via tone()
+const word piezoPulse = 250; //ms - used with piezo via tone()
const word relayPulse = 200; //ms - used with pulsed relay
//Soft power switches
@@ -62,11 +61,11 @@ const char ledPin = -1;
const byte unoffDur = 10; //sec
// How long (in ms) are the button hold durations?
-const word btnShortHold = 1000; //for setting the displayed feataure
-const word btnLongHold = 3000; //for for entering options menu
-const byte velThreshold = 150; //ms
+const word btnShortHold = 1000; //for entering setting mode, or hold-setting at low velocity
+const word btnLongHold = 3000; //for entering options menu, or hold-setting at high velocity
+const word velThreshold = 0; //ms
// When an adj up/down input (btn or rot) follows another in less than this time, value will change more (10 vs 1).
-// Recommend ~150 for rotaries. If you want to use this feature with buttons, extend to ~400.
+// 0 to disable. Recommend ~150 for rotaries. If you want to use this feature with buttons, extend to ~300.
// What is the "frame rate" of the tube cleaning and display scrolling? up to 65535 ms
const word cleanSpeed = 200; //ms
diff --git a/sixtube_lm/configs/v8c-6tube-relayswitch-pwm.h b/arduino-nixie/configs/v8c-6tube-relay.h
similarity index 87%
rename from sixtube_lm/configs/v8c-6tube-relayswitch-pwm.h
rename to arduino-nixie/configs/v8c-6tube-relay.h
index 2b1b6f2..c993714 100644
--- a/sixtube_lm/configs/v8c-6tube-relayswitch-pwm.h
+++ b/arduino-nixie/configs/v8c-6tube-relay.h
@@ -1,4 +1,4 @@
-//UNDB v8 modified to v9 spec (put Sel/Alt on A6/A7, Up/Down on A0/A1, relay on A3, led on 9, and cathode B4 on A2), buttons as labeled, with 6-digit display.
+//UNDB v8 modified to v9 spec (put Sel/Alt on A6/A7, Up/Down on A0/A1, relay on A3, led on 9, and cathode B4 on A2), relay enabled, buttons as labeled, with 6-digit display.
const byte displaySize = 6; //number of tubes in display module. Small display adjustments are made for 4-tube clocks
@@ -7,12 +7,11 @@ const byte fnIsTime = 0;
const byte fnIsDate = 1;
const byte fnIsAlarm = 2;
const byte fnIsTimer = 3;
-const byte fnIsDayCount = 4;
-const byte fnIsTemp = 5;
-const byte fnIsTubeTester = 6; //cycles all digits on all tubes 1/second, similar to anti-cathode-poisoning cleaner
+const byte fnIsTemp = 4;
+const byte fnIsTubeTester = 5; //cycles all digits on all tubes 1/second, similar to anti-cathode-poisoning cleaner
// functions enabled in this clock, in their display order. Only fnIsTime is required
-const byte fnsEnabled[] = {fnIsTime, fnIsDate, fnIsAlarm, fnIsTimer, fnIsDayCount}; //, fnIsTemp, fnIsTubeTester
-// To control which of these display persistently vs. switch back to Time after a few seconds, search "Temporary-display mode timeout"
+const byte fnsEnabled[] = {fnIsTime, fnIsDate, fnIsAlarm, fnIsTimer}; //, fnIsTemp, fnIsTubeTester
+// To control which of these display persistently vs. switch back to Time after a few seconds, search "Temporary-display function timeout"
// These are the RLB board connections to Arduino analog input pins.
// S1/PL13 = Reset
@@ -43,7 +42,7 @@ const byte relayMode = 0; //If relay is equipped, what does it do?
// 1 = pulsed mode: the relay will be pulsed, like the beeper is, to control an intermittent signaling device like a solenoid or indicator lamp. Specify pulse duration in relayPulse.
const word signalDur = 180; //sec - when pulsed signal is going, pulses are sent once/sec for this period (e.g. 180 = 3min)
const word switchDur = 7200; //sec - when alarm triggers switched relay, it's switched on for this period (e.g. 7200 = 2hr)
-const word piezoPulse = 500; //ms - used with piezo via tone()
+const word piezoPulse = 250; //ms - used with piezo via tone()
const word relayPulse = 200; //ms - used with pulsed relay
//Soft power switches
@@ -62,11 +61,11 @@ const char ledPin = 9;
const byte unoffDur = 10; //sec
// How long (in ms) are the button hold durations?
-const word btnShortHold = 1000; //for setting the displayed feataure
-const word btnLongHold = 3000; //for for entering options menu
-const byte velThreshold = 150; //ms
+const word btnShortHold = 1000; //for entering setting mode, or hold-setting at low velocity
+const word btnLongHold = 3000; //for entering options menu, or hold-setting at high velocity
+const word velThreshold = 0; //ms
// When an adj up/down input (btn or rot) follows another in less than this time, value will change more (10 vs 1).
-// Recommend ~150 for rotaries. If you want to use this feature with buttons, extend to ~400.
+// 0 to disable. Recommend ~150 for rotaries. If you want to use this feature with buttons, extend to ~300.
// What is the "frame rate" of the tube cleaning and display scrolling? up to 65535 ms
const word cleanSpeed = 200; //ms
diff --git a/arduino-nixie/configs/v8c-6tube-top-relay.h b/arduino-nixie/configs/v8c-6tube-top-relay.h
new file mode 100644
index 0000000..d215867
--- /dev/null
+++ b/arduino-nixie/configs/v8c-6tube-top-relay.h
@@ -0,0 +1,93 @@
+//UNDB v8 modified to v9 spec (put Sel/Alt on A7/A6, Up/Down on A0/A1, relay on A3, led on 9, and cathode B4 on A2), relay enabled, Sel/Alt buttons reversed, with 6-digit display.
+
+const byte displaySize = 6; //number of tubes in display module. Small display adjustments are made for 4-tube clocks
+
+// available clock functions, and unique IDs (between 0 and 200)
+const byte fnIsTime = 0;
+const byte fnIsDate = 1;
+const byte fnIsAlarm = 2;
+const byte fnIsTimer = 3;
+const byte fnIsTemp = 4;
+const byte fnIsTubeTester = 5; //cycles all digits on all tubes 1/second, similar to anti-cathode-poisoning cleaner
+// functions enabled in this clock, in their display order. Only fnIsTime is required
+const byte fnsEnabled[] = {fnIsTime, fnIsDate, fnIsAlarm, fnIsTimer}; //, fnIsTemp, fnIsTubeTester
+// To control which of these display persistently vs. switch back to Time after a few seconds, search "Temporary-display function timeout"
+
+// These are the RLB board connections to Arduino analog input pins.
+// S1/PL13 = Reset
+// S2/PL5 = A1
+// S3/PL6 = A0
+// S4/PL7 = A6
+// S5/PL8 = A3
+// S6/PL9 = A2
+// S7/PL14 = A7
+// A6-A7 are analog-only pins that aren't quite as responsive and require a physical pullup resistor (1K to +5V), and can't be used with rotary encoders because they don't support pin change interrupts.
+
+// What input is associated with each control?
+const byte mainSel = A7;
+const byte mainAdjUp = A0;
+const byte mainAdjDn = A1;
+const byte altSel = A6; //if not equipped, set to 0
+
+// What type of adj controls are equipped?
+// 1 = momentary buttons. 2 = quadrature rotary encoder (not currently supported).
+const byte mainAdjType = 1;
+
+//What are the signal pin(s) connected to?
+const char piezoPin = 10;
+const char relayPin = A3;
+// -1 to disable feature (no relay item equipped); A3 if equipped (UNDB v8)
+const byte relayMode = 0; //If relay is equipped, what does it do?
+// 0 = switched mode: the relay will be switched to control an appliance like a radio or light fixture. If used with timer, it will switch on while timer is running (like a "sleep" function). If used with alarm, it will switch on when alarm trips; specify duration of this in switchDur.
+// 1 = pulsed mode: the relay will be pulsed, like the beeper is, to control an intermittent signaling device like a solenoid or indicator lamp. Specify pulse duration in relayPulse.
+const word signalDur = 180; //sec - when pulsed signal is going, pulses are sent once/sec for this period (e.g. 180 = 3min)
+const word switchDur = 7200; //sec - when alarm triggers switched relay, it's switched on for this period (e.g. 7200 = 2hr)
+const word piezoPulse = 250; //ms - used with piezo via tone()
+const word relayPulse = 200; //ms - used with pulsed relay
+
+//Soft power switches
+const byte enableSoftAlarmSwitch = 1;
+// 1 = yes. Alarm can be switched on and off when clock is displaying the alarm time (fnIsAlarm).
+// 0 = no. Alarm will be permanently on. Use with switched relay if the appliance has its own switch on this relay circuit.
+const byte enableSoftPowerSwitch = 1; //works with switched relay only
+// 1 = yes. Relay can be switched on and off directly with Alt button at any time (except in options menu). This is useful if connecting an appliance (e.g. radio) that doesn't have its own switch, or if replacing the clock unit in a clock radio where the clock does all the switching (e.g. Telechron).
+// 0 = no. Use if the connected appliance has its own power switch (independent of this relay circuit) or does not need to be manually switched. In this case (and/or if there is no switched relay) Alt will act as a function preset.
+
+//LED circuit control with PWM
+const char ledPin = 9;
+// -1 to disable feature; 11 if equipped (UNDB v8 modded)
+
+//When display is dim/off, a press will light the tubes for how long?
+const byte unoffDur = 10; //sec
+
+// How long (in ms) are the button hold durations?
+const word btnShortHold = 1000; //for entering setting mode, or hold-setting at low velocity
+const word btnLongHold = 3000; //for entering options menu, or hold-setting at high velocity
+const word velThreshold = 0; //ms
+// When an adj up/down input (btn or rot) follows another in less than this time, value will change more (10 vs 1).
+// 0 to disable. Recommend ~150 for rotaries. If you want to use this feature with buttons, extend to ~300.
+
+// What is the "frame rate" of the tube cleaning and display scrolling? up to 65535 ms
+const word cleanSpeed = 200; //ms
+const word scrollSpeed = 100; //ms - e.g. scroll-in-and-out date at :30 - to give the illusion of a slow scroll that doesn't pause, use (timeoutTempFn*1000)/(displaySize+1) - e.g. 714 for displaySize=6 and timeoutTempFn=5
+
+// What are the timeouts for setting and temporarily-displayed functions? up to 65535 sec
+const unsigned long timeoutSet = 120; //sec
+const unsigned long timeoutTempFn = 5; //sec
+
+//This clock is 2x3 multiplexed: two tubes powered at a time.
+//The anode channel determines which two tubes are powered,
+//and the two SN74141 cathode driver chips determine which digits are lit.
+//4 pins out to each SN74141, representing a binary number with values [1,2,4,8]
+const char outA1 = 2;
+const char outA2 = 3;
+const char outA3 = 4;
+const char outA4 = 5;
+const char outB1 = 6;
+const char outB2 = 7;
+const char outB3 = 8;
+const char outB4 = 16; //A2 - was 9 before PWM fix pt2
+//3 pins out to anode channel switches
+const char anode1 = 11;
+const char anode2 = 12;
+const char anode3 = 13;
\ No newline at end of file
diff --git a/sixtube_lm/configs/v8c-6tube-relayswitch-pwm-top.h b/arduino-nixie/configs/v8c-6tube-top.h
similarity index 89%
rename from sixtube_lm/configs/v8c-6tube-relayswitch-pwm-top.h
rename to arduino-nixie/configs/v8c-6tube-top.h
index a86aa9d..43652df 100644
--- a/sixtube_lm/configs/v8c-6tube-relayswitch-pwm-top.h
+++ b/arduino-nixie/configs/v8c-6tube-top.h
@@ -7,12 +7,11 @@ const byte fnIsTime = 0;
const byte fnIsDate = 1;
const byte fnIsAlarm = 2;
const byte fnIsTimer = 3;
-const byte fnIsDayCount = 4;
-const byte fnIsTemp = 5;
-const byte fnIsTubeTester = 6; //cycles all digits on all tubes 1/second, similar to anti-cathode-poisoning cleaner
+const byte fnIsTemp = 4;
+const byte fnIsTubeTester = 5; //cycles all digits on all tubes 1/second, similar to anti-cathode-poisoning cleaner
// functions enabled in this clock, in their display order. Only fnIsTime is required
-const byte fnsEnabled[] = {fnIsTime, fnIsDate, fnIsAlarm, fnIsTimer, fnIsDayCount}; //, fnIsTemp, fnIsTubeTester
-// To control which of these display persistently vs. switch back to Time after a few seconds, search "Temporary-display mode timeout"
+const byte fnsEnabled[] = {fnIsTime, fnIsDate, fnIsAlarm, fnIsTimer}; //, fnIsTemp, fnIsTubeTester
+// To control which of these display persistently vs. switch back to Time after a few seconds, search "Temporary-display function timeout"
// These are the RLB board connections to Arduino analog input pins.
// S1/PL13 = Reset
@@ -43,7 +42,7 @@ const byte relayMode = 0; //If relay is equipped, what does it do?
// 1 = pulsed mode: the relay will be pulsed, like the beeper is, to control an intermittent signaling device like a solenoid or indicator lamp. Specify pulse duration in relayPulse.
const word signalDur = 180; //sec - when pulsed signal is going, pulses are sent once/sec for this period (e.g. 180 = 3min)
const word switchDur = 7200; //sec - when alarm triggers switched relay, it's switched on for this period (e.g. 7200 = 2hr)
-const word piezoPulse = 500; //ms - used with piezo via tone()
+const word piezoPulse = 250; //ms - used with piezo via tone()
const word relayPulse = 200; //ms - used with pulsed relay
//Soft power switches
@@ -62,11 +61,11 @@ const char ledPin = 9;
const byte unoffDur = 10; //sec
// How long (in ms) are the button hold durations?
-const word btnShortHold = 1000; //for setting the displayed feataure
-const word btnLongHold = 3000; //for for entering options menu
-const byte velThreshold = 150; //ms
+const word btnShortHold = 1000; //for entering setting mode, or hold-setting at low velocity
+const word btnLongHold = 3000; //for entering options menu, or hold-setting at high velocity
+const word velThreshold = 0; //ms
// When an adj up/down input (btn or rot) follows another in less than this time, value will change more (10 vs 1).
-// Recommend ~150 for rotaries. If you want to use this feature with buttons, extend to ~400.
+// 0 to disable. Recommend ~150 for rotaries. If you want to use this feature with buttons, extend to ~300.
// What is the "frame rate" of the tube cleaning and display scrolling? up to 65535 ms
const word cleanSpeed = 200; //ms
diff --git a/arduino-nixie/configs/v8c-6tube.h b/arduino-nixie/configs/v8c-6tube.h
new file mode 100644
index 0000000..d8164b9
--- /dev/null
+++ b/arduino-nixie/configs/v8c-6tube.h
@@ -0,0 +1,93 @@
+//UNDB v8 modified to v9 spec (put Sel/Alt on A6/A7, Up/Down on A0/A1, relay on A3, led on 9, and cathode B4 on A2), relay disabled, buttons as labeled, with 6-digit display.
+
+const byte displaySize = 6; //number of tubes in display module. Small display adjustments are made for 4-tube clocks
+
+// available clock functions, and unique IDs (between 0 and 200)
+const byte fnIsTime = 0;
+const byte fnIsDate = 1;
+const byte fnIsAlarm = 2;
+const byte fnIsTimer = 3;
+const byte fnIsTemp = 4;
+const byte fnIsTubeTester = 5; //cycles all digits on all tubes 1/second, similar to anti-cathode-poisoning cleaner
+// functions enabled in this clock, in their display order. Only fnIsTime is required
+const byte fnsEnabled[] = {fnIsTime, fnIsDate, fnIsAlarm, fnIsTimer}; //, fnIsTemp, fnIsTubeTester
+// To control which of these display persistently vs. switch back to Time after a few seconds, search "Temporary-display function timeout"
+
+// These are the RLB board connections to Arduino analog input pins.
+// S1/PL13 = Reset
+// S2/PL5 = A1
+// S3/PL6 = A0
+// S4/PL7 = A6
+// S5/PL8 = A3
+// S6/PL9 = A2
+// S7/PL14 = A7
+// A6-A7 are analog-only pins that aren't quite as responsive and require a physical pullup resistor (1K to +5V), and can't be used with rotary encoders because they don't support pin change interrupts.
+
+// What input is associated with each control?
+const byte mainSel = A6;
+const byte mainAdjUp = A0;
+const byte mainAdjDn = A1;
+const byte altSel = A7; //if not equipped, set to 0
+
+// What type of adj controls are equipped?
+// 1 = momentary buttons. 2 = quadrature rotary encoder (not currently supported).
+const byte mainAdjType = 1;
+
+//What are the signal pin(s) connected to?
+const char piezoPin = 10;
+const char relayPin = -1;
+// -1 to disable feature (no relay item equipped); A3 if equipped (UNDB v8)
+const byte relayMode = 0; //If relay is equipped, what does it do?
+// 0 = switched mode: the relay will be switched to control an appliance like a radio or light fixture. If used with timer, it will switch on while timer is running (like a "sleep" function). If used with alarm, it will switch on when alarm trips; specify duration of this in switchDur.
+// 1 = pulsed mode: the relay will be pulsed, like the beeper is, to control an intermittent signaling device like a solenoid or indicator lamp. Specify pulse duration in relayPulse.
+const word signalDur = 180; //sec - when pulsed signal is going, pulses are sent once/sec for this period (e.g. 180 = 3min)
+const word switchDur = 7200; //sec - when alarm triggers switched relay, it's switched on for this period (e.g. 7200 = 2hr)
+const word piezoPulse = 250; //ms - used with piezo via tone()
+const word relayPulse = 200; //ms - used with pulsed relay
+
+//Soft power switches
+const byte enableSoftAlarmSwitch = 1;
+// 1 = yes. Alarm can be switched on and off when clock is displaying the alarm time (fnIsAlarm).
+// 0 = no. Alarm will be permanently on. Use with switched relay if the appliance has its own switch on this relay circuit.
+const byte enableSoftPowerSwitch = 1; //works with switched relay only
+// 1 = yes. Relay can be switched on and off directly with Alt button at any time (except in options menu). This is useful if connecting an appliance (e.g. radio) that doesn't have its own switch, or if replacing the clock unit in a clock radio where the clock does all the switching (e.g. Telechron).
+// 0 = no. Use if the connected appliance has its own power switch (independent of this relay circuit) or does not need to be manually switched. In this case (and/or if there is no switched relay) Alt will act as a function preset.
+
+//LED circuit control with PWM
+const char ledPin = 9;
+// -1 to disable feature; 11 if equipped (UNDB v8 modded)
+
+//When display is dim/off, a press will light the tubes for how long?
+const byte unoffDur = 10; //sec
+
+// How long (in ms) are the button hold durations?
+const word btnShortHold = 1000; //for entering setting mode, or hold-setting at low velocity
+const word btnLongHold = 3000; //for entering options menu, or hold-setting at high velocity
+const word velThreshold = 0; //ms
+// When an adj up/down input (btn or rot) follows another in less than this time, value will change more (10 vs 1).
+// 0 to disable. Recommend ~150 for rotaries. If you want to use this feature with buttons, extend to ~300.
+
+// What is the "frame rate" of the tube cleaning and display scrolling? up to 65535 ms
+const word cleanSpeed = 200; //ms
+const word scrollSpeed = 100; //ms - e.g. scroll-in-and-out date at :30 - to give the illusion of a slow scroll that doesn't pause, use (timeoutTempFn*1000)/(displaySize+1) - e.g. 714 for displaySize=6 and timeoutTempFn=5
+
+// What are the timeouts for setting and temporarily-displayed functions? up to 65535 sec
+const unsigned long timeoutSet = 120; //sec
+const unsigned long timeoutTempFn = 5; //sec
+
+//This clock is 2x3 multiplexed: two tubes powered at a time.
+//The anode channel determines which two tubes are powered,
+//and the two SN74141 cathode driver chips determine which digits are lit.
+//4 pins out to each SN74141, representing a binary number with values [1,2,4,8]
+const char outA1 = 2;
+const char outA2 = 3;
+const char outA3 = 4;
+const char outA4 = 5;
+const char outB1 = 6;
+const char outB2 = 7;
+const char outB3 = 8;
+const char outB4 = 16; //A2 - was 9 before PWM fix pt2
+//3 pins out to anode channel switches
+const char anode1 = 11;
+const char anode2 = 12;
+const char anode3 = 13;
\ No newline at end of file
diff --git a/sports_button_lm/configs/v8c-6tube-relayswitch-pwm.h b/extras/sports_button_lm/configs/v8c-6tube-relayswitch-pwm.h
similarity index 100%
rename from sports_button_lm/configs/v8c-6tube-relayswitch-pwm.h
rename to extras/sports_button_lm/configs/v8c-6tube-relayswitch-pwm.h
diff --git a/sports_button_lm/sports_button_lm.ino b/extras/sports_button_lm/sports_button_lm.ino
similarity index 100%
rename from sports_button_lm/sports_button_lm.ino
rename to extras/sports_button_lm/sports_button_lm.ino
diff --git a/sports_reacttime_lm/configs/v8-cylinder.h b/extras/sports_reacttime_lm/configs/v8-cylinder.h
similarity index 100%
rename from sports_reacttime_lm/configs/v8-cylinder.h
rename to extras/sports_reacttime_lm/configs/v8-cylinder.h
diff --git a/sports_reacttime_lm/sports_reacttime_lm.ino b/extras/sports_reacttime_lm/sports_reacttime_lm.ino
similarity index 100%
rename from sports_reacttime_lm/sports_reacttime_lm.ino
rename to extras/sports_reacttime_lm/sports_reacttime_lm.ino
diff --git a/tube_test/tube_test.ino b/extras/tube_test/tube_test.ino
similarity index 100%
rename from tube_test/tube_test.ino
rename to extras/tube_test/tube_test.ino
diff --git a/v8c_mod_test/v8c_mod_test.ino b/extras/v8c_mod_test/v8c_mod_test.ino
similarity index 100%
rename from v8c_mod_test/v8c_mod_test.ino
rename to extras/v8c_mod_test/v8c_mod_test.ino