diff --git a/INSTRUCTIONS.md b/INSTRUCTIONS.md new file mode 100644 index 0000000..94297df --- /dev/null +++ b/INSTRUCTIONS.md @@ -0,0 +1,156 @@ +# Operating instructions, v2.0+ + +> [Instructions for earlier versions are here.](https://github.com/clockspot/arduino-clock/releases) To see your clock’s software version, hold **Select** briefly while powering up the clock. + +Your clock has four main functions: [time of day](#time-of-day), [calendar](#calendar), [alarm](#alarm), and [chrono/timer](#chronotimer). To cycle through these functions, press **Select**. + +To set anything, simply hold **Select** until the display blinks; use **Up/Down** to set, and **Select** to save. + +If your clock has an **Alt** button, it will act as a [function preset](#function-preset), [power button](#signals), and/or [Wi-Fi button](#wi-fi-support), depending on your clock’s features. + +Additional settings are available in the [settings menu](#settings-menu). If you have a Wi-Fi-enabled clock, you can configure these settings (and more) on the [settings page](#wi-fi-support), where you can also configure the clock to set itself. + +## Time of day + +The time of day can be [set](#settings-menu) to display in 12h or 24h format. + +* When setting, it is shown in 24h so you can tell AM from PM; and when the time is changed, the seconds will reset to zero. + +## Calendar + +The calendar cycles through several displays, before returning to the time of day: + +* **The date.** Several formats are available in the [settings menu](#settings-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. + * To disable the day counter (Wi-Fi clocks only), use the [settings page](#wi-fi-support). +* **Sunrise/sunset.** These two displays show the previous and next apparent sunrise/sunset times (indicated by `1` or `0` in place of seconds – during the day, it shows sunrise then sunset; at night, sunset then sunrise), in the same 12h/24h format as the time of day. + * Specify your latitude, longitude, and UTC offset in settings. + * To disable sunrise/sunset, set latitude/longitude to 0. +* _Future software versions will add support for weather forecasts for Wi-Fi-enabled clocks._ + +## Alarm + +The alarm is always shown in 24h format so you can tell AM from PM. In place of seconds, it displays `1`/`01`/`0` to indicate **on, skip, and off,** and the display dims when the alarm is off. + +* Use **Up/Down** to switch the alarm between **on, skip, and off,** indicated by high/medium/low beeps. + * **Skip** silences the next alarm in advance (hence the `01` meaning “off then on”) – useful if you’re taking a day off, or you wake up before your alarm. In the [settings menu](#settings-menu), you can set 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 to **on.** +* When the alarm [signals](#signals), 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/or the display will blink once). + * If the alarm is set to use a [switch signal](#signal), **Alt** will switch it off without snooze. +* **Fibonacci mode** wakes you gradually by starting the alarm about 27 minutes early, by beeping at increasingly shorter intervals per the [Fibonacci sequence](https://en.wikipedia.org/wiki/Fibonacci_number) (610 seconds, then 337, then 233...). This mode is enabled in the [settings menu](#settings-menu), and applies only to [beeper and pulse signals](#signals). + * Snooze does not take effect in this mode. Any button press will silence the alarm for the day, even if the set alarm time hasn’t been reached yet. + +## Chrono/Timer + +This feature can count up (chrono) or down (timer), up to 100 hours each way. When idle, it displays `0` (or if you have leading zeros enabled, `000000`). + +* To start and stop, press **Up**. + * While stopped, **Down** will reset to `0`. +* To use the chrono, start from `0`. + * While the chrono is running, **Down** will display a lap time. +* To use the timer, hold **Select** to set it. + * When setting, it will prompt for hours/minutes first, then seconds. For convenience, it will recall the last-used time – to reuse this time, simply press **Select** twice. Once the timer is set, press **Up** to start it. + * While the timer is running, **Down** will cycle through the runout options (what the timer will do when it runs out – clocks with [beeper signal](#signals) only): + * 1 beep: simply stop, with a long signal (default) + * 2 beeps: restart, with a short signal (makes a great interval timer!) + * 3 beeps: start the chrono, with a long signal + * 4 beeps: start the chrono, with a short signal +* When the timer [signals](#signals), press any button to silence it. +* You can switch displays while the chrono/timer is running, and it will continue to run in the background. It will reset to `0` if you switch displays while it’s stopped, if it’s stopped for an hour, if the chrono reaches 100 hours, or if power is lost. + +If your clock uses a knob (rotary encoder) for **Up/Down** rather than buttons, the controls differ slightly: + +* **Up** will start; **Down** will stop. +* While the chrono is running, **Up** will display a lap time. +* While the timer is running, **Up** will cycle through the runout options. +* To reset to `0`, switch to another display while stopped. (**Down** does nothing while stopped, to prevent accidental resets.) + +## Signals + +Your clock can trigger signals for the time of day (chime), alarm, and timer. If your clock supports multiple signal types, you can choose which type is triggered by each function in the [settings menu](#settings-menu). + +* **Beeper** signal is played on a piezo beeper, using various patterns and pitches. +* **Switch** signal switches on and off to control an appliance circuit (such as for a radio). If supported, **Alt** acts as a power switch to toggle it on and off manually. + * If the alarm is set to use this, it will switch on for up to two hours. **Alt** will switch it off without snooze (unlike the other buttons, which will trigger snooze as usual). + * If the timer is set to use this, it will switch on while the timer is running, like a “sleep” function. +* **Pulse** signal simply sends short pulses (such as to ring a bell). + +## Function preset + +If your clock supports _neither_ [switch signal](#signals) nor [Wi-Fi](#wi-fi-support), **Alt** acts as a function preset button. + +* While viewing the display you want quick access to (such as the alarm or chrono/timer), hold **Alt** until it beeps and the display blinks once; then you can use **Alt** to jump straight there. + * TIP: If **Alt** is set as the alarm preset, it will switch the alarm as well – so you can check and switch it with a few presses of a single button. + +## Settings menu + +* To enter the settings menu, hold **Select** for 3 seconds until the hour displays `1`. This indicates setting number 1. +* Use **Up/Down** to go to the setting number you want to set (see table below); press **Select** to open it for setting (display will blink); use **Up/Down** to set; and **Select** to save. +* When all done, hold **Select** to exit the menu. +* If you have a Wi-Fi-enabled clock, you can configure these settings (and more) on the [settings page](#wi-fi-support). + +| | Setting | Options | +| --- | --- | --- | +| | **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
The weekday is displayed as a number from 0 (Sunday) to 6 (Saturday).
Four-digit displays will show 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 zeros | 0 = no
1 = yes | +| 5 | Digit fade | 0–20, in hundredths of a second
(Clocks with nixie display 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.
If you observe DST but your locale’s rules are not represented here, leave this set to 0 and set the clock manually (and the [DST offset](#settingsgeography) if applicable). | +| 7 | Backlight | 0 = always off
1 = always on
2 = on until night/away shutoff (if enabled)
3 = on when alarm/timer signals
4 = on with [switch signal](#signals) (if equipped)
(Clocks with backlighting only) | +| 8 | Anti-cathode poisoning | Briefly cycles all nixie tubes 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. Clocks with nixie display only) | +| | **Alarm** | (Clocks with signals 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 (uses pitch and pattern below)
1 = switch (will stay on for 2 hours)
2 = pulse
(Clocks with multiple signal types 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 beeper pattern | 0 = long (1/2-second beep)
1 = short (1/4-second beep)
2 = double (two 1/8-second beeps)
3 = triple (three 1/12-second beeps)
4 = quad (four 1/16-second beeps)
5 = cuckoo (two 1/8-second beeps, descending major third)
(Clocks with beeper only) | +| 14 | Alarm snooze | 0–60 minutes. 0 disables snooze. | +| 15 | [Fibonacci mode](#alarm) | 0 = off
1 = on
(Clocks with [beeper or pulse signals](#signals) only) +| | **Chrono/Timer** | | +| 21 | Timer signal | 0 = beeper (uses pitch and pattern below)
1 = switch (will stay on until timer runs down)
2 = pulse
(Clocks with multiple signal types only) | +| 22 | Timer beeper pitch | [Note number](https://en.wikipedia.org/wiki/Piano_key_frequencies), from 49 (A4) to 88 (C8).
(Clocks with beeper only) | +| 23 | Timer beeper pattern | Same options as alarm beeper pattern.
(Clocks with beeper only) | +| | **Chime** | | +| 30 | Chime | Make noise on the hour:
0 = off
1 = single pulse
2 = [six pips](https://en.wikipedia.org/wiki/Greenwich_Time_Signal) (overrides pitch and pattern settings)
3 = pulse 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 signals](#signals) only) | +| 31 | Chime signal | 0 = beeper (uses pitch and pattern below)
2 = pulse
(Clocks with [beeper and pulse signals](#signals) only) | +| 32 | Chime beeper pitch | [Note number](https://en.wikipedia.org/wiki/Piano_key_frequencies), from 49 (A4) to 88 (C8).
(Clocks with beeper only) | +| 33 | Chime beeper pattern | Same options as alarm beeper pattern. Cuckoo recommended!
(Clocks with beeper only) | +| | **Night/away shutoff** | | +| 40 | Night shutoff | To save display life and/or preserve your sleep, dim or shut off the display nightly when you’re not around or sleeping.
0 = none (fully on)
1 = dim
2 = shut off
When off, you can press **Select** to illuminate the display 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 shutoff | To further save display life, shut off the display during daytime hours when you’re not around. This feature is designed to accommodate your work schedule.
0 = none (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 display 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`.)
If you observe DST but set the clock manually rather than using the [auto DST feature](#settingsgeneral), you must add an hour to the UTC offset during DST, or the sunrise/sunset times will be an hour early. | + +To reset the clock to “factory” settings, hold **Select** for 10 seconds while powering up the clock. You will see the time reset to 0:00. + +## Wi-Fi support + +If your clock is Wi-Fi-enabled, it offers a settings webpage that duplicates the menu above (and more), and can set itself by synchronizing to an [NTP time server](https://en.wikipedia.org/wiki/Time_server). + +**To activate the settings page,** grab a device with a web browser, and briefly hold **Alt**. + +* If the clock **is not** connected to Wi-Fi, it will display `7777`. + * This indicates it is broadcasting a Wi-Fi network called “Clock.” Connect your device to “Clock” and browse to [7.7.7.7](http://7.7.7.7). + +* If the clock **is** connected to Wi-Fi, it will flash its IP address (as a series of four numbers). + * Connect your device to the same Wi-Fi network as the clock, and browse to that IP address. + * If you don’t know what Wi-Fi network the clock is connected to, hold **Alt** for 10 seconds. It will disconnect and broadcast the “Clock” network instead, as above. + +When Wi-Fi settings are changed, the clock will immediately attempt to reconnect. As above, it will display its IP address if successful, or `7777` if not. Reconnect to the clock to continue configuring it. + +For security, the settings page and “Clock” network (if applicable) will deactivate after two minutes of inactivity. + +> If your clock has no **Alt** button, hold **Select** for 5 seconds to activate the page, and 10 seconds to force the “Clock” network. + +**When NTP sync is enabled,** the clock will attempt to synchronize every hour (at minute 59). If the clock displays the time without seconds, this indicates the time displayed was not synced to NTP in the last 24 hours. Check to make sure the clock is connected to Wi-Fi and configured to use a valid NTP server, and try a manual sync. If no Wi-Fi is available or you’re unable to sync for other reasons (such as network limitations or manual time setting), disable Wi-Fi or NTP sync to restore the seconds display. \ No newline at end of file diff --git a/README.md b/README.md index ddd29dc..ffed8ee 100644 --- a/README.md +++ b/README.md @@ -1,161 +1,60 @@ -# arduino-nixie - -![Nixie clocks](https://i.imgur.com/FemMWax.jpg) - -**A digital clock for the Arduino Nano and a nixie tube display.** - -* Features perpetual calendar with day counter/sunrise/sunset, alarm with skip/snooze, and chrono/timer. -* Supports four- or six-digit displays of Nixie tubes multiplexed in pairs via two SN74141 driver chips. -* Can do auto DST change, tube shutoff, and chimes, and can control PWM LEDs, piezo beeper, and/or 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+. - -[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. - -# Operating instructions, v1.7+ - -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 [time of day](#time-of-day), [calendar](#calendar), [alarm](#alarm), and [chrono/timer](#chronotimer). -* To set anything, simply hold **Select** until the display blinks; use **Up/Down** to set, and **Select** to save. Additional settings are available in the [options menu](#options-menu). - -## Time of day - -The time of day [can be set to show](#optionsgeneral) in 12h or 24h format, 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](#optionsgeneral) are available. 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), in the same 12h/24h format as the time of day. - * Specify your latitude, longitude, and UTC offset in the [options menu](#optionsgeography). (From v1.8.1, sunrise/sunset is not displayed if latitude/longitude are left at 0.) - * 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 - -The alarm is always shown in 24h format so you can tell AM from PM. - -* 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). - * If your clock has an **Alt** button and it’s [set as the alarm preset](#the-alt-button), it will switch the alarm as well – so you can check and switch it with a few presses of a single button. -* **Skip** silences the next alarm in advance – useful if you’re taking a day off, or you wake up before your alarm. You can [set the alarm to skip automatically](#optionsalarm) 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. -* 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). - * In [**Fibonacci mode**](#optionsalarm), snooze will not take effect; any button press will silence the alarm for the day, even if the set alarm time hasn’t been reached yet. - -> **Hardware variations** -> * If your clock has a [switched relay](#hardware-configuration) and the alarm is [set to use it](#optionsalarm), it will work like a clock radio, and switch on for two hours. In this case, the **Alt** button will “switch off” the relay immediately, without triggering snooze (as the other buttons will still do). - -## Chrono/Timer - -This feature can count up (chrono) or down (timer), up to 100 hours each way. When idle, it displays `0` (or if you have leading zeros enabled, `000000`). - -* To start and stop, press **Up**. - * When at `0`, this will start the chrono. - * While the chrono is running, **Down** will briefly display a lap time. -* To reset to `0`, press **Down** when stopped. -* To set the timer, hold **Select**. It will prompt for hours/minutes first, then seconds. For convenience, it will recall the last-used time – to reuse this time, simply press **Select** twice. Once the timer is set, press **Up** to start it. - * While the timer is running, **Down** will cycle through the runout options (what the timer will do when it runs out – clocks with beeper only): - * 1 beep: simply stop, with a long signal (default) - * 2 beeps: restart, with a short signal (makes a great interval timer!) - * 3 beeps: start the chrono, with a long signal - * 4 beeps: start the chrono, with a short signal -* When the timer signal sounds, press any button to silence it. -* You can switch displays while the chrono/timer is running, and it will continue to run in the background. It will reset to `0` if you switch displays while it’s stopped, if it’s stopped for an hour, if the chrono reaches 100 hours, or if power is lost. - -> **Hardware variations** -> * If your clock has a [switched relay](#hardware-configuration) and the chrono/timer is [set to use it](#optionstimer), it will switch on while the timer is running, like the “sleep” function on a clock radio. The runout options will still work, but won’t signal. -> * If your clock does not have a beeper, the runout options cannot be set. -> * If your clock uses a rotary encoder for **Up/Down** rather than buttons, while running, **Up** will display lap times (chrono) and cycle through runout options (timer), and **Down** will stop. To prevent accidental resets, **Down** does nothing while stopped. To reset to `0`, simply switch to another display while stopped. - -## The Alt button - -* If your clock has an **Alt** button, it typically works as a preset button. While viewing the display you want quick access to (such as the alarm or chrono/timer), hold **Alt** until it beeps and the display blinks once; then you can use **Alt** to jump straight there. - * TIP: If **Alt** is set as the alarm preset, it will switch the alarm as well – so you can check and switch it with a few presses of a single button. - -> **Hardware variation** -> * If your clock has a [switched relay](#hardware-configuration) with soft power switch enabled, **Alt** acts as that switch, like the power button on a clock radio. This is why, if the alarm is set to use the relay as well, **Alt** will “switch off” the alarm without triggering snooze. - -## Options menu - -* 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 blink); use **Up/Down** to set; and **Select** to save. -* When all done, hold **Select** to exit the options menu. - -| | Option | Settings | -| --- | --- | --- | -| | **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
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 zeros | 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)
If the clock is not powered at the time, it will correct itself when powered up.
If you observe DST but your locale’s rules are not represented here, leave this set to 0 and set the clock manually (and the [DST offset](#optionsgeography) if applicable). | -| 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 sounds
4 = 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 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 (uses pitch and pattern below)
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](https://en.wikipedia.org/wiki/Piano_key_frequencies), from 49 (A4) to 88 (C8).
(Clocks with beeper only) | -| 13 | Alarm beeper pattern | 0 = long (1/2-second beep)
1 = short (1/4-second beep)
2 = double (two 1/8-second beeps)
3 = triple (three 1/12-second beeps)
4 = quad (four 1/16-second beeps)
5 = cuckoo (two 1/8-second beeps, descending major third)
(Clocks with beeper only) | -| 14 | Alarm snooze | 0–60 minutes. 0 disables snooze. | -| 15 | Fibonacci mode | 0 = off
1 = on
To wake you more gradually, the alarm will start about 27 minutes early, by beeping at increasingly shorter intervals per the [Fibonacci sequence](https://en.wikipedia.org/wiki/Fibonacci_number) (610 seconds, then 337, then 233...). In this mode, snooze does not take effect; any button press will silence the alarm for the day, even if the set alarm time hasn’t been reached yet. Has no effect when alarm is set to use switched relay.
(Clocks with beeper and/or pulse relay only) -| | **Chrono/Timer** | | -| 21 | Timer signal | 0 = beeper (uses pitch and pattern below)
1 = relay (if in switch mode, will stay on until timer runs down)
(Clocks with both beeper and relay only) | -| 22 | Timer beeper pitch | [Note number](https://en.wikipedia.org/wiki/Piano_key_frequencies), from 49 (A4) to 88 (C8).
(Clocks with beeper only) | -| 23 | Timer beeper pattern | Same options as alarm beeper pattern.
(Clocks with beeper only) | -| | **Chime** | | -| 30 | Chime | Make noise on the hour:
0 = off
1 = single pulse
2 = [the pips](https://en.wikipedia.org/wiki/Greenwich_Time_Signal) (overrides pitch and pattern settings)
3 = pulse 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 (uses pitch and pattern below)
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).
(Clocks with beeper only) | -| 33 | Chime beeper pattern | Same options as alarm beeper pattern. Cuckoo recommended!
(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 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`.)
If you observe DST but set the clock manually rather than using the [auto DST feature](#optionsgeneral), you must add an hour to the UTC offset during DST, or the sunrise/sunset times will be an hour early. | - -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, 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: - -* **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 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 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 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 - -**To compile the sketch,** ensure these libraries are installed in your Arduino IDE via Library Manager, as needed: - -* Wire (Arduino built-in) -* EEPROM (Arduino built-in) -* [DS3231](https://github.com/NorthernWidget/DS3231) by NorthernWidget -* [Dusk2Dawn](https://github.com/dmkishi/Dusk2Dawn) by DM Kishi - if sunrise/sunset display is enabled -* [Encoder](https://github.com/PaulStoffregen/Encoder) by Paul Stoffregen - if rotary encoder is equipped - -**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 +# Universal Arduino Digital Clock + +![Arduino clocks](https://theclockspot.com/arduino-clock.jpg) + +## Operating instructions + +[The latest operating instructions (v2.0+) can be found here.](https://github.com/clockspot/arduino-clock/blob/master/INSTRUCTIONS.md) + +[Instructions for earlier versions are here.](https://github.com/clockspot/arduino-clock/releases) + +To see your clock’s software version, hold **Select** briefly while powering up the clock. + +## About + +**A universal digital clock codebase for Arduino,** maintained by [Luke](https://theclockspot.com). + +* **Time of day** with automatic DST change and chimes. +* **Perpetual calendar** with day counter and local sunrise/sunset times. +* **Alarm** with snooze and automatic weekday/weekend skipping. +* **Chronograph and timer** with reset/interval options. +* Runs on both classic Arduino Nano (AVR) and Nano 33 IoT (SAMD21). +* Supports **web-based config and NTP sync** over Wi-Fi on Nano 33 IoT. +* **Simple control** via three/four buttons, a rotary encoder, and/or Nano 33 IoT’s [IMU](https://en.wikipedia.org/wiki/Inertial_measurement_unit) (tilt control). +* **Signals** via piezo beeper, switch (e.g. appliance timer), and/or pulse (e.g. bell ringer). +* Supports **Nixie displays** of two SN74141 chips driving four/six tubes, with anti-cathode poisoning. +* Supports **LED displays** of three/four MAX7219 chips (via SPI) driving 8x8 LED matrices ([example](https://www.amazon.com/HiLetgo-MAX7219-Arduino-Microcontroller-Display/dp/B07FFV537V/)). +* Scheduled nightly/weekly **display dim/shutoff** and switchable backlighting with optional PWM fade. +* Timekeeping can be internal, or based on a DS3231 RTC (via I2C) for reliability/accuracy. +* Settings stored persistently in case of power loss, and mirrored in RAM in case of EEPROM/flash failure. + +Written to support [RLB Designs’](http://rlb-designs.com/) Universal Nixie Driver Board (UNDB): + +* Backlighting (PWM LED) supported on UNDB v8+ +* Switch and pulse signals supported on UNDB v9+ +* Nano 33 IoT support coming on future versions + +[The latest release can be downloaded here.](https://github.com/clockspot/arduino-clock/releases) Please note [known bugs and to-dos.](https://github.com/clockspot/arduino-clock/blob/master/TODO.md) + +# Configuration, compilation, and upload + +Various options, such as enabled functionality, RTC, display, I/O pins, timeouts, and control behaviors, are specified in a config file. This allows you to maintain configs for multiple clock hardware profiles, and simply include the relevant config at the top of `arduino-clock.h` before compiling. Several [example configs](https://github.com/clockspot/arduino-clock/tree/master/arduino-clock/configs) are provided, and [`~sample.h`](https://github.com/clockspot/arduino-clock/blob/master/arduino-clock/configs/%7Esample.h) includes all possible options with detailed comments. + +You may also wish to adjust the defaults for the clock’s user-configurable values to best suit its intended use, in case the user performs a hard reset. Some of these are specified in the config; others, for now, are hardcoded in `arduino-clock.ino` (`optsDef[]` for [settings](https://github.com/clockspot/arduino-clock/blob/master/INSTRUCTIONS.md#settings-menu) and `initEEPROM()` for other values). + +I use the Arduino IDE to compile and upload, due to the use of various Arduino and Arduino-oriented libraries. Make sure the relevant libraries are installed in the Library Manager, per the config in use. + +* EEPROM (Arduino) for AVR Arduinos (e.g. classic Nano) +* SPI (Arduino) and [LedControl](http://wayoda.github.io/LedControl) for MAX7219-based displays +* [Encoder](https://github.com/PaulStoffregen/Encoder) if rotary encoder is used for Up/Down inputs +* Arduino_LSM6DS3 (Arduino) if using Nano 33 IoT’s IMU for inputs +* WiFiNINA and WiFiUdp (Arduino) for Wi-Fi and NTP sync support on Nano 33 IoT +* [FlashStorage](https://github.com/cmaglie/FlashStorage/) for persistent storage on Nano 33 IoT +* Wire (Arduino) and [DS3231](https://github.com/NorthernWidget/DS3231) if using DS3231 RTC (via I2C) +* [Dusk2Dawn](https://github.com/dmkishi/Dusk2Dawn) if sunrise/sunset display is enabled + * Note: At this writing, for Nano 33 IoT, it’s necessary to download this library as .ZIP and [add manually](https://www.arduino.cc/en/guide/libraries#toc4), as the version in the Library Manager [is old](https://forum.arduino.cc/index.php?topic=479550.msg3852574#msg3852574) and, in my experience, will not compile for SAMD. + +Before compiling and uploading, you will need to select the correct board, port, and (for AVR) processor in the IDE’s Tools menu. + +* If your Arduino does not appear as a port option, you may have a clone that requires [drivers for the CH340 chipset](https://sparks.gogo.co.nz/ch340.html). +* If upload fails for an ATMega328P Arduino (e.g. classic Nano), try selecting/unselecting “Old Bootloader” in the processor menu. \ No newline at end of file diff --git a/TODO.md b/TODO.md index dc004b6..df94cb2 100644 --- a/TODO.md +++ b/TODO.md @@ -1,22 +1,28 @@ # To-dos -* 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? +* Startup SEL hold doesn't seem to work with IMU +* Reintroduce nixie clean and scroll +* Persistent storage on EEPROM - find a solution that writes to flash less often +* Network/NTP + * Why does the page sometimes drop? + * wi-fi credential save fails if keys are part of the string? + * DST calc may behave unpredictably between 1–2am on fallback day + * Redo NTP if it failed (networkStartWifi()) - per bad response - how to make it wait a retry period + * Make [2036-ready](https://en.wikipedia.org/wiki/Year_2038_problem#Network_Time_Protocol_timestamps) + * Notice when a leap second is coming and handle it + * When setting page is used to set day counter and date, and the month changes, set date max. For 2/29 it should just do 3/1 probably. + * Weather support + * Stop using strings? There is plenty of RAM available on SAMD I think +* Input: support other IMU orientations +* When day counter is set to count up from 12/31, override to display 365/366 on that date +* Bitmask to enable/disable features? * 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 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 +* I2C multicolor LED to indicate which function we're in? - possibly as part of display +* Metronome function? +* 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 (is this still applicable?) * Why does the display flicker sometimes? are we doubling up on a display cycle call? See other TODOs throughout code. \ No newline at end of file diff --git a/arduino-clock/arduino-clock.h b/arduino-clock/arduino-clock.h new file mode 100644 index 0000000..fce55ac --- /dev/null +++ b/arduino-clock/arduino-clock.h @@ -0,0 +1,81 @@ +#ifndef ARDUINO_CLOCK_H +#define ARDUINO_CLOCK_H + +////////// Hardware configuration ////////// +//Include the config file that matches your hardware setup. If needed, duplicate an existing one. + +#include "configs/undb-v9.h" + +//////////////////////////////////////////// + + +//Unique IDs for the functions - see also fnScroll +#define FN_TOD 0 //time of day +#define FN_CAL 1 //date, with optional day counter and sunrise/sunset pages +#define FN_ALARM 2 //alarm time +#define FN_TIMER 3 //countdown timer and chronograph +#define FN_THERM 4 //temperature per rtc – will likely read high +#define FN_TUBETEST 5 //simply cycles all digits for nixie tube testing +#define FN_OPTS 201 //fn values from here to 255 correspond to settings in the settings menu + +void setup(); +void loop(); +void ctrlEvt(byte ctrl, byte evt, byte evtLast, bool velocity=0); +void fnScroll(byte dir); +void fnOptScroll(byte dir); +void goToFn(byte thefn, byte thefnPg=0); +void switchAlarmState(byte dir); +void setAlarmState(byte state); +byte getAlarmState(); +void switchPower(byte dir); +void startSet(int n, int m, int x, byte p); +void doSet(int delta); +void clearSet(); +bool initEEPROM(bool hard); +void findFnAndPageNumbers(); +void checkRTC(bool force); +void fibonacci(byte h, byte m, byte s); +void autoDST(); +bool isDST(int y, byte m, byte d); +bool isDSTByHour(int y, byte m, byte d, byte h, bool setFlag); +byte nthSunday(int y, byte m, byte nth); +byte daysInMonth(word y, byte m); +int daysInYear(word y); +int dateToDayCount(word y, byte m, byte d); +byte dayOfWeek(word y, byte m, byte d); +int dateComp(int y, byte m, byte d, byte mt, byte dt, bool countUp); +bool isTimeInRange(word tstart, word tend, word ttest); +bool isDayInRange(byte dstart, byte dend, byte dtest); +void millisCheckDrift(); +void millisApplyDrift(); +void millisReset(); +unsigned long ms(); +void timerStart(); +void timerStop(); +void timerClear(); +void timerLap(); +void timerRunoutToggle(); +void cycleTimer(); +void timerSleepSwitch(bool on); +byte getTimerState(); +void setTimerState(char pos, bool val); +void tempDisplay(int i0, int i1=0, int i2=0, int i3=0); +void updateDisplay(); +void calcSun(); +void displaySun(byte which, int d, int tod); +void displayWeather(byte which); +void initOutputs(); +void signalStart(byte sigFn, byte sigDur); +void signalStop(); +void cycleSignal(); +word getSignalPitch(); +word getHz(byte note); +byte getSignalOutput(); +byte getSignalPattern(); +void quickBeep(int pitch); +void quickBeepPattern(int source, int pattern); +void updateBacklight(); +void cycleBacklight(); +byte getVersionPart(byte part); + +#endif //ARDUINO_CLOCK_H \ No newline at end of file diff --git a/arduino-nixie/arduino-nixie.ino b/arduino-clock/arduino-clock.ino similarity index 51% rename from arduino-nixie/arduino-nixie.ino rename to arduino-clock/arduino-clock.ino index bd64ce1..004afa6 100644 --- a/arduino-nixie/arduino-nixie.ino +++ b/arduino-clock/arduino-clock.ino @@ -1,54 +1,64 @@ -// Digital clock code for the Arduino Nano in RLB Designs' Universal Nixie Driver Board -// featuring timekeeping by DS3231 RTC, driving up to six digits multiplexed 3x2 via two SN74141 driver chips -// Sketch by Luke McKenzie (luke@theclockspot.com) - https://github.com/clockspot/arduino-nixie +// Universal digital clock codebase for Arduino - https://github.com/clockspot/arduino-clock +// Sketch by Luke McKenzie (luke@theclockspot.com) +// Written to support RLB Designs’ Universal Nixie Driver Board // Inspired by original sketch by Robin Birtles (rlb-designs.com) and Chris Gerekos -// Display cycling code derived from http://arduinix.com/Main/Code/ANX-6Tube-Clock-Crossfade.txt -////////// Hardware configuration ////////// -//Include the config file that matches your hardware setup. If needed, duplicate an existing one. - -#include "configs/v9-6tube.h" +#include +#include "arduino-clock.h"; ////////// Software version ////////// -const byte vMajor = 1; -const byte vMinor = 9; +const byte vMajor = 2; +const byte vMinor = 0; const byte vPatch = 0; +const bool vDev = 0; -////////// Other includes, global consts, and vars ////////// -#include //Arduino - GNU LPGL -#include //Arduino - GNU LPGL -#include //NorthernWidget - The Unlicense - install in your Arduino IDE -#if ENABLE_DATE_RISESET - #include //DM Kiski - unlicensed - install in your Arduino IDE if needed -#endif -#if CTRL_UPDN_TYPE==2 //rotary encoder - #include //Paul Stoffregen - install in your Arduino IDE if needed +////////// Includes ////////// + +// These modules are used per the available hardware and features enabled in the config file. +// The disp and rtc options are mutually exclusive and define the same functions. +// Because the Arduino IDE preprocessor seems to #include without regard to #if blocks (see https://forum.arduino.cc/index.php?topic=134226.0), I don't have #ifdef blocks around these header file inclusions. Instead I simply include them all, and have #ifdef blocks around the corresponding cpp code so only the specified code is compiled. It's dumb, but it works. + +#if ENABLE_DATE_RISESET //this probably doesn't work, per the above, but ¯\_(ツ)_/¯ + #include //DM Kishi - unlicensed - install in your Arduino IDE if needed - test without #endif +#include "storage.h" //for persistent storage - supports both AVR EEPROM and SAMD flash +#include "dispNixie.h" //if DISP_NIXIE is defined in config - for a SN74141-multiplexed nixie array +#include "dispMAX7219.h" //if DISP_MAX7219 is defined in config - for a SPI MAX7219 8x8 LED array +#include "rtcDS3231.h" //if RTC_DS3231 is defined in config – for an I2C DS3231 RTC module +#include "rtcMillis.h" //if RTC_MILLIS is defined in config – for a fake RTC based on millis +#include "input.h" //for Sel/Alt/Up/Dn - supports buttons, rotary control, and Nano 33 IoT IMU +#include "network.h" //if not AVR – enables WiFi/web-based config/NTP sync on Nano 33 IoT WiFiNINA + -/*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. -Setting values are byte (0 to 255) or int (-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 byte 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 +////////// Variables and storage ////////// -These ones are set outside the options menu (defaults defined in initEEPROM()): +/*Some variables are backed by persistent storage. These are referred to by their location in that storage. See storage.cpp. + +Values for these are bytes (0 to 255) or signed 16-bit ints (-32768 to 32767) where high byte is loc and low byte is loc+1. In updateDisplay(), special setting formats are deduced from the max value (max 1439 is a time of day, max 156 is a UTC offset, etc) and whether a value is an int or byte is deduced from whether the max value > 255. + +IMPORTANT! If adding more variables, be sure to increase STORAGE_SPACE in storage.cpp (and use the ones marked [free] below first). + +These ones are set outside the settings menu (defaults defined in initEEPROM() where applicable): 0-1 Alarm time, mins - 2 Alarm on (mirrors volatile var) + 2 Alarm on 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 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) + const unsigned int FN_ENAB_TIMER = 1<<0; //1 + const unsigned int FN_ENAB_DAYCOUNT = 1<<1; //2 + const unsigned int FN_ENAB_SUN = 1<<2; //4 + const unsigned int FN_ENAB_WEATHER = 1<<3; //8 + 9 NTP sync on + 15 DST on (last known state - set indirectly via time sets and such) + 51-54 NTP server IP address (4 bytes) + 55-86 Wi-Fi SSID (32 bytes) + 87-150 Wi-Fi WPA passphrase/key or WEP key (64 bytes) + 151 Wi-Fi WEP key index -These ones are set inside the options menu (defaults defined in arrays below). +These ones are set inside the settings 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 @@ -58,12 +68,12 @@ Some are skipped when they wouldn't apply to a given clock's hardware config, se 18 Display date during time 19 Leading zeros in time hour, calendar, and chrono/timer 20 Digit fade duration - 21 Strike - skipped when no piezo and relay is switch (start=0) + 21 Strike - piezo or pulse signal only (start=0) 22 Auto DST 23 Alarm days 24 Alarm snooze - 25 [free] - formerly Timer interval mode - skipped when no piezo and relay is switch (start=0) - 26 LED circuit behavior - skipped when no led pin + 25 [free] - formerly Timer interval mode (now a volatile var) + 26 Backlight behavior - skipped when no backlight pin 27 Night shutoff 28-29 Night start, mins 30-31 Night end, mins @@ -72,57 +82,36 @@ Some are skipped when they wouldn't apply to a given clock's hardware config, se 34 Last day of workweek 35-36 Work starts at, mins 37-38 Work ends at, mins - 39 Alarm beeper pitch - skipped when no piezo - 40 Timer beeper pitch - skipped when no piezo - 41 Strike beeper pitch - skipped when no piezo - 42 Alarm signal, 0=beeper, 1=relay - skipped when no relay (start=0) or no piezo (start=0) - 43 Timer signal - skipped when no relay (start=0) or no piezo (start=1) - 44 Strike signal - skipped when no pulse relay (start=0) or no piezo (start=1) + 39 Alarm beeper pitch - piezo signal only + 40 Timer beeper pitch - piezo signal only + 41 Strike beeper pitch - piezo signal only + 42 Alarm signal (0=piezo, 1=switch, 2=pulse) + 43 Timer signal + 44 Strike signal 45 Temperature format - skipped when !ENABLE_TEMP_FN TODO also useful for weather display 46 Anti-cathode poisoning - 47 Alarm beeper pattern - skipped when no piezo - 48 Timer beeper pattern - skipped when no piezo - 49 Strike beeper pattern - skipped when no piezo + 47 Alarm beeper pattern - piezo signal only + 48 Timer beeper pattern - piezo signal only + 49 Strike beeper pattern - piezo signal only 50 Alarm Fibonacci mode */ -//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 shutoff Lat Long UTC±* +//Settings menu numbers (displayed in UI and readme), locs, and default/min/max/current values. +//Setting numbers/order can be changed (though try to avoid for user convenience); +//but locs should be maintained so AVR EEPROM doesn't need reset after an upgrade (SAMD does it anyway). +// General Alarm Timer Strike Night and away shutoff Geo const byte optsNum[] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10,11,12,13,14,15, 21,22,23, 30,31,32,33, 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,47,24,50, 43,40,48, 21,44,41,49, 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,76, 4, 9, 0, 0,76, 2, 0, 0,68, 5, 0,1320, 360, 0, 1, 5, 480,1080, 0, 0,100}; const int optsMin[] = { 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0,49, 0, 0, 0, 0,49, 0, 0, 0,49, 0, 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, 5,60, 1, 1,88, 5, 4, 1,88, 5, 2,1439,1439, 2, 6, 6,1439,1439, 900, 1800,156}; +const int optsMax[] = { 2, 5, 3, 1,20, 6, 4, 2, 1, 2, 2,88, 5,60, 1, 2,88, 5, 4, 2,88, 5, 2,1439,1439, 2, 6, 6,1439,1439, 900, 1800,156}; -//RTC objects -DS3231 ds3231; //an object to access the ds3231 specifically (temp, etc) -RTClib rtc; //an object to access a snapshot of the ds3231 via rtc.now() -DateTime tod; //stores the rtc.now() snapshot for several functions to use -byte toddow; //stores the day of week (read separately from ds3231 dow counter) - -// Hardware inputs and value setting -byte btnCur = 0; //Momentary button currently in use - only one allowed at a time -byte btnCurHeld = 0; //Button hold thresholds: 0=none, 1=unused, 2=short, 3=long, 4=set by btnStop() -unsigned long inputLast = 0; //When a button was last pressed -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. -#if CTRL_UPDN_TYPE==2 //rotary encoder - Encoder rot(CTRL_DN,CTRL_UP); -#endif +//The rest of these variables are not backed by persistent storage, so they are regular named vars. // Functions and pages -// Unique IDs - see also fnScroll -const byte fnIsTime = 0; //time of day -const byte fnIsDate = 1; //date, with optional day counter and sunrise/sunset pages -const byte fnIsAlarm = 2; //alarm time -const byte fnIsTimer = 3; //countdown timer and chronograph -const byte fnIsTemp = 4; //temperature per DS3231 – will likely read high -const byte fnIsTest = 5; //simply cycles all tubes -const byte fnOpts = 201; //fn values from here to 255 correspond to options in the options menu byte fn = 0; //currently displayed fn per above 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 +byte fnSetPg = 0; //whether this function is currently being set, and which page it's on int fnSetVal; //the value currently being set, if any int fnSetValMin; //min possible int fnSetValMax; //max possible @@ -139,10 +128,8 @@ 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; //which function triggered the signal - fnIsTime (chime), fnIsAlarm, or fnIsTimer +byte signalSource = 0; //which function triggered the signal - FN_TOD (chime), FN_ALARM, or FN_TIMER byte signalPattern = 0; //the pattern for that source word signalRemain = 0; //alarm/timer signal timeout counter, seconds word snoozeRemain = 0; //snooze timeout counter, seconds @@ -153,181 +140,128 @@ unsigned long timerTime = 0; //timestamp of timer target / chrono origin (while unsigned long timerLapTime = 0; const byte millisCorrectionInterval = 30; //used to calibrate millis() to RTC for timer/chrono purposes unsigned long millisAtLastCheck = 0; -word unoffRemain = 0; //un-off (briefly turn on tubes during full night/away shutoff) timeout counter, seconds +word unoffRemain = 0; //un-off (briefly turn on display during full night/away shutoff) timeout counter, seconds byte displayDim = 2; //dim per display or function: 2=normal, 1=dim, 0=off -byte cleanRemain = 0; //anti-cathode-poisoning clean timeout counter, increments at CLEAN_SPEED ms (see loop()). Start at 11 to run at clock startup -int8_t scrollRemain = 0; //"frames" of scroll – signed byte - 0=not scrolling, >0=coming in, <0=going out, -128=scroll out at next change. -byte versionRemain = 3; //display version at start +bool versionShowing = false; //display version if Select held at start - until it is released or long-held +//If we need to temporarily display a value (or values in series), we can put them here. Can't be zero. +//This is used by network to display IP addresses, and various other bits. +int tempValDispQueue[4]; +const int tempValDispDur = 2500; //ms +unsigned int tempValDispLast = 0; + +#define SHOW_IRRELEVANT_OPTIONS 0 //whether to show everything in settings menu and page (network) ////////// Main code control ////////// void setup(){ - Serial.begin(9600); - Wire.begin(); - initInputs(); - delay(100); //prevents the below from firing in the event there's a capacitor stabilizing the input, which can read low falsely - initEEPROM(readInput(CTRL_SEL)==LOW); //Do a hard init of EEPROM if button is held; else do a soft init to make sure vals in range - //Some options need to be set to a fixed value per the configuration. - //These options will also be skipped in fnOptScroll so the user can't change them. - if(RELAY_PIN<0 || PIEZO_PIN<0) { //If no relay or no piezo, set each signal output to [if no relay, then piezo; else relay] - writeEEPROM(42,(RELAY_PIN<0?0:1),false); //alarm - writeEEPROM(43,(RELAY_PIN<0?0:1),false); //timer - writeEEPROM(44,(RELAY_PIN<0?0:1),false); //strike + // Serial.begin(9600); + // #ifndef __AVR__ //SAMD only + // while(!Serial); + // #endif + rtcInit(); + initStorage(); //pulls persistent storage data into volatile vars - see storage.cpp + byte changed = initEEPROM(false); //do a soft init to make sure vals in range + initDisplay(); + initOutputs(); //depends on some EEPROM settings + if(initInputs()){ //inits inputs and returns true if CTRL_SEL is held + versionShowing = 1; + //skip network for now, since wifi connect hangs - we'll do it after version is done + } else { + if(networkSupported()) initNetwork(); } - if(PIEZO_PIN>=0 && RELAY_MODE==0) { //If piezo and switched relay - writeEEPROM(44,0,false); //strike forced to piezo + + //Some settings need to be set to a fixed value per the configuration. + //These settings will also be skipped in fnOptScroll so the user can't change them. + + //Signals: if the current eeprom selection is not available, + //try to use the default specified in the config, failing to pulse and then switch (alarm/timer only) + if((readEEPROM(42,false)==0 && PIEZO_PIN<0) || (readEEPROM(42,false)==1 && SWITCH_PIN<0) || (readEEPROM(42,false)==2 && PULSE_PIN<0)) + changed += writeEEPROM(42,(ALARM_SIGNAL==0 && PIEZO_PIN>=0? 0: (ALARM_SIGNAL==2 && PULSE_PIN>=0? 2: 1)),false,false); //alarm + if((readEEPROM(43,false)==0 && PIEZO_PIN<0) || (readEEPROM(43,false)==1 && SWITCH_PIN<0) || (readEEPROM(43,false)==2 && PULSE_PIN<0)) + changed += writeEEPROM(43,(TIMER_SIGNAL==0 && PIEZO_PIN>=0? 0: (TIMER_SIGNAL==2 && PULSE_PIN>=0? 2: 1)),false,false); //timer + if((readEEPROM(44,false)==0 && PIEZO_PIN<0) || (readEEPROM(44,false)==1 && SWITCH_PIN<0) || (readEEPROM(44,false)==2 && PULSE_PIN<0)) + changed += writeEEPROM(44,(CHIME_SIGNAL==0 && PIEZO_PIN>=0? 0: 2),false,false); //chime + + if((PIEZO_PIN<0 && SWITCH_PIN<0 && PULSE_PIN<0) || !ENABLE_ALARM_FN){ //can't do alarm + changed += writeEEPROM(2,0,false,false); //force alarm off + changed += writeEEPROM(23,0,false,false); //force autoskip off + changed += writeEEPROM(50,0,false,false); //force fibonacci off + } else { //ok to do alarm + if(!ENABLE_SOFT_ALARM_SWITCH) changed += writeEEPROM(2,1,false,false); //no soft alarm switch: force alarm on + if(!ENABLE_SOFT_ALARM_SWITCH || !ENABLE_ALARM_AUTOSKIP) changed += writeEEPROM(23,0,false,false); //no soft switch or no autoskip: force autoskip off + if((PIEZO_PIN<0 && PULSE_PIN<0) || !ENABLE_ALARM_FIBONACCI) changed += writeEEPROM(50,0,false,false); //no fibonacci, or no piezo or pulse: force fibonacci off } - if(PIEZO_PIN<0 && RELAY_MODE==0) { //If switched relay and no piezo - writeEEPROM(21,0,false); //turn off strike - writeEEPROM(50,0,false); //turn off fibonacci mode + + if((PIEZO_PIN<0 && PULSE_PIN<0) || !ENABLE_TIME_CHIME){ //can't do chime + changed += writeEEPROM(21,0,false,false); //force chime off } - if(!ENABLE_ALARM_FN) alarmOn = 0; //if alarm is disabled in config - else if(!ENABLE_SOFT_ALARM_SWITCH) alarmOn = 1; //force alarm on if software switch is disabled - else alarmOn = (readEEPROM(2,false)>0); //otherwise set alarm per EEPROM backup + switch(readEEPROM(7,false)){ //if the preset is set to a function that is no longer enabled, use alarm if enabled, else use time - case fnIsDate: if(!ENABLE_DATE_FN) writeEEPROM(7,(ENABLE_ALARM_FN?fnIsAlarm:fnIsTime),false); break; - case fnIsAlarm: if(!ENABLE_ALARM_FN) writeEEPROM(7,fnIsTime,false); break; - case fnIsTimer: if(!ENABLE_TIMER_FN) writeEEPROM(7,(ENABLE_ALARM_FN?fnIsAlarm:fnIsTime),false); break; - case fnIsTemp: if(!ENABLE_TEMP_FN) writeEEPROM(7,(ENABLE_ALARM_FN?fnIsAlarm:fnIsTime),false); break; - case fnIsTest: if(!ENABLE_TUBETEST_FN) writeEEPROM(7,(ENABLE_ALARM_FN?fnIsAlarm:fnIsTime),false); break; - default: writeEEPROM(7,(ENABLE_ALARM_FN?fnIsAlarm:fnIsTime),false); break; + case FN_CAL: if(!ENABLE_DATE_FN) changed += writeEEPROM(7,(ENABLE_ALARM_FN?FN_ALARM:FN_TOD),false,false); break; + case FN_ALARM: if(!ENABLE_ALARM_FN) changed += writeEEPROM(7,FN_TOD,false,false); break; + case FN_TIMER: if(!ENABLE_TIMER_FN) changed += writeEEPROM(7,(ENABLE_ALARM_FN?FN_ALARM:FN_TOD),false,false); break; + case FN_THERM: if(!ENABLE_TEMP_FN) changed += writeEEPROM(7,(ENABLE_ALARM_FN?FN_ALARM:FN_TOD),false,false); break; + case FN_TUBETEST: if(!ENABLE_TUBETEST_FN) changed += writeEEPROM(7,(ENABLE_ALARM_FN?FN_ALARM:FN_TOD),false,false); break; + default: changed += writeEEPROM(7,(ENABLE_ALARM_FN?FN_ALARM:FN_TOD),false,false); break; } - if(!ENABLE_ALARM_AUTOSKIP) writeEEPROM(23,0,false); //alarm autoskip off - if(!ENABLE_ALARM_FIBONACCI) writeEEPROM(50,0,false); //fibonacci off - if(!ENABLE_TIME_CHIME) writeEEPROM(21,0,false); //chime off - if(!ENABLE_SHUTOFF_NIGHT) writeEEPROM(27,0,false); //night shutoff off - if(!ENABLE_SHUTOFF_AWAY) writeEEPROM(32,0,false); //away shutoff off - 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 + if(!ENABLE_SHUTOFF_NIGHT) changed += writeEEPROM(27,0,false,false); //night shutoff off + if(!ENABLE_SHUTOFF_AWAY) changed += writeEEPROM(32,0,false,false); //away shutoff off + //if backlight circuit is not switched (v5.0 board), the backlight menu setting (eeprom 26) doesn't matter + if(changed) commitEEPROM(); //for SAMD + findFnAndPageNumbers(); //initial values - initOutputs(); //depends on some EEPROM settings } void loop(){ //Every loop cycle, check the RTC and inputs (previously polled, but works fine without and less flicker) - checkEffects(false); //cleaning and scrolling display effects - not handled by checkRTC since they have their own timing + //checkEffects(false); //cleaning and scrolling display effects - not handled by checkRTC since they have their own timing checkRTC(false); //if clock has ticked, decrement timer if running, and updateDisplay millisApplyDrift(); checkInputs(); //if inputs have changed, this will do things + updateDisplay as needed - #if CTRL_UPDN_TYPE==1 //buttons - doSetHold(false); //if inputs have been held, this will do more things + updateDisplay as needed - #endif + if(networkSupported()) cycleNetwork(); cycleTimer(); - cycleDisplay(); //keeps the display hardware multiplexing cycle going - cycleLEDs(); + cycleDisplay(displayDim,fnSetPg); //keeps the display hardware multiplexing cycle going + cycleBacklight(); cycleSignal(); } -////////// Control inputs ////////// -void initInputs(){ - //TODO are there no "loose" pins left floating after this? per https://electronics.stackexchange.com/q/37696/151805 - pinMode(CTRL_SEL, INPUT_PULLUP); - pinMode(CTRL_UP, INPUT_PULLUP); - pinMode(CTRL_DN, INPUT_PULLUP); - #if CTRL_ALT!=0 - pinMode(CTRL_ALT, INPUT_PULLUP); - #endif -} +////////// Input handling and value setting ////////// -void checkInputs(){ - //TODO potential issue: if user only means to rotate or push encoder but does both? - checkBtn(CTRL_SEL); //select - #if CTRL_UPDN_TYPE==1 //buttons - checkBtn(CTRL_UP); checkBtn(CTRL_DN); - #endif - #if CTRL_UPDN_TYPE==2 //rotary encoder - checkRot(); - #endif - #if CTRL_ALT!=0 //alt (if equipped) - checkBtn(CTRL_ALT); - #endif -} +void ctrlEvt(byte ctrl, byte evt, byte evtLast, bool velocity){ + //Handle control events from inputs, based on current fn and set state. + //evt: 1=press, 2=short hold, 3=long hold, 4=verylong, 5=superlong, 0=release. + //We only handle press evts for up/down ctrls, as that's the only evt encoders generate, + //and input.cpp sends repeated presses if up/down buttons are held. + //But for sel/alt (always buttons), we can handle different hold states here. -bool readInput(byte pin){ - if(pin==A6 || pin==A7) return analogRead(pin)<100?0:1; //analog-only pins - else return digitalRead(pin); -} -void checkBtn(byte btn){ - //Changes in momentary buttons, LOW = pressed. - //When a button event has occurred, will call ctrlEvt - bool bnow = readInput(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; inputLast = now; inputLastTODMins = tod.hour()*60+tod.minute(); - ctrlEvt(btn,1); //hey, the button has been pressed - } - //If the button is being held... - if(btnCur==btn && bnow==LOW) { - if((unsigned long)(now-inputLast)>=CTRL_HOLD_LONG_DUR && btnCurHeld < 3) { //account for rollover - btnCurHeld = 3; - ctrlEvt(btn,3); //hey, the button has been long-held - } - else if((unsigned long)(now-inputLast)>=CTRL_HOLD_SHORT_DUR && btnCurHeld < 2) { - btnCurHeld = 2; - ctrlEvt(btn,2); //hey, the button has been short-held - } - } - //If the button has just been released... - if(btnCur==btn && bnow==HIGH) { - btnCur = 0; - if(btnCurHeld < 4) ctrlEvt(btn,0); //hey, the button was released - btnCurHeld = 0; - } -} -void btnStop(){ - //In some cases, when handling btn evt 1/2/3, we may call this so following events 2/3/0 won't cause unintended behavior (e.g. after a fn change, or going in or out of set) - btnCurHeld = 4; -} - -bool rotVel = 0; //high velocity setting (x10 rather than x1) -#if CTRL_UPDN_TYPE==2 //rotary encoder -unsigned long rotLastStep = 0; //timestamp of last completed step (detent) -int rotLastVal = 0; -void checkRot(){ - //Changes in rotary encoder. When rotation(s) occur, will call ctrlEvt to simulate btn presses. During setting, ctrlEvt will take rotVel into account. - int rotCurVal = rot.read(); - if(rotCurVal!=rotLastVal){ //we've sensed a state change - rotLastVal = rotCurVal; - if(rotCurVal>=4 || rotCurVal<=-4){ //we've completed a step of 4 states (this library doesn't seem to drop states much, so this is reasonably reliable) - unsigned long now = millis(); - inputLast = now; inputLastTODMins = tod.hour()*60+tod.minute(); - if((unsigned long)(now-rotLastStep)<=ROT_VEL_START) rotVel = 1; //kick into high velocity setting (x10) - else if((unsigned long)(now-rotLastStep)>=ROT_VEL_STOP) rotVel = 0; //fall into low velocity setting (x1) - rotLastStep = now; - while(rotCurVal>=4) { rotCurVal-=4; ctrlEvt(CTRL_UP,1); } - while(rotCurVal<=-4) { rotCurVal+=4; ctrlEvt(CTRL_DN,1); } - rot.write(rotCurVal); + //If the version display is showing, ignore all else until Sel is released (cancel) or long-held (cancel and eeprom reset) + if(versionShowing){ + if(ctrl==CTRL_SEL && (evt==0 || evt==5)){ //SEL release or superlong hold + if(evt==5){ initEEPROM(true); commitEEPROM(); } //superlong hold: reset EEPROM + versionShowing = false; inputStop(); updateDisplay(); + if(networkSupported()) initNetwork(); //we didn't do this earlier since the wifi connect makes the clock hang + return; + } else { + return; //ignore other controls } - } -} //end checkRot() -#endif - - -////////// Input handling and value setting ////////// - -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). + } //end if versionShowing //If the signal is going, any press should silence it if(signalRemain>0 && evt==1){ signalStop(); - if(signalSource==fnIsAlarm) { //If this was the alarm - //If the alarm is using the switched relay and this is the Alt button; or if alarm is *not* using the switched relay and this is Fibonacci mode; don't set the snooze - if((RELAY_MODE==0 && readEEPROM(42,false)==1 && CTRL_ALT!=0 && ctrl==CTRL_ALT) || (readEEPROM(50,false) && !(RELAY_PIN>=0 && RELAY_MODE==0 && readEEPROM(42,false)==1))) { + if(signalSource==FN_ALARM) { //If this was the alarm + //If the alarm is using the switch signal and this is the Alt button; or if alarm is *not* using the switch signal and this is Fibonacci mode; don't set the snooze + if((readEEPROM(42,false)==1 && CTRL_ALT>0 && ctrl==CTRL_ALT) || (readEEPROM(42,false)!=1 && readEEPROM(50,false))) { quickBeep(64); //Short signal to indicate the alarm has been silenced until tomorrow displayBlink(); //to indicate this as well } else { //start snooze snoozeRemain = readEEPROM(24,false)*60; //snoozeRemain is seconds, but snooze duration is minutes } } - btnStop(); + inputStop(); return; } //If the snooze is going, any press should cancel it, with a signal @@ -335,49 +269,57 @@ void ctrlEvt(byte ctrl, byte evt){ snoozeRemain = 0; quickBeep(64); //Short signal to indicate the alarm has been silenced until tomorrow displayBlink(); //to indicate this as well - btnStop(); - return; - } - //If the clean is going, any press should cancel it, with a display update - if(cleanRemain>0 && evt==1){ - cleanRemain = 0; - btnStop(); - updateDisplay(); - return; - } - //If a scroll is waiting to scroll out, cancel it, and let the button event do what it will - if(scrollRemain==-128 && evt==1){ - scrollRemain = 0; - } - //If a scroll is going, fast-forward to end of scroll in/out - see also checkRTC - else if(scrollRemain!=0 && evt==1){ - btnStop(); - if(scrollRemain>0) scrollRemain = 1; - else scrollRemain = -1; - checkEffects(true); - return; - } - //If the version display is going, any press should cancel it, with a display update - if(versionRemain>0 && evt==1){ - versionRemain = 0; - btnStop(); - updateDisplay(); + inputStop(); return; } + // //TODO NIXIE + // //If the clean is going, any press should cancel it, with a display update + // if(cleanRemain>0 && evt==1){ + // cleanRemain = 0; + // inputStop(); + // updateDisplay(); + // return; + // } + // //TODO NIXIE?? + // //If a scroll is waiting to scroll out, cancel it, and let the button event do what it will + // if(scrollRemain==-128 && evt==1){ + // scrollRemain = 0; + // } + // //If a scroll is going, fast-forward to end of scroll in/out - see also checkRTC + // else if(scrollRemain!=0 && evt==1){ + // inputStop(); + // if(scrollRemain>0) scrollRemain = 1; + // else scrollRemain = -1; + // checkEffects(true); + // return; + // } //Is it a press for an un-off? unoffRemain = UNOFF_DUR; //always do this so continued button presses during an unoff keep it alive if(displayDim==0 && evt==1) { updateDisplay(); - btnStop(); + inputStop(); return; } - if(fn < fnOpts) { //normal fn running/setting (not in options menu) + if(networkSupported()){ + //Short hold, Alt; or very long hold, Sel if no Alt: start admin + if((evt==2 && ctrl==CTRL_ALT)||(evt==4 && ctrl==CTRL_SEL && CTRL_ALT<=0)) { + networkStartAdmin(); + return; + } + //Super long hold, Alt, or Sel if no Alt: start AP (TODO would we rather it forget wifi?) + if(evt==5 && (ctrl==CTRL_ALT || (ctrl==CTRL_SEL && CTRL_ALT<=0))) { + networkStartAP(); + return; + } + } + + if(fn < FN_OPTS) { //normal fn running/setting (not in settings menu) - if(evt==3 && ctrl==CTRL_SEL) { //CTRL_SEL long hold: enter options menu - btnStop(); - fn = fnOpts; + if(evt==3 && ctrl==CTRL_SEL) { //CTRL_SEL long hold: enter settings menu + //inputStop(); commented out to to enable evt==4 and evt==5 per above + fn = FN_OPTS; clearSet(); //don't need updateDisplay() here because this calls updateRTC with force=true return; } @@ -385,11 +327,11 @@ void ctrlEvt(byte ctrl, byte evt){ if(!fnSetPg) { //fn running if(evt==2 && ctrl==CTRL_SEL) { //CTRL_SEL hold: enter setting mode switch(fn){ - case fnIsTime: //set mins - startSet((tod.hour()*60)+tod.minute(),0,1439,1); break; - case fnIsDate: //depends what page we're on + case FN_TOD: //set mins + startSet(rtcGetTOD(),0,1439,1); break; + case FN_CAL: //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); + fnSetValDate[1]=rtcGetMonth(), fnSetValDate[2]=rtcGetDate(); startSet(rtcGetYear(),2000,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 @@ -397,14 +339,14 @@ void ctrlEvt(byte ctrl, byte evt){ } else if(fnPg==fnDateWeathernow || fnDateWeathernext){ //temperature units?? //TODO } break; - case fnIsAlarm: //set mins + case FN_ALARM: //set mins startSet(readEEPROM(0,true),0,1439,1); break; - case fnIsTimer: //set mins + case FN_TIMER: //set mins if(timerTime!=0 || timerState&1) { timerClear(); } // updateDisplay(); break; } //If the timer is nonzero or running, zero it. But rather than stop there, just go straight into setting – since adjDn (or cycling fns) can reset to zero startSet(timerInitialMins,0,5999,1); break; //minutes //fnIsDayCount removed in favor of paginated calendar - case fnIsTemp: //could do calibration here if so inclined - case fnIsTest: + case FN_THERM: //could do calibration here if so inclined + case FN_TUBETEST: default: break; } return; @@ -412,19 +354,20 @@ void ctrlEvt(byte ctrl, byte evt){ else if((ctrl==CTRL_SEL && evt==0) || ((ctrl==CTRL_UP || ctrl==CTRL_DN) && evt==1)) { //sel release or adj press //we can't handle sel press here because, if attempting to enter setting mode, it would switch the fn first if(ctrl==CTRL_SEL){ //sel release - if(fn==fnIsTimer && !(timerState&1)) timerClear(); //if timer is stopped, clear it + //Serial.println(F("sel release")); + if(fn==FN_TIMER && !(timerState&1)) timerClear(); //if timer is stopped, clear it 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==CTRL_UP || ctrl==CTRL_DN) { - if(fn==fnIsAlarm) switchAlarm(ctrl==CTRL_UP?1:0); //switch alarm - if(fn==fnIsTimer){ + if(fn==FN_ALARM) switchAlarmState(ctrl==CTRL_UP?1:0); //switch alarm + if(fn==FN_TIMER){ if(ctrl==CTRL_UP){ if(!(timerState&1)){ //stopped timerStart(); } else { //running - #if CTRL_UPDN_TYPE==2 //rotary encoder + #ifdef INPUT_UPDN_ROTARY if((timerState>>1)&1) timerLap(); //chrono: lap else timerRunoutToggle(); //timer: runout option #else //button @@ -433,7 +376,7 @@ void ctrlEvt(byte ctrl, byte evt){ } } else { //CTRL_DN if(!(timerState&1)){ //stopped - #if CTRL_UPDN_TYPE==1 //buttons + #ifdef INPUT_UPDN_BUTTONS timerClear(); //if we wanted to reset to the previous time, we could use this; but sel hold is easy enough to get there // //same as //save timer secs @@ -445,7 +388,7 @@ void ctrlEvt(byte ctrl, byte evt){ updateDisplay(); #endif } else { //running - #if CTRL_UPDN_TYPE==2 //rotary encoder + #ifdef INPUT_UPDN_ROTARY timerStop(); #else if((timerState>>1)&1) timerLap(); //chrono: lap @@ -453,21 +396,25 @@ void ctrlEvt(byte ctrl, byte evt){ #endif } } - } //end if fnIsTimer - //if(fn==fnIsTime) TODO volume in I2C radio + } //end if FN_TIMER + //if(fn==FN_TOD) TODO volume in I2C radio } //else do nothing } //end sel release or adj press - else if(CTRL_ALT>0 && ctrl==CTRL_ALT) { //alt sel press - //if switched relay, and soft switch enabled, we'll switch power. - if(ENABLE_SOFT_POWER_SWITCH && RELAY_PIN>=0 && RELAY_MODE==0) { switchPower(2); btnStop(); } - //Otherwise, this becomes our function preset. - else { - //On long hold, if this is not currently the preset, we'll set it, double beep, and btnStop. + else if(CTRL_ALT>0 && ctrl==CTRL_ALT) { + //if soft power switch, we'll switch on release - but only if not held past activating settings page/AP + if(ENABLE_SOFT_POWER_SWITCH && SWITCH_PIN>=0) { + //If holds are used to activate network stuff, and we've passed those thresholds, do not switch. + //Otherwise, switch no matter how long held. + if(evt==0 && !(networkSupported() && evtLast<2)) switchPower(2); + } + //If neither soft power switch nor network support, this becomes our function preset + else if(!networkSupported()) { + //On long hold, if this is not currently the preset, we'll set it, double beep, and inputStop. //(Decided not to let this button set things, because then it steps on the toes of Sel's functionality.) if(evt==2) { if(readEEPROM(7,false)!=fn) { - btnStop(); + inputStop(); writeEEPROM(7,fn,false); quickBeep(76); displayBlink(); @@ -475,45 +422,45 @@ void ctrlEvt(byte ctrl, byte evt){ } //On short release, jump to the preset fn. else if(evt==0) { - btnStop(); + inputStop(); if(fn!=readEEPROM(7,false)) fn=readEEPROM(7,false); else { //Special case: if this is the alarm, toggle the alarm switch - if(fn==fnIsAlarm) switchAlarm(2); + if(fn==FN_ALARM) switchAlarmState(2); } fnPg = 0; //reset page counter in case we were in a paged display updateDisplay(); } } - } + } //end alt } //end fn running else { //fn setting if(evt==1) { //press //TODO could we do release/shorthold on CTRL_SEL 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 + //currently no, because we don't inputStop() when short hold goes into fn setting, in case long hold may go to settings menu //so we can't handle a release because it would immediately save if releasing from the short hold. - //Consider recording the btn start time when going into fn setting so we can distinguish its release from a future one - if(ctrl==CTRL_SEL) { //CTRL_SEL push: go to next option or save and exit setting mode - btnStop(); //not waiting for CTRL_SELHold, so can stop listening here - //We will set ds3231 time parts directly + //Consider recording the input start time when going into fn setting so we can distinguish its release from a future one + //TODO the above can be revisited now that we pass evtLast + if(ctrl==CTRL_SEL) { //CTRL_SEL push: go to next setting or save and exit setting mode + inputStop(); //not waiting for CTRL_SELHold, so can stop listening here + //We will set rtc time parts directly //con: potential for very rare clock rollover while setting; pro: can set date separate from time switch(fn){ - case fnIsTime: //save in RTC + case FN_TOD: //save in RTC if(fnSetValDid){ //but only if the value was actually changed - ds3231.setHour(fnSetVal/60); - ds3231.setMinute(fnSetVal%60); - ds3231.setSecond(0); + rtcSetTime(fnSetVal/60,fnSetVal%60,0); + if(networkSupported()) clearNTPSyncLast(); millisAtLastCheck = 0; //see ms() - calcSun(tod.year(),tod.month(),tod.day()); - isDSTByHour(tod.year(),tod.month(),tod.day(),fnSetVal/60,true); + calcSun(); + isDSTByHour(rtcGetYear(),rtcGetMonth(),rtcGetDate(),fnSetVal/60,true); } clearSet(); break; - case fnIsDate: //depends what page we're on + case FN_CAL: //depends what page we're on if(fnPg==0){ //regular date display: save in RTC switch(fnSetPg){ case 1: //save year, set month - displayBlink(); //to indicate save. Safe b/c we've btnStopped. See below for why + displayBlink(); //to indicate save. Safe b/c we've inputStopped. See below for why fnSetValDate[0]=fnSetVal; startSet(fnSetValDate[1],1,12,2); break; case 2: //save month, set date @@ -521,12 +468,11 @@ void ctrlEvt(byte ctrl, byte evt){ 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); + rtcSetDate(fnSetValDate[0],fnSetValDate[1],fnSetVal, + dayOfWeek(fnSetValDate[0],fnSetValDate[1],fnSetVal)); + if(networkSupported()) clearNTPSyncLast(); + calcSun(); + isDSTByHour(fnSetValDate[0],fnSetValDate[1],fnSetVal,rtcGetHour(),true); clearSet(); break; default: break; } @@ -540,8 +486,9 @@ void ctrlEvt(byte ctrl, byte evt){ case 2: //save date, set direction displayBlink(); //to indicate save. writeEEPROM(6,fnSetVal,false); - startSet(readEEPROM(4,false),0,1,3); break; + startSet(readEEPROM(4,false),1,2,3); break; case 3: //save date + displayBlink(); //to indicate save. writeEEPROM(4,fnSetVal,false); clearSet(); break; default: break; @@ -552,10 +499,10 @@ void ctrlEvt(byte ctrl, byte evt){ //TODO } break; - case fnIsAlarm: + case FN_ALARM: writeEEPROM(0,fnSetVal,true); clearSet(); break; - case fnIsTimer: //timer - depends what page we're on + case FN_TIMER: //timer - depends what page we're on switch(fnSetPg){ case 1: //save timer mins, set timer secs displayBlink(); //to indicate save. @@ -568,68 +515,62 @@ void ctrlEvt(byte ctrl, byte evt){ if(timerTime!=0){ bitWrite(timerState,1,0); //set timer direction (bit 1) to down (0) //timerStart(); //we won't automatically start, we'll let the user do that - //TODO: in timer radio mode, skip setting setting the seconds (display placeholder) and start when done. May even want to skip runout options even if the beeper is there. Or could make it an option in the settings file. + //TODO: in timer radio mode, skip setting the seconds (display placeholder) and start when done. May even want to skip runout options even if the beeper is there. Or could make it an option in the config file. } clearSet(); break; default: break; } break; //fnIsDayCount removed in favor of paginated calendar - case fnIsTemp: + case FN_THERM: break; default: break; } //end switch fn } //end CTRL_SEL push - if(ctrl==CTRL_UP) doSet(rotVel ? 10 : 1); - if(ctrl==CTRL_DN) doSet(rotVel ? -10 : -1); + if(ctrl==CTRL_UP) doSet(velocity ? 10 : 1); + if(ctrl==CTRL_DN) doSet(velocity ? -10 : -1); } //end if evt==1 - else if(evt==2){ //short hold - trigger doSetHold directly for better timing - #if CTRL_UPDN_TYPE==1 //buttons - if(ctrl==CTRL_UP || ctrl==CTRL_DN) doSetHold(true); - #endif - } } //end fn setting } //end normal fn running/setting - else { //options menu setting - to/from EEPROM + else { //settings menu setting - to/from EEPROM - byte opt = fn-fnOpts; //current option index + byte opt = fn-FN_OPTS; //current setting index - if(evt==2 && ctrl==CTRL_SEL) { //CTRL_SEL short hold: exit options menu - btnStop(); + if(evt==2 && ctrl==CTRL_SEL) { //CTRL_SEL short hold: exit settings menu + inputStop(); //if we were setting a value, writes setting val to EEPROM if needed if(fnSetPg) writeEEPROM(optsLoc[opt],fnSetVal,optsMax[opt]>255?true:false); - fn = fnIsTime; + fn = FN_TOD; //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); + calcSun(); //TODO pull from clock + isDSTByHour(rtcGetYear(),rtcGetMonth(),rtcGetDate(),rtcGetHour(),true); clearSet(); return; } - if(!fnSetPg){ //viewing option number - if(ctrl==CTRL_SEL && evt==0) { //CTRL_SEL release: enter option value setting + if(!fnSetPg){ //setting number + if(ctrl==CTRL_SEL && evt==0 && evtLast<3) { //CTRL_SEL release (but not after holding to get into the menu): enter setting value startSet(readEEPROM(optsLoc[opt],optsMax[opt]>255?true:false),optsMin[opt],optsMax[opt],1); } if(ctrl==CTRL_UP && evt==1) fnOptScroll(1); //next one up or cycle to beginning if(ctrl==CTRL_DN && evt==1) fnOptScroll(0); //next one down or cycle to end? updateDisplay(); - } //end viewing option number + } //end setting number - else { //setting option value - if(ctrl==CTRL_SEL && evt==0) { //CTRL_SEL release: save and exit option value setting - //Writes setting val to EEPROM if needed + else { //setting value + if(ctrl==CTRL_SEL && evt==0) { //CTRL_SEL release: save value and exit writeEEPROM(optsLoc[opt],fnSetVal,optsMax[opt]>255?true:false); clearSet(); } if(evt==1 && (ctrl==CTRL_UP || ctrl==CTRL_DN)){ - if(ctrl==CTRL_UP) doSet(rotVel ? 10 : 1); - if(ctrl==CTRL_DN) doSet(rotVel ? -10 : -1); + if(ctrl==CTRL_UP) doSet(velocity ? 10 : 1); + if(ctrl==CTRL_DN) doSet(velocity ? -10 : -1); updateDisplay(); //may also make sounds for sampling } - } //end setting option value - } //end options menu setting + } //end setting value + } //end settings menu setting } //end ctrlEvt @@ -637,224 +578,211 @@ void fnScroll(byte dir){ //0=down, 1=up //Switch to the next (up) or previous (down) enabled function. This determines the order. //We'll use switch blocks *without* breaks to cascade to the next enabled function + bool alarmOK = (PIEZO_PIN>=0 || SWITCH_PIN>=0 || PULSE_PIN>=0) && ENABLE_ALARM_FN; //skip alarm if no signals available if(dir) { // up switch(fn) { - case fnIsTime: if(ENABLE_DATE_FN) { fn = fnIsDate; break; } - case fnIsDate: if(ENABLE_ALARM_FN) { fn = fnIsAlarm; break; } - case fnIsAlarm: if(ENABLE_TIMER_FN) { fn = fnIsTimer; break; } - case fnIsTimer: if(ENABLE_TEMP_FN) { fn = fnIsTemp; break; } - case fnIsTemp: if(ENABLE_TUBETEST_FN) { fn = fnIsTest; break; } - case fnIsTest: default: fn = fnIsTime; break; + case FN_TOD: if(ENABLE_DATE_FN) { fn = FN_CAL; break; } + case FN_CAL: if(alarmOK) { fn = FN_ALARM; break; } + case FN_ALARM: if(ENABLE_TIMER_FN) { fn = FN_TIMER; break; } + case FN_TIMER: if(ENABLE_TEMP_FN) { fn = FN_THERM; break; } + case FN_THERM: if(ENABLE_TUBETEST_FN) { fn = FN_TUBETEST; break; } + case FN_TUBETEST: default: fn = FN_TOD; break; } } else { // down switch(fn) { - case fnIsTime: if(ENABLE_TUBETEST_FN) { fn = fnIsTest; break; } - case fnIsTest: if(ENABLE_TEMP_FN) { fn = fnIsTemp; break; } - case fnIsTemp: if(ENABLE_TIMER_FN) { fn = fnIsTimer; break; } - case fnIsTimer: if(ENABLE_ALARM_FN) { fn = fnIsAlarm; break; } - case fnIsAlarm: if(ENABLE_DATE_FN) { fn = fnIsDate; break; } - case fnIsDate: default: fn = fnIsTime; break; + case FN_TOD: if(ENABLE_TUBETEST_FN) { fn = FN_TUBETEST; break; } + case FN_TUBETEST: if(ENABLE_TEMP_FN) { fn = FN_THERM; break; } + case FN_THERM: if(ENABLE_TIMER_FN) { fn = FN_TIMER; break; } + case FN_TIMER: if(alarmOK) { fn = FN_ALARM; break; } + case FN_ALARM: if(ENABLE_DATE_FN) { fn = FN_CAL; break; } + case FN_CAL: default: fn = FN_TOD; break; } } } void fnOptScroll(byte dir){ //0=down, 1=up - //Switch to the next options fn between min and max (inclusive), looping around at range ends - byte posLast = fnOpts+sizeof(optsLoc)-1; - if(dir==1) fn = (fn==posLast? fnOpts: fn+1); - if(dir==0) fn = (fn==fnOpts? posLast: fn-1); - //Certain options don't apply to some configurations; skip those. - byte optLoc = optsLoc[fn-fnOpts]; - if( - //Hardware config - (PIEZO_PIN<0 && (optLoc==39||optLoc==40||optLoc==41||optLoc==47||optLoc==48||optLoc==49)) //no piezo: no signal pitches or alarm/timer/strike beeper pattern - || ((PIEZO_PIN<0 && RELAY_MODE==0) && (optLoc==21||optLoc==50)) //no piezo, and relay is switch: no strike, or alarm fibonacci mode - || ((RELAY_PIN<0 || PIEZO_PIN<0) && (optLoc==42||optLoc==43||optLoc==44)) //no relay or no piezo: no alarm/timer/strike signal - || ((RELAY_MODE==0) && (optLoc==44)) //relay is switch: no strike signal - || ((LED_PIN<0) && (optLoc==26)) //no led pin: no led control - || ((LED_PIN<0) && (optLoc==26)) //no led pin: no led control + //Switch to the next setting, looping around at range ends + byte posLast = FN_OPTS+sizeof(optsLoc)-1; + if(dir==1) fn = (fn==posLast? FN_OPTS: fn+1); + if(dir==0) fn = (fn==FN_OPTS? posLast: fn-1); + //Certain settings don't apply to some configurations; skip those. + byte optLoc = optsLoc[fn-FN_OPTS]; + if(!SHOW_IRRELEVANT_OPTIONS && ( //see also: network requestType=1 + //Signals disabled + (PIEZO_PIN<0 && (optLoc==39||optLoc==40||optLoc==41||optLoc==47||optLoc==48||optLoc==49)) //no piezo: skip all pitches and patterns + || ((PIEZO_PIN<0 || PULSE_PIN<0) && (optLoc==44)) //no piezo or pulse: skip strike signal type + || ((PIEZO_PIN<0 && PULSE_PIN<0) && (optLoc==21||optLoc==50)) //no piezo and pulse: skip the rest of strike, and alarm fibonacci mode + || (((PIEZO_PIN>=0)+(SWITCH_PIN>=0)+(PULSE_PIN>=0)<2) && (optLoc==42||optLoc==43)) //fewer than two signal types: skip alarm/timer signal type + || (((PIEZO_PIN>=0)+(SWITCH_PIN>=0)+(PULSE_PIN>=0)<1) && (optLoc==23||optLoc==24)) //no signal types: skip the rest of alarm (autoskip and snooze) + || ((BACKLIGHT_PIN<0) && (optLoc==26)) //no backlight pin: no backlight control //Functions disabled - || (!ENABLE_DATE_FN && (optLoc==17||optLoc==18||optLoc==10||optLoc==12||optLoc==14)) //date fn disabled in config: skip date and geography options - || (!ENABLE_ALARM_FN && (optLoc==23||optLoc==42||optLoc==39||optLoc==47||optLoc==24||optLoc==50)) //alarm fn disabled in config: skip alarm options - || (!ENABLE_TIMER_FN && (optLoc==43||optLoc==40||optLoc==48)) //timer fn disabled in config: skip timer options + || (!ENABLE_DATE_FN && (optLoc==17||optLoc==18||optLoc==10||optLoc==12)) //date fn disabled in config: skip date and geography settings - don't skip utc offset as that's now used when setting clock from network ||optLoc==14 + || (!ENABLE_ALARM_FN && (optLoc==23||optLoc==42||optLoc==39||optLoc==47||optLoc==24||optLoc==50)) //alarm fn disabled in config: skip alarm settings + || (!ENABLE_TIMER_FN && (optLoc==43||optLoc==40||optLoc==48)) //timer fn disabled in config: skip timer settings || (!ENABLE_TEMP_FN && (optLoc==45)) //temp fn disabled in config: skip temp format TODO good for weather also //Other functionality disabled - || (!ENABLE_DATE_RISESET && (optLoc==10||optLoc==12||optLoc==14)) //date rise/set disabled in config: skip geography + || (!ENABLE_DATE_RISESET && (optLoc==10||optLoc==12)) //date rise/set disabled in config: skip geography - don't skip utc offset as that's now used when setting clock from network ||optLoc==14 || (!ENABLE_ALARM_AUTOSKIP && (optLoc==23)) //alarm autoskip disabled in config: skip autoskip switch || (!ENABLE_ALARM_FIBONACCI && (optLoc==50)) //fibonacci mode disabled in config: skip fibonacci switch || (!ENABLE_TIME_CHIME && (optLoc==21||optLoc==44||optLoc==41||optLoc==49)) //chime disabled in config: skip chime || (!ENABLE_SHUTOFF_NIGHT && (optLoc==27||optLoc==28||optLoc==30)) //night shutoff disabled in config: skip night || ((!ENABLE_SHUTOFF_NIGHT || !ENABLE_SHUTOFF_AWAY) && (optLoc==32||optLoc==35||optLoc==37)) //night or away shutoff disabled in config: skip away (except workweek) || ((!ENABLE_SHUTOFF_NIGHT || !ENABLE_SHUTOFF_AWAY) && (!ENABLE_ALARM_AUTOSKIP || !ENABLE_ALARM_FN) && (optLoc==33||optLoc==34)) //(night or away) and alarm autoskip disabled: skip workweek - ) { + //Nixie-specific + #ifndef DISPLAY_NIXIE + || (optLoc==20||optLoc==46) //digit fade and anti-poisoning + #endif + )) { fnOptScroll(dir); } } +void goToFn(byte thefn, byte thefnPg){ //A shortcut that also sets inputLast per human activity + fn = thefn; + fnPg = thefnPg; + setInputLast(); +} -void switchAlarm(byte dir){ +void switchAlarmState(byte dir){ //0=down, 1=up, 2=toggle if(ENABLE_SOFT_ALARM_SWITCH){ - //signalStop(); //snoozeRemain = 0; //TODO I don't think we need this anymore – test //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==2) dir=(alarmOn?0: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==0){ - 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 - } - } - } + //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. + if(dir==2) dir=(readEEPROM(2,false)?0:1); //If alarm is off, cycle button goes up; otherwise down. + if(dir==1) setAlarmState(2); //if off or skip, go straight to on + if(dir==0) setAlarmState(getAlarmState()-1); //if on, skip; if skip, off updateDisplay(); } //TODO don't make alarm permanent until leaving setting to minimize writes to eeprom as user cycles through options? } +void setAlarmState(byte state){ + //0=off, 1=on with skip, 2=on + if(!ENABLE_SOFT_ALARM_SWITCH || getAlarmState()==state) return; //don't act unless it's different + writeEEPROM(2,state>0,false); //on or off + alarmSkip = (state==1); //on with skip + quickBeep(state==2? 76: (state==1? 71: 64)); //C7, G6, C6 +} +byte getAlarmState(){ + //0=off, 1=on with skip, 2=on + return (readEEPROM(2,false)?2:0)-alarmSkip; +} void switchPower(byte dir){ //0=down, 1=up, 2=toggle signalRemain = 0; snoozeRemain = 0; //in case alarm is going now - alternatively use signalStop()? - //If the timer is running down and is using the switched relay, this instruction conflicts with it, so cancel it + //If the timer is running down and is using the switch signal, this instruction conflicts with it, so cancel it if(timerState&1 && !((timerState>>1)&1) && readEEPROM(43,false)==1) { timerClear(); updateDisplay(); return; } - //RELAY_PIN state is the reverse of the appliance state: LOW = device on, HIGH = device off + //SWITCH_PIN state is the reverse of the appliance state: LOW = device on, HIGH = device off // Serial.print(millis(),DEC); - // Serial.print(F(" Relay requested to ")); + // Serial.print(F(" Switch pin requested to ")); if(dir==2) { //toggle - dir = (digitalRead(RELAY_PIN)?1:0); //LOW = device on, so this effectively does our dir reversion for us + dir = (digitalRead(SWITCH_PIN)?1:0); //LOW = device on, so this effectively does our dir reversion for us //Serial.print(dir==1?F("toggle on"):F("toggle off")); } else { //Serial.print(dir==1?F("switch on"):F("switch off")); } - digitalWrite(RELAY_PIN,(dir==1?0:1)); updateLEDs(); //LOW = device on + digitalWrite(SWITCH_PIN,(dir==1?0:1)); updateBacklight(); //LOW = device on //Serial.println(F(", switchPower")); } 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; + if(fnSetValMax==59) blankDisplay(0, 3, false); //setting in seconds area - blank h:m + else blankDisplay(4, 5, false); //setting in h:m area - blank seconds 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-fnSetVal0) if(fnSetValMax-fnSetVal=FN_OPTS){ //in settings menu + switch(optsLoc[fn-FN_OPTS]){ //setting loc, per current setting index + case 42: case 43: case 44: //signal type: only allow those which are equipped + if( (fnSetVal==0 && PIEZO_PIN>=0) + ||(fnSetVal==1 && SWITCH_PIN>=0) + ||(fnSetVal==2 && PULSE_PIN>=0)) did = true; + //else leave as false + break; + case 26: //backlighting: skip "follow switch signal" option if not equipped + if(fnSetVal<4 || (fnSetVal==4 && SWITCH_PIN>=0)) did = true; + //else leave as false + break; + default: did = true; break; + } + } else did = true; + } fnSetValDid=true; updateDisplay(); } -#if CTRL_UPDN_TYPE==1 //buttons -unsigned long doSetHoldLast; -void doSetHold(bool start){ - //When we're setting via an adj button that's passed a hold point, fire off doSet commands at intervals - //TODO integrate this with checkInputs? - unsigned long now = millis(); - //The interval used to be 250, but in order to make the value change by a full 9 values between CTRL_HOLD_SHORT_DUR and CTRL_HOLD_LONG_DUR, - //the interval is now that difference divided by 9. TODO divisor may need to be a bit higher in case CTRL_HOLD_LONG_DUR ever fires before 9th - it seems indeed it did, so 9.5. - //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)>=(((CTRL_HOLD_LONG_DUR-CTRL_HOLD_SHORT_DUR)*2)/19)) { //(x*2)/19 = x/9.5 - doSetHoldLast = now; - if(fnSetPg!=0 && (btnCur==CTRL_UP || btnCur==CTRL_DN) ){ //if we're setting, and this is an adj btn - bool dir = (btnCur==CTRL_UP ? 1 : 0); - //If short hold, or long hold but high velocity isn't supported, use low velocity (delta=1) - if(btnCurHeld==2 || (btnCurHeld==3 && fnSetValVel==false)) doSet(dir?1:-1); - //else if long hold, use high velocity (delta=10) - else if(btnCurHeld==3) doSet(dir?10:-10); - } - } -} -#endif + void clearSet(){ //Exit set state startSet(0,0,0,0); fnSetValDid=false; - updateLEDs(); //in case LED setting was changed + updateBacklight(); //in case backlight setting was changed checkRTC(true); //force an update to tod and updateDisplay() } -//EEPROM values are exclusively bytes (0-255) or words (unsigned ints, 0-65535) -//If it's a word, high byte is in loc, low byte is in loc+1 -void initEEPROM(bool hard){ +//EEPROM values are bytes (0 to 255) or signed 16-bit ints (-32768 to 32767) where high byte is loc and low byte is loc+1. +bool initEEPROM(bool hard){ //If hard, set EEPROM and clock to defaults //Otherwise, just make sure stuff is in range - //First prevent the held button from doing anything else - btnCur = CTRL_SEL; btnStop(); + byte changed = 0; + hard = (hard || readEEPROM(16,false)==0); //if EEPROM is uninitiated, do defaults //If a hard init, set the clock if(hard) { - ds3231.setYear(20); - ds3231.setMonth(1); - ds3231.setDate(1); - ds3231.setDoW(3); //2020-01-01 is Wednesday. DS3231 will keep count from here - ds3231.setHour(0); - ds3231.setMinute(0); - ds3231.setSecond(0); + rtcSetDate(2021,1,1,dayOfWeek(2021,1,1)); + rtcSetTime(0,0,0); + if(networkSupported()) clearNTPSyncLast(); } - if(hard || readEEPROM(0,true)>1439) writeEEPROM(0,420,true); //0-1: alarm at 7am + //The vars outside the settings menu + if(hard || readEEPROM(0,true)>1439) changed += writeEEPROM(0,420,true,false); //0-1: alarm at 7am //2: alarm on, handled by init //3: 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) - if(hard) writeEEPROM(7,0,false); //7: Alt function preset + if(hard || readEEPROM(4,false)<0 || readEEPROM(4,false)>2) changed += writeEEPROM(4,2,false,false); //4: day counter direction: count up... + if(hard || readEEPROM(5,false)<1 || readEEPROM(5,false)>12) changed += writeEEPROM(5,12,false,false); //5: ...December... + if(hard || readEEPROM(6,false)<1 || readEEPROM(6,false)>31) changed += writeEEPROM(6,31,false,false); //6: ...31st. (This gives the day of the year) + if(hard) changed += writeEEPROM(7,0,false,false); //7: Alt function preset //8: TODO functions/pages enabled (bitmask) - //9: free - if(hard) writeEEPROM(15,0,false); //15: last known DST on flag - clear on hard reset (to match the reset RTC/auto DST/anti-poisoning settings to trigger midnight tubes as a tube test) - //then the options menu defaults + if(hard) changed += writeEEPROM(15,0,false,false); //15: last known DST on flag - clear on hard reset (to match the reset RTC/auto DST/anti-poisoning settings to trigger midnight tubes as a tube test) + if(networkSupported()){ + if(hard){ //everything in here needs no range testing + //51-54 NTP server IP address (4 bytes) - e.g. from https://tf.nist.gov/tf-cgi/servers.cgi + //Serial.println(F("setting IP address in eeprom")); + //Serial.print(readEEPROM(51,false),DEC); Serial.print(F(".")); Serial.print(readEEPROM(52,false),DEC); Serial.print(F(".")); Serial.print(readEEPROM(53,false),DEC); Serial.print(F(".")); Serial.println(readEEPROM(54,false),DEC); + changed += writeEEPROM(51,129,false,false); + changed += writeEEPROM(52, 6,false,false); + changed += writeEEPROM(53, 15,false,false); + changed += writeEEPROM(54, 27,false,false); + //Serial.print(readEEPROM(51,false),DEC); Serial.print(F(".")); Serial.print(readEEPROM(52,false),DEC); Serial.print(F(".")); Serial.print(readEEPROM(53,false),DEC); Serial.print(F(".")); Serial.println(readEEPROM(54,false),DEC); + //55-86 Wi-Fi SSID (32 bytes) + //87-150 Wi-Fi WPA passphrase/key or WEP key (64 bytes) + for(byte i=0; i<96; i++) changed += writeEEPROM(55+i,0,false,false); //Clear out the old values (32+64+1) + } + //9 NTP sync enabled + if(hard || readEEPROM(9,false)>1) changed += writeEEPROM(9,0,false,false); + //151 Wi-Fi WEP key index + if(hard || readEEPROM(151,false)>3) changed += writeEEPROM(151,0,false,false); + } //end network supported + //The vars inside the settings menu bool isInt = false; for(byte opt=0; opt255?true:false); if(hard || readEEPROM(optsLoc[opt],isInt)optsMax[opt]) - writeEEPROM(optsLoc[opt],optsDef[opt],isInt); + changed += writeEEPROM(optsLoc[opt],optsDef[opt],isInt,false); } //end for + return changed>0; //whether EEPROM was changed } //end initEEPROM() -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 { - // 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 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")); } Serial.print(F(";")); - // Serial.print(F(" loc ")); Serial.print(loc+1,DEC); Serial.print(F(" val ")); - // 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(val,DEC); Serial.print(F("):")); - // Serial.print(F(" loc ")); Serial.print(loc,DEC); Serial.print(F(" val ")); - // if(EEPROM.read(loc)!=val) { Serial.println(val,DEC); } else { Serial.println(F("no change")); } - EEPROM.update(loc,val); - } -} void findFnAndPageNumbers(){ //Each function, and each page in a paged function, has a number. //TODO should pull from EEPROM 8 fnDatePages = 1; //date function always has a page for the date itself - if(ENABLE_DATE_COUNTER){ fnDatePages++; fnDateCounter=fnDatePages-1; } + if(ENABLE_DATE_COUNTER && readEEPROM(4,false)){ fnDatePages++; fnDateCounter=fnDatePages-1; } if(ENABLE_DATE_RISESET){ fnDatePages++; fnDateSunlast=fnDatePages-1; } if(false){ fnDatePages++; fnDateWeathernow=fnDatePages-1; } if(ENABLE_DATE_RISESET){ fnDatePages++; fnDateSunnext=fnDatePages-1; } @@ -873,134 +801,155 @@ void checkRTC(bool force){ unsigned long now = millis(); //Things to do every time this is called: timeouts to reset display. These may force a tick. - //Option/setting timeout: if we're in the options menu, or we're setting a value - if(fnSetPg || fn>=fnOpts){ - if((unsigned long)(now-inputLast)>=SETTING_TIMEOUT*1000) { fnSetPg = 0; fn = fnIsTime; force=true; } //Time out after 2 mins + //Setting timeout: if we're in the settings menu, or we're setting a value + if(fnSetPg || fn>=FN_OPTS){ + if((unsigned long)(now-getInputLast())>=SETTING_TIMEOUT*1000) { fnSetPg = 0; fn = FN_TOD; force=true; } //Time out after 2 mins } - //Paged-display function timeout //TODO change fnIsDate to consts? //TODO timeoutPageFn var - else if(fn==fnIsDate && (unsigned long)(now-inputLast)>=FN_PAGE_TIMEOUT*1000) { //3sec per date page - //If a scroll in is going, fast-forward to end - see also ctrlEvt - if(scrollRemain>0) { - scrollRemain = 1; - checkEffects(true); - } + //Paged-display function timeout //TODO change FN_CAL to consts? //TODO timeoutPageFn var + else if(fn==FN_CAL && (unsigned long)(now-getInputLast())>=FN_PAGE_TIMEOUT*1000) { //3sec per date page + // //If a scroll in is going, fast-forward to end - see also ctrlEvt + // if(scrollRemain>0) { + // scrollRemain = 1; + // checkEffects(true); + // } //Here we just have to increment the page and decide when to reset. updateDisplay() will do the rendering - fnPg++; inputLast+=(FN_PAGE_TIMEOUT*1000); //but leave inputLastTODMins alone so the subsequent page displays will be based on the same TOD + fnPg++; setInputLast(FN_PAGE_TIMEOUT*1000); //but leave inputLastTODMins alone so the subsequent page displays will be based on the same TOD while(fnPg= fnDatePages){ fnPg = 0; fn = fnIsTime; } // when we run out of pages, go back to time. When the half-minute date is triggered, fnPg is set to 254, so it will be 255 here and be cancelled after just the one page. + if(fnPg >= fnDatePages){ fnPg = 0; fn = FN_TOD; } // when we run out of pages, go back to time. When the half-minute date is triggered, fnPg is set to 254, so it will be 255 here and be cancelled after just the one page. force=true; } //Temporary-display function timeout: if we're *not* in a permanent one (time, or running/signaling timer) // Stopped/non-signaling timer shouldn't be permanent, but have a much longer timeout, mostly in case someone is waiting to start the chrono in sync with some event, so we'll give that an hour. - else if(fn!=fnIsTime && !(fn==fnIsTimer && (timerState&1 || signalRemain>0))){ - if((unsigned long)(now-inputLast)>=(fn==fnIsTimer?3600:FN_TEMP_TIMEOUT)*1000) { fnSetPg = 0; fn = fnIsTime; force=true; } + else if(fn!=FN_TOD && !(fn==FN_TIMER && (timerState&1 || signalRemain>0))){ + if((unsigned long)(now-getInputLast())>=(fn==FN_TIMER?3600:FN_TEMP_TIMEOUT)*1000) { fnSetPg = 0; fn = FN_TOD; force=true; } + } + + //Temporary value display queue + if(tempValDispQueue[0] && !tempValDispLast){ //just starting + tempValDispLast = now; if(!tempValDispLast) tempValDispLast = 1; //can't be zero + updateDisplay(); + } + if(tempValDispLast && (unsigned long)(now-tempValDispLast)>=tempValDispDur){ //already going - time to advance? + for(char i=0; i<=3; i++) tempValDispQueue[i] = (i==3?0:tempValDispQueue[i+1]); + if(!tempValDispQueue[0]) tempValDispLast = 0; //zero found, time to stop + else { tempValDispLast = now; if(!tempValDispLast) tempValDispLast = 1; } //can't be zero + updateDisplay(); } //Update things based on RTC - tod = rtc.now(); - toddow = ds3231.getDoW()-1; //ds3231 weekday is 1-index + rtcTakeSnap(); - if(rtcSecLast != tod.second() || force) { //If it's a new RTC second, or we are forcing it + if(rtcSecLast != rtcGetSecond() || 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()); } + if(rtcSecLast==61) { autoDST(); calcSun(); } //Things to do every natural second (decrementing real-time counters) - if(rtcSecLast != tod.second()) { + if(rtcSecLast != rtcGetSecond()) { //If alarm snooze has time on it, decrement, and if we reach zero and alarm is still on, resume //Won't check alarm skip status here, as it reflects tomorrow if(snoozeRemain>0) { snoozeRemain--; //Serial.print("sr "); Serial.println(snoozeRemain,DEC); - if(snoozeRemain<=0 && alarmOn) { - fnSetPg = 0; fn = fnIsTime; - if((readEEPROM(50,false) && !(RELAY_PIN>=0 && RELAY_MODE==0 && readEEPROM(42,false)==1))) fibonacci(tod.hour(),tod.minute(),tod.second()); //fibonacci sequence - else signalStart(fnIsAlarm,1); //regular alarm + if(snoozeRemain<=0 && readEEPROM(2,false)) { //alarm on + fnSetPg = 0; fn = FN_TOD; + if(readEEPROM(50,false) && readEEPROM(42,false)!=1) fibonacci(rtcGetHour(),rtcGetMinute(),rtcGetSecond()); //fibonacci sequence + else signalStart(FN_ALARM,1); //regular alarm } } if(unoffRemain>0) { unoffRemain--; //updateDisplay will naturally put it back to off state if applicable } - if(versionRemain>0) { - versionRemain--; - } } //end natural second //Things to do at specific times //Timer drift correction: per the millisCorrectionInterval - if(tod.second()%millisCorrectionInterval==0){ //if time: + if(rtcGetSecond()%millisCorrectionInterval==0){ //if time: if(!(rtcDid&1)) millisCheckDrift(); bitWrite(rtcDid,0,1); //do if not done, set as done } else bitWrite(rtcDid,0,0); //if not time: set as not done //DST change check: every 2am - if(tod.second()==0 && tod.minute()==0 && tod.hour()==2) autoDST(); + if(rtcGetSecond()==0 && rtcGetMinute()==0 && rtcGetHour()==2) autoDST(); //Alarm check: at top of minute for normal alarm, or 23 seconds past for fibonacci (which starts 26m37s early) - //Only do fibonacci if enabled and if the alarm is not using the switched relay - otherwise do regular - bool fibOK = readEEPROM(50,false) && !(RELAY_PIN>=0 && RELAY_MODE==0 && readEEPROM(42,false)==1); - if((tod.second()==0 && !fibOK) || (tod.second()==23 && fibOK)){ + //Only do fibonacci if enabled and if the alarm is not using the switch signal - otherwise do regular + bool fibOK = readEEPROM(50,false) && readEEPROM(42,false)!=1; + if((rtcGetSecond()==0 && !fibOK) || (rtcGetSecond()==23 && fibOK)){ int alarmTime = readEEPROM(0,true); - if(tod.second()==23){ alarmTime-=27; if(alarmTime<0) alarmTime+=1440; } //set min to n-27 with midnight rollover - if(tod.hour()*60+tod.minute()==alarmTime){ - //Serial.println(tod.second()==23?F("It's fibonacci time"):F("It's regular alarm time")); - if(alarmOn && !alarmSkip) { //if the alarm is on and not skipped, sound it! - fnSetPg = 0; fn = fnIsTime; - if(tod.second()==23) fibonacci(tod.hour(),tod.minute(),tod.second()); //fibonacci sequence - else signalStart(fnIsAlarm,1); //regular alarm + if(rtcGetSecond()==23){ alarmTime-=27; if(alarmTime<0) alarmTime+=1440; } //set min to n-27 with midnight rollover + if(rtcGetHour()*60+rtcGetMinute()==alarmTime){ + //Serial.println(rtcGetSecond()==23?F("It's fibonacci time"):F("It's regular alarm time")); + if(readEEPROM(2,false) && !alarmSkip) { //if the alarm is on and not skipped, sound it! + fnSetPg = 0; fn = FN_TOD; + if(rtcGetSecond()==23) fibonacci(rtcGetHour(),rtcGetMinute(),rtcGetSecond()); //fibonacci sequence + else signalStart(FN_ALARM,1); //regular alarm } //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))) || + (readEEPROM(23,false)==1 && isDayInRange(readEEPROM(33,false),readEEPROM(34,false),(rtcGetWeekday()==6?0:rtcGetWeekday()+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))) + (readEEPROM(23,false)==2 && !isDayInRange(readEEPROM(33,false),readEEPROM(34,false),(rtcGetWeekday()==6?0:rtcGetWeekday()+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 && cleanRemain==0 && scrollRemain==0 && versionRemain==0) { - if(readEEPROM(18,false)>=2) { fn = fnIsDate; inputLast = now; inputLastTODMins = tod.hour()*60+tod.minute(); fnPg = 254; updateDisplay(); } - if(readEEPROM(18,false)==3) { startScroll(); } + if(rtcGetSecond()==30 && fn==FN_TOD && fnSetPg==0 && unoffRemain==0 && versionShowing==false) { /*cleanRemain==0 && scrollRemain==0 && */ + if(readEEPROM(18,false)>=2) { goToFn(FN_CAL,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 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 = 151; //run routine for fifteen cycles - break; - case 1: //every hour - if(tod.second()==1 && tod.minute()==0) //at min/sec :00:01 - cleanRemain = 101; //run routine for ten cycles - break; - case 2: //every minute - if(tod.second()==1) //at second :01 - cleanRemain = 21; //run routine for two cycles - break; - default: break; - } + if(rtcGetSecond()<2 && displayDim==2 && fnSetPg==0 && unoffRemain==0) { + //temporarily we'll recalculate the sun stuff every day + if(readEEPROM(27,false)>0? //is night shutoff enabled? + rtcGetSecond()==0 && rtcGetHour()*60+rtcGetMinute()==readEEPROM(28,true): //if so, at start of night shutoff (at second :00 before dim is in effect) + rtcGetSecond()==1 && rtcGetHour()*60+rtcGetMinute()==0) //if not, at 00:00:01 + calcSun(); //take this opportunity to perform a calculation that blanks the display for a bit + // TODO the below will need to change cleanRemain=x to displayClean(x) + + // switch(readEEPROM(46,false)) { //how often should the routine run? + // case 0: //every day + // if(readEEPROM(27,false)>0? //is night shutoff enabled? + // rtcGetSecond()==0 && rtcGetHour()*60+rtcGetMinute()==readEEPROM(28,true): //if so, at start of night shutoff (at second :00 before dim is in effect) + // rtcGetSecond()==1 && rtcGetHour()*60+rtcGetMinute()==0) //if not, at 00:00:01 + // cleanRemain = 151; //run routine for fifteen cycles + // break; + // case 1: //every hour + // if(rtcGetSecond()==1 && rtcGetMinute()==0) //at min/sec :00:01 + // cleanRemain = 101; //run routine for ten cycles + // break; + // case 2: //every minute + // if(rtcGetSecond()==1) //at second :01 + // cleanRemain = 21; //run routine for two cycles + // break; + // default: break; + // } + } + + //NTP cue at :59:00 + if(rtcGetMinute()==59 && networkSupported()){ + if(rtcGetSecond()==0) cueNTP(); + if(rtcGetSecond()==30 && ntpSyncAgo()>=30000) cueNTP(); //if at first you don't succeed... } //Strikes - only if fn=clock, not setting, not signaling/snoozing, not night/away. Setting 21 will be off if signal type is no good //The six pips - if(tod.minute()==59 && tod.second()==55 && readEEPROM(21,false)==2 && signalRemain==0 && snoozeRemain==0 && fn==fnIsTime && fnSetPg==0 && displayDim==2) { - signalStart(fnIsTime,6); //the signal code knows to use pip durations as applicable + if(rtcGetMinute()==59 && rtcGetSecond()==55 && readEEPROM(21,false)==2 && signalRemain==0 && snoozeRemain==0 && fn==FN_TOD && fnSetPg==0 && displayDim==2) { + signalStart(FN_TOD,6); //the signal code knows to use pip durations as applicable } //Strikes on/after the hour - if(tod.second()==0 && (tod.minute()==0 || tod.minute()==30) && signalRemain==0 && snoozeRemain==0 && fn==fnIsTime && fnSetPg==0 && displayDim==2){ - byte hr; hr = tod.hour(); hr = (hr==0?12:(hr>12?hr-12:hr)); + if(rtcGetSecond()==0 && (rtcGetMinute()==0 || rtcGetMinute()==30) && signalRemain==0 && snoozeRemain==0 && fn==FN_TOD && fnSetPg==0 && displayDim==2){ + byte hr; hr = rtcGetHour(); hr = (hr==0?12:(hr>12?hr-12:hr)); switch(readEEPROM(21,false)) { case 1: //single beep - if(tod.minute()==0) signalStart(fnIsTime,0); break; + if(rtcGetMinute()==0) signalStart(FN_TOD,0); break; case 3: //hour strike via normal signal cycle - if(tod.minute()==0) signalStart(fnIsTime,hr); break; + if(rtcGetMinute()==0) signalStart(FN_TOD,hr); break; case 4: //ship's bell at :00 and :30 mins via normal signal cycle - signalStart(fnIsTime,((hr%4)*2)+(tod.minute()==30?1:0)); break; + signalStart(FN_TOD,((hr%4)*2)+(rtcGetMinute()==30?1:0)); break; default: break; } //end strike type } //end strike @@ -1008,9 +957,9 @@ void checkRTC(bool force){ //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 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(); + if(fnSetPg==0 && (true || force) && !(fn==FN_CAL && !force)) updateDisplay(); /*scrollRemain==0 ||*/ - rtcSecLast = tod.second(); + rtcSecLast = rtcGetSecond(); } //end if force or new second } //end checkRTC() @@ -1039,13 +988,13 @@ void fibonacci(byte h, byte m, byte s){ if(diff<=n) { if(diff>0) { //Beep and snooze signalPattern = 1; //short beep - signalSource = fnIsAlarm; + signalSource = FN_ALARM; signalStart(-1,0); //Play a signal measure using above pattern and source snoozeRemain = nnn; //Serial.print(F(" SR")); Serial.print(snoozeRemain,DEC); } else { //Time for regular alarm //Serial.print(F(" Alarm!")); - signalStart(fnIsAlarm,1); + signalStart(FN_ALARM,1); } break; } @@ -1055,12 +1004,13 @@ void fibonacci(byte h, byte m, byte s){ } //end fibonacci() void autoDST(){ - //Change the clock if the current DST differs from the dstOn flag. + //Change the clock if the current DST differs from the new one. //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 dstNow = isDSTByHour(rtcGetYear(),rtcGetMonth(),rtcGetDate(),rtcGetHour(),false); + if(dstNow!=readEEPROM(15,false)){ + rtcSetHour(dstNow>readEEPROM(15,false)? 3: 1); //spring forward or fall back + writeEEPROM(15,dstNow,false); + } } bool isDST(int y, byte m, byte d){ //returns whether DST is in effect on this date (after 2am shift) @@ -1087,9 +1037,8 @@ bool isDSTByHour(int y, byte m, byte d, byte h, bool setFlag){ d--; if(d<1){ m--; if(m<1){ y--; m=12; } d=daysInMonth(y,m); } if(dstNow!=isDST(y,m,d) && h<2) dstNow=!dstNow; if(setFlag){ - dstOn = dstNow; - writeEEPROM(15,dstOn,false); - //Serial.print(F("DST is ")); Serial.println(dstOn?F("on"):F("off")); + writeEEPROM(15,dstNow,false); + //Serial.print(F("DST is ")); Serial.println(readEEPROM(15,false)?F("on"):F("off")); } return dstNow; } @@ -1113,8 +1062,8 @@ int dateToDayCount(word y, byte m, byte d){ return dc; } byte dayOfWeek(word y, byte m, byte d){ - //DS3231 doesn't really calculate the day of the week, it just keeps a counter. - //When setting date, we'll calculate per https://en.wikipedia.org/wiki/Zeller%27s_congruence + //Used by nthSunday and in calls to rtcSetDate + //Calculated per https://en.wikipedia.org/wiki/Zeller%27s_congruence byte yb = y%100; //2-digit year byte ya = y/100; //century //For this formula, Jan and Feb are considered months 11 and 12 of the previous year. @@ -1157,7 +1106,7 @@ bool isDayInRange(byte dstart, byte dend, byte dtest) { } // Chrono/Timer -// There are two timing sources in the UNDB – the Arduino itself (eg millis()), which gives subsecond precision but isn't very accurate, so it's only good for short-term timing and taking action in response to user activity (eg button press hold thresholds); and the DS3231, which is very accurate but only gives seconds (unless you're monitoring its square wave via a digital pin), so it's only good for long-term timing and taking action in response to time of day. The one place we need both short-term precision and long-term accuracy is in the chrono/timer – so I have based it on millis() but with an offset applied to correct for its drift, periodically adjusted per the DS3231. I also use it for the signal, so the 1/sec measure cycle stays in sync with real time; but we don't need to use it for stuff like button polling. +// There are two timing sources in the UNDB – the Arduino itself (eg millis()), which gives subsecond precision but isn't very accurate, so it's only good for short-term timing and taking action in response to user activity (eg button press hold thresholds); and the rtc, which is very accurate but only gives seconds (unless you're monitoring its square wave via a digital pin, in DS3231's case), so it's only good for long-term timing and taking action in response to time of day. The one place we need both short-term precision and long-term accuracy is in the chrono/timer – so I have based it on millis() but with an offset applied to correct for its drift, periodically adjusted per the rtc. I also use it for the signal, so the 1/sec measure cycle stays in sync with real time; but we don't need to use it for stuff like button polling. unsigned long millisDriftOffset = 0; //The cumulative running offset. Since it's circular, doesn't matter whether signed or not. //unsigned long millisAtLastCheck (defined at top, so ctrlEvt can reset it when setting RTC). 0 when unreliable (at start and after RTC set). //const byte millisCorrectionInterval (defined at top, so checkRTC can see it) @@ -1170,15 +1119,15 @@ void millisCheckDrift(){ if(abs((long)(millisDrift+millisDriftBuffer))>32767){} // If adding drift to buffer would make it overflow, ignore it this time else { millisDriftBuffer -= millisDrift; - // tod = rtc.now(); - // if(tod.hour()<10) Serial.print(F("0")); - // Serial.print(tod.hour(),DEC); + // rtcTakeSnap(); + // if(rtcGetHour()<10) Serial.print(F("0")); + // Serial.print(rtcGetHour(),DEC); // Serial.print(F(":")); - // if(tod.minute()<10) Serial.print(F("0")); - // Serial.print(tod.minute(),DEC); + // if(rtcGetMinute()<10) Serial.print(F("0")); + // Serial.print(rtcGetMinute(),DEC); // Serial.print(F(":")); - // if(tod.second()<10) Serial.print(F("0")); - // Serial.print(tod.second(),DEC); + // if(rtcGetSecond()<10) Serial.print(F("0")); + // Serial.print(rtcGetSecond(),DEC); // Serial.print(F(" millis: ")); // Serial.print(now,DEC); // Serial.print(F(" drift: ")); @@ -1196,14 +1145,14 @@ void millisApplyDrift(){ millisDriftOffset += (millisDriftBuffer>0? 1: -1); millisDriftBuffer -= (millisDriftBuffer>0? 1: -1); // tod = rtc.now(); - // if(tod.hour()<10) Serial.print(F("0")); - // Serial.print(tod.hour(),DEC); + // if(rtcGetHour()<10) Serial.print(F("0")); + // Serial.print(rtcGetHour(),DEC); // Serial.print(F(":")); - // if(tod.minute()<10) Serial.print(F("0")); - // Serial.print(tod.minute(),DEC); + // if(rtcGetMinute()<10) Serial.print(F("0")); + // Serial.print(rtcGetMinute(),DEC); // Serial.print(F(":")); - // if(tod.second()<10) Serial.print(F("0")); - // Serial.print(tod.second(),DEC); + // if(rtcGetSecond()<10) Serial.print(F("0")); + // Serial.print(rtcGetSecond(),DEC); // Serial.print(F(" new offset: ")); // Serial.print(millisDriftOffset,DEC); // Serial.print(F(" new buffer: ")); @@ -1211,6 +1160,9 @@ void millisApplyDrift(){ // Serial.println(); } } +void millisReset(){ + millisAtLastCheck = 0; //because setting the RTC makes this unreliable +} unsigned long ms(){ // Returns millis() with the drift offset applied, for timer/chrono purposes. // WARNING: Since the offset is being periodically adjusted, there is the possibility of a discontinuity in ms() output – if we give out a timestamp and then effectively set the clock back, the next timestamp might possibly be earlier than the last one, which could mess up duration math. I tried to think of a way to monitor for that discontinuity – e.g. if now-then is greater than then-now, due to overflow – but it gets tricky since millis() is effectively circular, and that condition occurs naturally at rollover as well – so I think we would need a flag that millisCheckDrift sets when it sets the offset backward, and ms clears when the real time has caught up.... or something like that. @@ -1225,7 +1177,7 @@ void timerStart(){ //If chrono (count up), timestamp is an origin in the past: now minus duration. //If timer (count down), timestamp is a destination in the future: now plus duration. timerTime = ((timerState>>1)&1? ms() - timerTime: ms() + timerTime); - if(!((timerState>>1)&1)) timerSwitchSleepRelay(1); //possibly switch the relay, but only if counting down + if(!((timerState>>1)&1)) timerSleepSwitch(1); //possibly toggle the switch signal, but only if counting down quickBeep(69); } //end timerStart() void timerStop(){ @@ -1235,7 +1187,7 @@ void timerStop(){ //If chrono (count up), timestamp is an origin in the past: duration is now minus timestamp. //If timer (count down), timestamp is a destination in the future: duration is timestamp minus now. timerTime = ((timerState>>1)&1? ms() - timerTime: timerTime - ms()); - if(!((timerState>>1)&1)) timerSwitchSleepRelay(0); //possibly switch the relay, but only if counting down + if(!((timerState>>1)&1)) timerSleepSwitch(0); //possibly toggle the switch signal, but only if counting down quickBeep(64); bitWrite(timerState,4,0); //set timer lap display (bit 4) to off (0) updateDisplay(); //since cycleTimer won't do it @@ -1244,7 +1196,7 @@ void timerClear(){ bitWrite(timerState,0,0); //set timer running (bit 0) to off (0) bitWrite(timerState,1,1); //set timer direction (bit 1) to up (1) TODO is this necessary timerTime = 0; //set timer duration - timerSwitchSleepRelay(0); + timerSleepSwitch(0); bitWrite(timerState,4,0); //set timer lap display (bit 4) to off (0) //updateDisplay is called not long after this } @@ -1260,7 +1212,7 @@ void timerRunoutToggle(){ if(!((timerState>>2)&1)) timerState ^= (1<<3); //if it's 0, toggle runout chrono bit //do a quick signal to indicate the selection signalPattern = ((timerState>>2)&3)+1; //convert 00/01/10/11 to 1/2/3/4 - signalSource = fnIsTimer; + signalSource = FN_TIMER; signalStart(-1,0); //Play a signal measure using above pattern and source } } @@ -1273,24 +1225,24 @@ void cycleTimer(){ //runout action and display if((timerState>>3)&1){ //runout chrono - keep target, change direction, kill sleep, change display bitWrite(timerState,1,1); //set timer direction (bit 1) to up (1) - timerSwitchSleepRelay(0); - fnSetPg = 0; fn = fnIsTimer; + timerSleepSwitch(0); + fnSetPg = 0; fn = FN_TIMER; } else { if((timerState>>2)&1){ //runout repeat - keep direction, change target, keep sleep, don't change display timerTime += (timerInitialMins*60000)+(timerInitialSecs*1000); //set timer duration ahead by initial setting } else { //runout clear - clear timer, change display timerClear(); - //If switched relay (radio sleep), go to time of day; otherwise go to empty timer to appear with signal - fnSetPg = 0; fn = (RELAY_PIN>=0 && RELAY_MODE==0 && readEEPROM(43,false)==1 ? fnIsTime: fnIsTimer); + //If switch signal (radio sleep), go to time of day; otherwise go to empty timer to appear with signal + fnSetPg = 0; fn = (readEEPROM(43,false)==1 ? FN_TOD: FN_TIMER); updateDisplay(); } } - //signal (if not switched relay) + //piezo or pulse signal if((timerState>>2)&1){ //short signal (piggybacks on runout repeat flag) - if(RELAY_PIN<0 || RELAY_MODE!=0 || readEEPROM(43,false)!=1) signalStart(fnIsTimer,1); - //using 1 instead of 0, because in signalStart, fnIsTimer "quick measure" has a custom pitch for runout option setting + if(readEEPROM(43,false)!=1) signalStart(FN_TIMER,1); + //using 1 instead of 0, because in signalStart, FN_TIMER "quick measure" has a custom pitch for runout option setting } else { //long signal - if(RELAY_PIN<0 || RELAY_MODE!=0 || readEEPROM(43,false)!=1) signalStart(fnIsTimer,SIGNAL_DUR); + if(readEEPROM(43,false)!=1) signalStart(FN_TIMER,SIGNAL_DUR); } } } else { //If we are counting up, @@ -1310,202 +1262,186 @@ void cycleTimer(){ } } //If it's on display, update - if(fn==fnIsTimer) updateDisplay(); + if(fn==FN_TIMER) updateDisplay(); } } //end cycleTimer() -void timerSwitchSleepRelay(bool on){ - //When timer is set to use switched relay, it's on while timer is running, "radio sleep" style. - //This doesn't count as a signal (using signal methods) so that other signals might not interrupt it. TODO confirm - if(RELAY_PIN>=0 && RELAY_MODE==0 && readEEPROM(43,false)==1) { //start "radio sleep" - digitalWrite(RELAY_PIN,(on?LOW:HIGH)); updateLEDs(); //LOW = device on +void timerSleepSwitch(bool on){ + //When timer is set to use switch signal, it's on while timer is running, "radio sleep" style. + //We won't use the true signal methods so that other signals might not interrupt it. TODO confirm + if(readEEPROM(43,false)==1) { //start "radio sleep" + digitalWrite(SWITCH_PIN,(on?LOW:HIGH)); updateBacklight(); //LOW = device on // Serial.print(millis(),DEC); - // if(on) Serial.println(F(" Relay on, timerSwitchSleepRelay (radio sleep)")); - // else Serial.println(F(" Relay off, timerSwitchSleepRelay (radio sleep)")); + // if(on) Serial.println(F(" Switch signal on, timerSleepSwitch")); + // else Serial.println(F(" Switch signal off, timerSleepSwitch")); } } - - -////////// Display data formatting ////////// - -byte displayNext[6] = {15,15,15,15,15,15}; //Internal representation of display. Blank to start. Change this to change tubes. -byte displayLast[6] = {11,11,11,11,11,11}; //for noticing changes to displayNext and fading the display to it -byte scrollDisplay[6] = {15,15,15,15,15,15}; //For animating a value into displayNext from right, and out to left - -unsigned long pollCleanLast = 0; //every CLEAN_SPEED ms -unsigned long pollScrollLast = 0; //every SCROLL_SPEED ms -void checkEffects(bool force){ - //control the cleaning/scrolling effects - similar to checkRTC but it has its own timings - unsigned long now = millis(); - //If we're running a tube cleaning, advance it every CLEAN_SPEED ms. - if(cleanRemain && (unsigned long)(now-pollCleanLast)>=CLEAN_SPEED) { //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 SCROLL_SPEED ms. - else if(scrollRemain!=0 && scrollRemain!=-128 && ((unsigned long)(now-pollScrollLast)>=SCROLL_SPEED || force)) { - pollScrollLast=now; - if(scrollRemain<0) { - scrollRemain++; updateDisplay(); - } else { - scrollRemain--; updateDisplay(); - if(scrollRemain==0) scrollRemain = -128; - } - } +byte getTimerState(){ return timerState; } +void setTimerState(char pos, bool val){ + if(val) timerState |= (1<= DISPLAY_SIZE, then blank - -3: 1 2[3 4 ] - -2: 1 2 3[4 ] - -1: 1 2 3 4[ ] - 0: (scroll functions are skipped to display whatever's applicable) - */ - else if(scrollRemain>0) { //scrolling display: value coming in - these don't use editDisplay as we're going array to array - for(byte i=0; i=DISPLAY_SIZE? 15: scrollDisplay[isrc]); //allow to fade - } - } - else if(versionRemain>0) { + // if(scrollRemain==-128) { //If the current display is flagged to be scrolled out, do it. This is kind of the counterpart to startScroll() + // for(byte i=0; i<6; i++) scrollDisplay[i] = displayNext[i]; //cache the current value in scrollDisplay[] just in case it changed + // scrollRemain = (0-DISPLAY_SIZE)-1; + // } + // + // if(cleanRemain) { //cleaning tubes + // displayDim = 2; + // byte digit = 10-((cleanRemain-1)%10); //(11-cleanRemain)%10; + // editDisplay(digit,0,0,true,false); + // editDisplay(digit,1,1,true,false); + // editDisplay(digit,2,2,true,false); + // editDisplay(digit,3,3,true,false); + // editDisplay(digit,4,4,true,false); + // editDisplay(digit,5,5,true,false); + // } + // /* + // Scrolling "frames" (ex. on 4-tube clock): + // To use this, call editDisplay() as usual, then startScroll() + // 4: [ ]1 2 3 4 tube[n] (0-index) is source[n-scrollRemain] unless that index < 0, then blank + // 3: [ 1]2 3 4 + // 2: [ 1 2]3 4 + // 1: [ 1 2 3]4 + // 0/-128: [1 2 3 4] + // -4: 1[2 3 4 ] tube[n] is source[n+DISPLAY_SIZE+scrollRemain+1] unless that index >= DISPLAY_SIZE, then blank + // -3: 1 2[3 4 ] + // -2: 1 2 3[4 ] + // -1: 1 2 3 4[ ] + // 0: (scroll functions are skipped to display whatever's applicable) + // */ + // else if(scrollRemain>0) { //scrolling display: value coming in - these don't use editDisplay as we're going array to array + // for(byte i=0; i=DISPLAY_SIZE? 15: scrollDisplay[isrc]); //allow to fade + // } + // } //todo move cleanRemain, scrollRemain to dispNixie + // else + if(versionShowing) { 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 + else if(tempValDispQueue[0]>0){ + editDisplay(tempValDispQueue[0], 0, 3, false, true); + blankDisplay(4, 5, true); + } + else if(fnSetPg) { //setting value, for either fn or settings menu displayDim = 2; - blankDisplay(4, 5, false); - byte fnOptCurLoc = (fn>=fnOpts? optsLoc[fn-fnOpts]: 0); //current option index loc, to tell what's being set + // blankDisplay(4, 5, false); //taken over by startSet + byte fnOptCurLoc = (fn>=FN_OPTS? optsLoc[fn-FN_OPTS]: 0); //current setting index loc, to tell what's being set if(fnSetValMax==1439) { //Time of day (0-1439 mins, 0:00–23:59): show hrs/mins - editDisplay(fnSetVal/60, 0, 1, readEEPROM(19,false), false); //hours with leading zero per options + editDisplay(fnSetVal/60, 0, 1, readEEPROM(19,false), false); //hours with leading zero per settings editDisplay(fnSetVal%60, 2, 3, true, false); } else if(fnSetValMax==5999) { //Timer duration mins (0-5999 mins, up to 99:59): show hrs/mins w/regular leading - editDisplay(fnSetVal/60, 0, 1, readEEPROM(19,false), false); //hours with leading zero per options + editDisplay(fnSetVal/60, 0, 1, readEEPROM(19,false), false); //hours with leading zero per settings editDisplay(fnSetVal%60, 2, 3, true, false); //minutes with leading zero always } else if(fnSetValMax==59) { //Timer duration secs: show with leading - //If 6 tubes (0-5), display on 4-5 - //If 4 tubes (0-3), dislpay on 2-3 - blankDisplay(0, 3, false); + //If 6 digits (0-5), display on 4-5 + //If 4 digits (0-3), dislpay on 2-3 + // blankDisplay(0, 3, false); //taken over by startSet editDisplay(fnSetVal, (DISPLAY_SIZE>4? 4: 2), (DISPLAY_SIZE>4? 5: 3), true, false); } else if(fnSetValMax==88) { //A piezo pitch. Play a short demo beep. editDisplay(fnSetVal, 0, 3, false, false); - if(PIEZO_PIN>=0) { noTone(PIEZO_PIN); tone(PIEZO_PIN, getHz(fnSetVal), 100); } //Can't use signalStart since we need to specify pitch directly + quickBeep(fnSetVal); //Can't use signalStart since we need to specify pitch directly } else if(fnOptCurLoc==47 || fnOptCurLoc==48 || fnOptCurLoc==49) { //Signal pattern. Play a demo measure. editDisplay(fnSetVal, 0, 3, false, false); - signalPattern = fnSetVal; - signalSource = (fnOptCurLoc==49?fnIsTime:(fnOptCurLoc==48?fnIsTimer:fnIsAlarm)); - signalStart(-1,1); //Play a sample using the above source and pattern + quickBeepPattern((fnOptCurLoc==49?FN_TOD:(fnOptCurLoc==48?FN_TIMER:FN_ALARM)),fnSetVal); } 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 + //If 6 digits (0-5), display degrees on 0-3 and tenths on 4, with 5 blank + //If 4 digits (0-3), display degrees on 0-2 and tenths on 3 editDisplay(abs(fnSetVal), 0, (DISPLAY_SIZE>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 + else if(fn >= FN_OPTS){ //settings menu, but not setting a value displayDim = 2; - editDisplay(optsNum[fn-fnOpts],0,1,false,false); //display option number on hour tubes + editDisplay(optsNum[fn-FN_OPTS],0,1,false,false); //display setting number on hour digits blankDisplay(2,5,false); } else { //fn running - //Set displayDim per night/away settings - fnIsAlarm may override this + //Set displayDim per night/away settings - FN_ALARM may override this //issue: moving from off alarm to next fn briefly shows alarm in full brightness. I think because of the display delays. TODO - word todmins = tod.hour()*60+tod.minute(); + word todmins = rtcGetHour()*60+rtcGetMinute(); //In order of precedence: //TODO can we fade between dim states? //clock at work: away on weekends, all day - if( readEEPROM(32,false)==1 && !isDayInRange(readEEPROM(33,false),readEEPROM(34,false),toddow) ) + if( readEEPROM(32,false)==1 && !isDayInRange(readEEPROM(33,false),readEEPROM(34,false),rtcGetWeekday()) ) 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); + else if( readEEPROM(32,false)==2 && isDayInRange(readEEPROM(33,false),readEEPROM(34,false),rtcGetWeekday()) && isTimeInRange(readEEPROM(35,true), readEEPROM(37,true), todmins) ) displayDim = (unoffRemain>0? 2: 0); //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; - updateLEDs(); + updateBacklight(); switch(fn){ - case fnIsTime: - byte hr; hr = tod.hour(); + case FN_TOD: + byte hr; hr = rtcGetHour(); if(readEEPROM(16,false)==1) hr = (hr==0?12:(hr>12?hr-12:hr)); editDisplay(hr, 0, 1, readEEPROM(19,false), true); - editDisplay(tod.minute(), 2, 3, true, true); - if(readEEPROM(18,false)==1) editDisplay(tod.day(), 4, 5, readEEPROM(19,false), true); //date - else editDisplay(tod.second(), 4, 5, true, true); //seconds + editDisplay(rtcGetMinute(), 2, 3, true, true); + //Serial.print(millis(),DEC); Serial.println(F("show display per regular (hours/mins at least)")); + if(networkSupported() && readEEPROM(9,false) && ntpSyncAgo()>=86400000){ blankDisplay(4,5,true); break; } + if(readEEPROM(18,false)==1) editDisplay(rtcGetDate(), 4, 5, readEEPROM(19,false), true); //date + else editDisplay(rtcGetSecond(), 4, 5, true, true); //seconds break; - case fnIsDate: //a paged display + case FN_CAL: //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 + editDisplay((df==1||df==3?rtcGetMonth():rtcGetDate()),0,1,readEEPROM(19,false),true); //month or date first + editDisplay((df==1||df==3?rtcGetDate():rtcGetMonth()),2,3,readEEPROM(19,false),true); //date or month second + editDisplay((df<=2?rtcGetWeekday():rtcGetYear()),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); + editDisplay(rtcGetYear(),0,1,true,true); //year always has leading zero + editDisplay(rtcGetMonth(),2,3,readEEPROM(19,false),true); + editDisplay(rtcGetDate(),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); + editDisplay(dateComp(rtcGetYear(),rtcGetMonth(),rtcGetDate(),readEEPROM(5,false),readEEPROM(6,false),readEEPROM(4,false)-1),0,3,false,true); blankDisplay(4,5,true); } //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==fnDateSunlast) displaySun(0,rtcGetDate(),getInputLastTODMins()); else if(fnPg==fnDateWeathernow) displayWeather(0); - else if(fnPg==fnDateSunnext) displaySun(1,tod.day(),inputLastTODMins); + else if(fnPg==fnDateSunnext) displaySun(1,rtcGetDate(),getInputLastTODMins()); else if(fnPg==fnDateWeathernext) displayWeather(1); - break; //end fnIsDate + break; //end FN_CAL //fnIsDayCount removed in favor of paginated calendar - case fnIsAlarm: //alarm + case FN_ALARM: //alarm + displayDim = (readEEPROM(2,false)?2:1); //status bright/dim 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); - if(alarmOn && alarmSkip){ //alarm on+skip + if(readEEPROM(2,false) && 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); + editDisplay(readEEPROM(2,false),4,4,false,true); blankDisplay(5,5,true); } - displayDim = (alarmOn?2:1); //status bright/dim break; - case fnIsTimer: //timer - display time + case FN_TIMER: //timer - display time unsigned long td; td = (!(timerState&1)? timerTime: //If stopped, use stored duration //If running, use same math timerStop() does to calculate duration ((timerState>>1)&1? ((timerState>>4)&1? timerLapTime: ms()) - timerTime: //count up - use timerLapTime during lap display @@ -1518,24 +1454,24 @@ void updateDisplay(){ //Countup shows H:M:S, but if H<1, M:S:C, but if DISPLAY_SIZE<6 and M<1, S:C bool lz; lz = readEEPROM(19,false)&1; if((timerState>>1)&1){ //count up - if(DISPLAY_SIZE<6 && td<60){ //under 1 min, 4-tubers: [SS]CC-- + if(DISPLAY_SIZE<6 && td<60){ //under 1 min, 4-digit displays: [SS]CC-- if(td>=1||lz) editDisplay(td,0,1,lz,true); else blankDisplay(0,1,true); //secs, leading per lz, fade editDisplay(tdc,2,3,td>=1||lz,false); //cents, leading if >=1sec or lz, no fade - blankDisplay(4,5,true); //just in case 4-tube code's running on a 6-tube clock, don't look ugly + blankDisplay(4,5,true); //just in case 4-digit code's running on a 6-digit display, don't look ugly } else if(td<3600){ //under 1 hr: [MM][SS]CC if(td>=60||lz) editDisplay(td/60,0,1,lz,true); else blankDisplay(0,1,true); //mins, leading per lz, fade if(td>=1||lz) editDisplay(td%60,2,3,td>=60||lz,true); else blankDisplay(2,3,true); //secs, leading if >=1min or lz, fade - editDisplay(tdc,4,5,td>=1||lz,false); //cents, leading if >=1sec or lz, no fade - hidden on 4-tubers + editDisplay(tdc,4,5,td>=1||lz,false); //cents, leading if >=1sec or lz, no fade - hidden on 4-digit display } else { //over 1 hr: HHMMSS editDisplay(td/3600,0,1,lz,true); //hrs, leading per lz, fade editDisplay((td%3600)/60,2,3,true,true); //mins, leading, fade editDisplay(td%60,4,5,true,true); //secs, leading, fade } } else { //count down - if(DISPLAY_SIZE<6 && td<3600){ //under 1 hr, 4-tubers: [MM]SS-- + if(DISPLAY_SIZE<6 && td<3600){ //under 1 hr, 4-digit displays: [MM]SS-- if(td>=60||lz) editDisplay(td/60,0,1,lz,true); else blankDisplay(0,1,true); //mins, leading per lz, fade if(td>=1||lz) editDisplay(td%60,2,3,td>=60||lz,true); else blankDisplay(2,3,true); //secs, leading if >=1min or lz, fade - blankDisplay(4,5,true); //just in case 4-tube code's running on a 6-tube clock, don't look ugly + blankDisplay(4,5,true); //just in case 4-digit code's running on a 6-digit display, don't look ugly } else { //[HH][MM]SS if(td>=3600||lz) editDisplay(td/3600,0,1,lz,true); else blankDisplay(0,1,true); //hrs, leading per lz, fade if(td>=60||lz) editDisplay((td%3600)/60,2,3,td>=3600||lz,true); else blankDisplay(2,3,true); //mins, leading if >=1h or lz, fade @@ -1543,37 +1479,37 @@ void updateDisplay(){ } } break; - case fnIsTemp: //thermometer - int temp; temp = ds3231.getTemperature()*100; + case FN_THERM: //thermometer TODO disable if rtc doesn't support it + int temp; temp = rtcGetTemp(); if(readEEPROM(45,false)==1) temp = temp*1.8 + 3200; - //TODO another option to apply offset + //TODO another setting to apply offset? editDisplay(abs(temp)/100,1,3,(temp<0?true:false),true); //leading zeros if negative editDisplay(abs(temp)%100,4,5,true,true); break; - case fnIsTest: - editDisplay(tod.second(),0,0,true,false); - editDisplay(tod.second(),1,1,true,false); - editDisplay(tod.second(),2,2,true,false); - editDisplay(tod.second(),3,3,true,false); - editDisplay(tod.second(),4,4,true,false); - editDisplay(tod.second(),5,5,true,false); + case FN_TUBETEST: + editDisplay(rtcGetSecond(),0,0,true,false); + editDisplay(rtcGetSecond(),1,1,true,false); + editDisplay(rtcGetSecond(),2,2,true,false); + editDisplay(rtcGetSecond(),3,3,true,false); + editDisplay(rtcGetSecond(),4,4,true,false); + editDisplay(rtcGetSecond(),5,5,true,false); default: break; }//end switch } //end if fn running // 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); + // if(rtcGetHour()<10) Serial.print(F("0")); + // Serial.print(rtcGetHour(),DEC); // Serial.print(F(":")); - // if(tod.minute()<10) Serial.print(F("0")); - // Serial.print(tod.minute(),DEC); + // if(rtcGetMinute()<10) Serial.print(F("0")); + // Serial.print(rtcGetMinute(),DEC); // Serial.print(F(":")); - // if(tod.second()<10) Serial.print(F("0")); - // Serial.print(tod.second(),DEC); + // if(rtcGetSecond()<10) Serial.print(F("0")); + // Serial.print(rtcGetSecond(),DEC); // Serial.print(F(" ")); // for(byte i=0; i9) Serial.print(F("-")); //blanked tube + // if(displayNext[i]>9) Serial.print(F("-")); //blanked digit // else Serial.print(displayNext[i],DEC); // } // Serial.println(); @@ -1590,9 +1526,9 @@ void updateDisplay(){ // 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 serialPrintTime(int mins){ +// if(mins/60<10) Serial.print(F("0")); Serial.print(mins/60,DEC); Serial.print(F(":")); +// if(mins%60<10) Serial.print(F("0")); Serial.print(mins%60,DEC); // } //A snapshot of sun times, in minutes past midnight, calculated at clean time and when the date or time is changed. @@ -1605,10 +1541,15 @@ 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, byte m, byte d){ +void calcSun(){ //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); + int y = rtcGetYear(); + int m = rtcGetMonth(); + int d = rtcGetDate(); + //Serial.print(millis(),DEC); Serial.println(F("blank display per calcsun")); + //blankDisplay(0,5,false); //immediately blank display so we can fade in from it elegantly + //TODO causes nixie blinking during initial startup and after ntp sync + Dusk2Dawn here(float(readEEPROM(10,true))/10, float(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. @@ -1670,187 +1611,33 @@ void displaySun(byte which, int d, int tod){ } #else //to give other fns something empty to call, when rise/set isn't enabled -void calcSun(int y, byte m, byte d){} +void calcSun(){} void displaySun(byte which, int d, int tod){} #endif void displayWeather(byte which){ - //shows high/low temp (for day/night respectively) on main tubes, and precipitation info on seconds tubes + //shows high/low temp (for day/night respectively) in place of hour/min, and precipitation info in place of seconds //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 - //If number has more places than posEnd-posStart, the higher places are truncated off (e.g. 10015 on 4 tubes --> 0015) - word place; - for(byte i=0; i<=posEnd-posStart; i++){ - switch(i){ //because int(pow(10,1))==10 but int(pow(10,2))==99... - case 0: place=1; break; - 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))); - if(!fade) displayLast[posEnd-i] = displayNext[posEnd-i]; //cycleDisplay will be none the wiser - } -} //end editDisplay() -void blankDisplay(byte posStart, byte posEnd, byte fade){ - for(byte i=posStart; i<=posEnd; i++) { displayNext[i]=15; if(!fade) displayLast[i]=15; } -} //end blankDisplay(); -void startScroll() { //To scroll a value in, call this after calling editDisplay as normal - for(byte i=0; i<6; i++) scrollDisplay[i] = displayNext[i]; //cache the incoming value in scrollDisplay[] - blankDisplay(0,5,true); - scrollRemain = DISPLAY_SIZE+1; //this will trigger updateDisplay() to start scrolling. DISPLAY_SIZE+1 adds blank frame at front -} //end startScroll() - -////////// Hardware outputs ////////// -//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] -byte binOutA[4] = {OUT_A1,OUT_A2,OUT_A3,OUT_A4}; -byte binOutB[4] = {OUT_B1,OUT_B2,OUT_B3,OUT_B4}; -//3 pins out to anode channel switches -byte anodes[3] = {ANODE_1,ANODE_2,ANODE_3}; -const int fadeDur = 5; //ms - each multiplexed pair of digits appears for this amount of time per cycle -const int dimDur = 4; //ms - portion of fadeDur that is left dark during dim times -int fadeNextDur = 0; //ms - during fade, incoming digit's portion of fadeDur -int fadeLastDur = 0; //ms - during fade, outgoing digit's portion of fadeDur -unsigned long fadeStartLast = 0; //millis - when the last digit fade was started -unsigned long setStartLast = 0; //to control flashing during start -unsigned long displayBlinkStart = 0; //when nonzero, display should briefly blank +////////// Hardware outputs ////////// void initOutputs() { - for(byte i=0; i<4; i++) { pinMode(binOutA[i],OUTPUT); pinMode(binOutB[i],OUTPUT); } - for(byte i=0; i<3; i++) { pinMode(anodes[i],OUTPUT); } if(PIEZO_PIN>=0) pinMode(PIEZO_PIN, OUTPUT); - if(RELAY_PIN>=0) { - pinMode(RELAY_PIN, OUTPUT); digitalWrite(RELAY_PIN, HIGH); //LOW = device on - quickBeep(71); //"primes" the beeper, seems necessary when relay pin is spec'd, otherwise first intentional beep doesn't happen TODO still true? - } - if(LED_PIN>=0) pinMode(LED_PIN, OUTPUT); - updateLEDs(); //set to initial value + if(SWITCH_PIN>=0) { pinMode(SWITCH_PIN, OUTPUT); digitalWrite(SWITCH_PIN, HIGH); } //LOW = device on + if(PULSE_PIN>=0) { pinMode(PULSE_PIN, OUTPUT); digitalWrite(PULSE_PIN, HIGH); } //LOW = device on + if(BACKLIGHT_PIN>=0) pinMode(BACKLIGHT_PIN, OUTPUT); + updateBacklight(); //set to initial value } -void displayBlink(){ - displayBlinkStart = millis(); -} -void cycleDisplay(){ - unsigned long now = millis(); - - if(displayBlinkStart){ - if((unsigned long)(now-displayBlinkStart)<250){ delay(fadeDur*3); return; } - // The delay is to make cycleDisplay take up the same amount of loop time it usually does. Not sure if necessary. - else displayBlinkStart = 0; - } - - //Other display code decides whether we should dim per function or time of day - bool dim = (displayDim==1?1:0); - //But if we're setting, decide here to dim for every other 500ms since we started setting - if(fnSetPg>0) { - if(setStartLast==0) setStartLast = now; - dim = 1-(((unsigned long)(now-setStartLast)/500)%2); - } 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 - - if(readEEPROM(20,false)==0 || dim) { //fading disabled or dim - if(fadeStartLast) fadeStartLast = 0; //cancel any fade currently going - dim state doesn't have enough steps to fade well - for(byte i=0; i<6; i++) if(displayNext[i] != displayLast[i]) displayLast[i] = displayNext[i]; - } - else { //fading enabled - if(fadeStartLast==0) { //not fading - time to fade? - for(byte i=0; i<6; i++) if(displayNext[i] != displayLast[i]) { fadeStartLast = now; break; } - } - if(fadeStartLast!=0) { //currently fading - //let the next digit steal some display time from the last digit - //ex: if fade time (from EEPROM) is 20ms, and fadeDur (next+last) is 6ms: - // at 0ms, next = (( 0*(6-1))/20)+1 = 1; last = (6-nextDur) = 5; - // at 10ms, next = ((10*(6-1))/20)+1 = 3; last = (6-nextDur) = 3; ... - // at 20ms, next = ((20*(6-1))/20)+1 = 6; next = total, so fade is over! - //TODO facilitate longer fades by writing a tweening function that smooths the frames, i.e. 111121222 - or use delayMicroseconds as below - //TODO does this have more problems with the millis rollover issue? - fadeNextDur = (((unsigned long)(now-fadeStartLast)*(fadeDur-1))/(readEEPROM(20,false)*10))+1; - if(fadeNextDur >= fadeLastDur) { //fade is over - fadeStartLast = 0; - fadeNextDur = 0; - fadeLastDur = fadeDur; - for(byte j=0; j<6; j++) displayLast[j] = displayNext[j]; - } //end fade is over - else { //shorten last digit display duration by subtracting next display duration from it - fadeLastDur = fadeLastDur - fadeNextDur; - } - } //end curently fading - } //end fading enabled - - //TODO consider using delayMicroseconds() which, with its tighter resolution, may give better control over fades and dim levels - if(displayDim>0) { //if other display code says to shut off entirely, skip this part - //Anode channel 0: tubes #2 (min x10) and #5 (sec x1) - setCathodes(displayLast[2],displayLast[5]); //Via d2b decoder chip, set cathodes to old digits - digitalWrite(anodes[0], HIGH); //Turn on tubes - delay(fadeLastDur);//-(dim?dimDur:0)); //Display for fade-out cycles - setCathodes(displayNext[2],displayNext[5]); //Switch cathodes to new digits - delay(fadeNextDur);//-(dim?dimDur:0)); //Display for fade-in cycles - digitalWrite(anodes[0], LOW); //Turn off tubes - - if(dim) delay(dimDur); - - //Anode channel 1: tubes #4 (sec x10) and #1 (hour x1) - setCathodes(displayLast[4],displayLast[1]); - digitalWrite(anodes[1], HIGH); - delay(fadeLastDur); - setCathodes(displayNext[4],displayNext[1]); - delay(fadeNextDur); - digitalWrite(anodes[1], LOW); - - if(dim) delay(dimDur); - - //Anode channel 2: tubes #0 (hour x10) and #3 (min x1) - setCathodes(displayLast[0],displayLast[3]); - digitalWrite(anodes[2], HIGH); - delay(fadeLastDur); - setCathodes(displayNext[0],displayNext[3]); - delay(fadeNextDur); - digitalWrite(anodes[2], LOW); - - 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){ - bool binVal[4]; //4-bit binary number with values [1,2,4,8] - decToBin(binVal,decValA); //have binary value of decVal set into binVal - for(byte i=0; i<4; i++) digitalWrite(binOutA[i],binVal[i]); //set bin inputs of SN74141 - decToBin(binVal,decValB); - for(byte i=0; i<4; i++) digitalWrite(binOutB[i],binVal[i]); //set bin inputs of SN74141 -} //end setCathodes() - -void decToBin(bool binVal[], byte i){ - //binVal is a reference (modify in place) of a binary number bool[4] with values [1,2,4,8] - if(i<0 || i>15) i=15; //default value, turns tubes off - binVal[3] = int(i/8)%2; - binVal[2] = int(i/4)%2; - binVal[1] = int(i/2)%2; - binVal[0] = i%2; -} //end decToBin() - -//Signals are like songs played on the beeper or relay, when the alarm/timer runs out or as an indicator. -//Like songs, a signal is made up of measures (usually 1sec each) tracked by the signalRemain counter. -//Measures are made up of steps such as beeps and relay pulses, tracked by signalMeasureStep. -//When used with switched relay, it simply turns on at the start, and off at the end, like a clock radio – the measures just wait it out. +//Signals are like songs made up of measures (usually 1sec each) tracked by the signalRemain counter. +//Measures are made up of steps such as piezo beeps and pulses, tracked by signalMeasureStep. +//When used with switch signal, it simply turns on at the start, and off at the end, like a clock radio – the measures just wait it out. //Timed using ms() instead of millis() – see timer/chrono for details. unsigned long signalMeasureStartTime = 0; //to keep track of individual measures byte signalMeasureStep = 0; //step number, or 255 if waiting for next measure, or 0 if not signaling @@ -1858,13 +1645,13 @@ void signalStart(byte sigFn, byte sigDur){ //sigFn isn't necessarily the current fn, just the one generating the signal //sigDur is the number of measures to put on signalRemain, // or 0 for a single "quick measure" as applicable (i.e. skipped in radio mode). - //Special case: if sigFn==fnIsAlarm, and sigDur>0, we'll use SIGNAL_DUR or SWITCH_DUR as appropriate. + //Special case: if sigFn==FN_ALARM, and sigDur>0, we'll use SIGNAL_DUR or SWITCH_DUR as appropriate. //If sigFn is given as -1 (255), we will use both the existing signalSource and signalPattern for purposes of configs and fibonacci. - if(!(sigFn==255 && signalSource==fnIsTimer)) signalStop(); // if there is a signal going per the current signalSource, stop it - can only have one signal at a time – except if this is a forced fnIsTimer signal (for signaling runout options) which is cool to overlap timer sleep + if(!(sigFn==255 && signalSource==FN_TIMER)) signalStop(); // if there is a signal going per the current signalSource, stop it - can only have one signal at a time – except if this is a forced FN_TIMER signal (for signaling runout options) which is cool to overlap timer sleep //except if this is a forced if(sigFn!=255) signalSource = sigFn; if(sigFn!=255) signalPattern = ( - (signalSource==fnIsTime && readEEPROM(21,false)==2)? -1: //special case: the pips + (signalSource==FN_TOD && readEEPROM(21,false)==2)? -1: //special case: the pips getSignalPattern() //usual: get pattern from user settings ); // Serial.print(F("signalStart, sigFn=")); @@ -1881,13 +1668,13 @@ void signalStart(byte sigFn, byte sigDur){ signalMeasureStartTime = ms(); signalMeasureStep = 1; //waiting to start a new measure if(sigDur!=0){ //long-duration signal (alarm, sleep, etc) - set signalRemain - //If switched relay, except if this is a forced fnIsTimer signal (for signaling runout options) - if(getSignalOutput()==1 && RELAY_PIN>=0 && RELAY_MODE==0 && !(sigFn==255 && signalSource==fnIsTimer)) { //turn it on now - signalRemain = (sigFn==fnIsAlarm? SWITCH_DUR: sigDur); //For alarm signal, use switched relay duration from config (eg 2hr) - digitalWrite(RELAY_PIN,LOW); updateLEDs(); //LOW = device on - //Serial.print(millis(),DEC); Serial.println(F(" Relay on, signalStart")); - } else { //start pulsing. If there is no beeper or pulsed relay, this will have no effect since cycleSignal will clear it - signalRemain = (sigFn==fnIsAlarm? SIGNAL_DUR: sigDur); //For alarm signal, use signal duration from config (eg 2min) + //If switch signal, except if this is a forced FN_TIMER signal (for signaling runout options) + if(getSignalOutput()==1 && !(sigFn==255 && signalSource==FN_TIMER)) { //turn it on now + signalRemain = (sigFn==FN_ALARM? SWITCH_DUR: sigDur); //For alarm signal, use switch signal duration from config (eg 2hr) + digitalWrite(SWITCH_PIN,LOW); updateBacklight(); //LOW = device on + //Serial.print(millis(),DEC); Serial.println(F(" Switch signal on, signalStart")); + } else { //start piezo or pulse signal. If neither is present, this will have no effect since cycleSignal will clear it + signalRemain = (sigFn==FN_ALARM? SIGNAL_DUR: sigDur); //For alarm signal, use signal duration from config (eg 2min) } } //cycleSignal will pick up from here @@ -1896,16 +1683,21 @@ void signalStop(){ //stop current signal and clear out signal timer if applicabl //Serial.println(F("signalStop")); signalRemain = 0; snoozeRemain = 0; signalMeasureStep = 0; if(getSignalOutput()==0 && PIEZO_PIN>=0) noTone(PIEZO_PIN); - if(getSignalOutput()==1 && RELAY_PIN>=0){ - digitalWrite(RELAY_PIN,HIGH); updateLEDs(); //LOW = device on - //Serial.print(millis(),DEC); Serial.println(F(" Relay off, signalStop")); + if(getSignalOutput()==1 && SWITCH_PIN>=0){ + digitalWrite(SWITCH_PIN,HIGH); //LOW = device on + //Serial.print(millis(),DEC); Serial.println(F(" Switch signal off, signalStop")); + } + if(getSignalOutput()==2 && PULSE_PIN>=0){ + digitalWrite(PULSE_PIN,HIGH); //LOW = device on + //Serial.print(millis(),DEC); Serial.println(F(" Pulse signal off, signalStop")); } + updateBacklight(); } //end signalStop() void cycleSignal(){ //Called on every loop to control the signal. word measureDur = 1000; //interval between measure starts, ms - beep pattern can customize this if(signalMeasureStep){ //if there's a measure going (or waiting for a new one) - if((getSignalOutput()==0 || (signalRemain==0 && signalSource==fnIsTimer)) && PIEZO_PIN>=0) { // beeper, or single measure for fnIsTimer runout setting + if((getSignalOutput()==0 || (signalRemain==0 && signalSource==FN_TIMER)) && PIEZO_PIN>=0) { // beeper, or single measure for FN_TIMER runout setting //Since tone() handles the duration of each beep, //we only need to use signalMeasureStep to track beep starts; they'll stop on their own. byte bc = 0; //this many beeps @@ -1935,7 +1727,7 @@ void cycleSignal(){ if((unsigned long)(ms()-signalMeasureStartTime)>=(signalMeasureStep-1)*bd*2){ word piezoPitch = (signalPattern==5 && signalMeasureStep==2? getSignalPitch()*0.7937: //cuckoo: go down major third (2^(-4/12)) on 2nd beep (signalPattern==255? 1000: //the pips: use 1000Hz just like the Beeb - (signalRemain==0 && signalSource==fnIsTimer? getHz(69): //fnIsTimer runout setting: use timer start pitch + (signalRemain==0 && signalSource==FN_TIMER? getHz(69): //FN_TIMER runout setting: use timer start pitch getSignalPitch() //usual: get pitch from user settings ) ) @@ -1957,9 +1749,9 @@ void cycleSignal(){ } } } //end beeper - else if(getSignalOutput()==1 && RELAY_PIN>=0 && RELAY_MODE==1){ //pulsed relay - //We don't follow the beep pattern here, we simply energize the relay for RELAY_PULSE time - //Unlike beeper, we need to use a signalMeasureStep (2) to stop the relay. + else if(getSignalOutput()==2 && PULSE_PIN>=0){ //pulse signal + //We don't follow the beep pattern here, we simply energize the pulse signal for PULSE_LENGTH time + //Unlike beeper, we need to use a signalMeasureStep (2) to stop the pulse. //See if it's time to start a new measure if(signalMeasureStep==255 && (unsigned long)(ms()-signalMeasureStartTime)>=measureDur){ //Serial.println(F("Starting new measure, sPS -1 -> 1")); @@ -1968,21 +1760,21 @@ void cycleSignal(){ } //Upon new measure, start the pulse immediately if(signalMeasureStep==1){ - digitalWrite(RELAY_PIN,LOW); updateLEDs(); //LOW = device on - //Serial.print(millis(),DEC); Serial.println(F(" Relay on, cycleSignal")); + digitalWrite(PULSE_PIN,LOW); updateBacklight(); //LOW = device on + //Serial.print(millis(),DEC); Serial.println(F(" Pulse signal on, cycleSignal")); signalMeasureStep = 2; //set it up to stop } //See if it's time to stop the pulse - else if(signalMeasureStep==2 && (unsigned long)(ms()-signalMeasureStartTime)>=RELAY_PULSE) { - digitalWrite(RELAY_PIN,HIGH); updateLEDs(); //LOW = device on - //Serial.print(millis(),DEC); Serial.println(F(" Relay off, cycleSignal")); + else if(signalMeasureStep==2 && (unsigned long)(ms()-signalMeasureStartTime)>=PULSE_LENGTH) { + digitalWrite(PULSE_PIN,HIGH); updateBacklight(); //LOW = device on + //Serial.print(millis(),DEC); Serial.println(F(" Pulse signal off, cycleSignal")); //Set up for the next event if(signalRemain) signalRemain--; //this measure is done if(signalRemain) signalMeasureStep = 255; //if more measures, set up to start another measure else signalMeasureStep = 0; //otherwise, go idle - not using signalStop so as to let the beep expire on its own & fibonacci snooze continue } - } //end pulsed relay - else { //switched relay / default + } //end pulse signal + else { //switch signal / default //Simply decrement signalRemain until it runs out - signalMeasureStep doesn't matter as long as it stays nonzero as at start //See if it's time to start a new measure if((unsigned long)(ms()-signalMeasureStartTime)>=measureDur){ @@ -1991,12 +1783,12 @@ void cycleSignal(){ } //Set up for the next event if(signalRemain) signalRemain--; //this measure is done - but no need to change signalMeasureStep as we haven't changed it - if(!signalRemain) signalStop(); //go idle - will kill the relay - } //end switched relay + if(!signalRemain) signalStop(); //go idle - will kill the switch signal + } //end switch signal / default } //end if there's a measure going } //end cycleSignal() word getSignalPitch(){ //for current signal: chime, timer, or (default) alarm - return getHz(readEEPROM((signalSource==fnIsTime?41:(signalSource==fnIsTimer?40:39)),false)); + return getHz(readEEPROM((signalSource==FN_TOD?41:(signalSource==FN_TIMER?40:39)),false)); } word getHz(byte note){ //Given a piano key note, return frequency @@ -2005,17 +1797,17 @@ word getHz(byte note){ word mult = 440*pow(2,reloct); return mult; } -byte getSignalOutput(){ //for current signal: chime, timer, or (default) alarm: 0=piezo, 1=relay - return readEEPROM((signalSource==fnIsTime?44:(signalSource==fnIsTimer?43:42)),false); +byte getSignalOutput(){ //for current signal: chime, timer, or (default) alarm: 0=piezo, 1=switch, 2=pulse + return readEEPROM((signalSource==FN_TOD?44:(signalSource==FN_TIMER?43:42)),false); } -byte getSignalPattern(){ //for current signal: chime, timer, or (default) alarm: +byte getSignalPattern(){ //for current signal: chime, timer, or (default) alarm: (applies only to piezo) //0 = long (1/2-second beep) //1 = short (1/4-second beep) //2 = double (two 1/8-second beeps) //5 = cuckoo (two 1/8-second beeps, descending major third) //3 = triple (three 1/12-second beeps) //4 = quad (four 1/16-second beeps) - return readEEPROM((signalSource==fnIsTime?49:(signalSource==fnIsTimer?48:47)),false); + return readEEPROM((signalSource==FN_TOD?49:(signalSource==FN_TIMER?48:47)),false); } void quickBeep(int pitch){ //This is separate from signal system @@ -2029,52 +1821,70 @@ void quickBeep(int pitch){ //F7 = 81 if(PIEZO_PIN>=0) { noTone(PIEZO_PIN); tone(PIEZO_PIN, getHz(pitch), 100); } } +void quickBeepPattern(int source, int pattern){ + signalPattern = pattern; + signalSource = source; + signalStart(-1,1); //Play a sample using the above source and pattern +} -const byte ledFadeStep = 10; //fade speed – with every loop() we'll increment/decrement the LED brightness (between 0-255) by this amount -byte ledStateNow = 0; -byte ledStateTarget = 0; -void updateLEDs(){ - //Run whenever something is changed that might affect the LED state: initial (initOutputs), signal start/stop, relay on/off, setting change - if(LED_PIN>=0) { +//BACKLIGHT_FADE is PWM fade speed – if >0, with every loop() we'll increment/decrement the PWM (between 0-255) by this amount. If 0, we'll just switch it on and off (no PWM). +byte backlightNow = 0; +byte backlightTarget = 0; +void updateBacklight(){ + //Run whenever something is changed that might affect the backlight state: initial (initOutputs), signal start/stop, switch signal on/off, setting change + if(BACKLIGHT_PIN>=0) { switch(readEEPROM(26,false)){ case 0: //always off - ledStateTarget = 0; - //Serial.println(F("LEDs off always")); + backlightTarget = 0; + //Serial.println(F("Backlight off always")); break; case 1: //always on - ledStateTarget = 255; - //Serial.println(F("LEDs on always")); + backlightTarget = 255; + //Serial.println(F("Backlight on always")); break; 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")); + backlightTarget = (displayDim==2? 255: (displayDim==1? 127: 0)); + //Serial.print(displayDim==2? F("Backlight on"): (displayDim==1? F("Backlight dim"): F("Backlight off"))); Serial.println(F(" per dim state")); break; case 3: //off, but on when alarm/timer sounds - ledStateTarget = (signalRemain && (signalSource==fnIsAlarm || signalSource==fnIsTimer)? 255: 0); - //Serial.print(signalRemain && (signalSource==fnIsAlarm || signalSource==fnIsTimer)?F("LEDs on"):F("LEDs off")); Serial.println(F(" per alarm/timer")); + backlightTarget = (signalRemain && (signalSource==FN_ALARM || signalSource==FN_TIMER)? 255: 0); + //Serial.print(signalRemain && (signalSource==FN_ALARM || signalSource==FN_TIMER)?F("Backlight on"):F("Backlight off")); Serial.println(F(" per alarm/timer")); break; - case 4: //off, but on with switched relay - if(RELAY_PIN>=0 && RELAY_MODE==0) { - ledStateTarget = (!digitalRead(RELAY_PIN)? 255: 0); //LOW = device on - //Serial.print(!digitalRead(RELAY_PIN)? F("LEDs on"): F("LEDs off")); Serial.println(F(" per switched relay")); + case 4: //off, but on with switch signal + if(SWITCH_PIN>=0) { + backlightTarget = (!digitalRead(SWITCH_PIN)? 255: 0); //LOW = device on + //Serial.print(!digitalRead(SWITCH_PIN)? F("Backlight on"): F("Backlight off")); Serial.println(F(" per switch signal")); } break; default: break; } //end switch - } //if LED_PIN -} //end updateLEDs -void cycleLEDs() { - //Allows us to fade the LEDs to ledStateTarget by stepping via ledFadeStep - //TODO: it appears setting analogWrite(pin,0) does not completely turn the LEDs off. Anything else we could do? - if(ledStateNow != ledStateTarget) { - if(ledStateNow < ledStateTarget) { - ledStateNow = (ledStateTarget-ledStateNow <= ledFadeStep? ledStateTarget: ledStateNow+ledFadeStep); - } else { - ledStateNow = (ledStateNow-ledStateTarget <= ledFadeStep? ledStateTarget: ledStateNow-ledFadeStep); + } //if BACKLIGHT_PIN +} //end updateBacklight +void cycleBacklight() { + //Allows us to fade the backlight to backlightTarget by stepping via BACKLIGHT_FADE, if applicable + //TODO: it appears setting analogWrite(pin,0) does not completely turn the backlight off. Anything else we could do? + if(backlightNow != backlightTarget) { + if(BACKLIGHT_FADE){ //PWM + if(backlightNow < backlightTarget) { //fade up + backlightNow = (backlightTarget-backlightNow <= BACKLIGHT_FADE? backlightTarget: backlightNow+BACKLIGHT_FADE); + } else { //fade down + backlightNow = (backlightNow-backlightTarget <= BACKLIGHT_FADE? backlightTarget: backlightNow-BACKLIGHT_FADE); + } + // Serial.print(backlightNow,DEC); + // Serial.print(F(" => ")); + // Serial.println(backlightTarget,DEC); + analogWrite(BACKLIGHT_PIN,backlightNow); + } else { //just switch + backlightNow = backlightTarget = (backlightTarget<255? 0: 255); + digitalWrite(BACKLIGHT_PIN,(backlightNow?LOW:HIGH)); //LOW = device on } - // Serial.print(ledStateNow,DEC); - // Serial.print(F(" => ")); - // Serial.println(ledStateTarget,DEC); - analogWrite(LED_PIN,ledStateNow); + } +} +byte getVersionPart(byte part){ + switch(part){ + case 0: return vMajor; break; + case 1: return vMinor; break; + case 2: return vPatch; break; + case 3: return vDev; break; } } \ No newline at end of file diff --git a/arduino-clock/configs/led-iot.h b/arduino-clock/configs/led-iot.h new file mode 100644 index 0000000..2f54549 --- /dev/null +++ b/arduino-clock/configs/led-iot.h @@ -0,0 +1,104 @@ +//Arduino IoT with nothing but the LED connected + +#ifndef CONFIG +#define CONFIG + + +///// Functionality ///// + +// Which functionality is enabled in this clock? +// Related settings will also be enabled in the settings menu. +// The operating instructions assume all of these are enabled except temp and tubetest. +#define ENABLE_DATE_FN true // Date function, optionally including pages below +#define ENABLE_DATE_COUNTER true // Adds date page with an anniversary counter +#define ENABLE_DATE_RISESET true // Adds date pages with sunrise/sunset times. Requires Dusk2Dawn library by DM Kichi to be installed in IDE. +#define ENABLE_ALARM_FN true +#define ENABLE_ALARM_AUTOSKIP true +#define ENABLE_ALARM_FIBONACCI true +#define ENABLE_TIMER_FN true +#define ENABLE_TIME_CHIME true +#define ENABLE_SHUTOFF_NIGHT true // If disabled, tubes will be full brightness all the time. +#define ENABLE_SHUTOFF_AWAY true // Requires night shutoff. +#define ENABLE_TEMP_FN false //Temperature per DS3231 - will read high – leave false for production +#define ENABLE_TUBETEST_FN false //Cycles through all tubes – leave false for production + + +///// Real-Time Clock ///// +//If using no RTC (a fake RTC based on millis()): +#define RTC_MILLIS +#define ANTI_DRIFT 0 //msec to add/remove per second - or seconds to add/remove per day divided by 86.4 - to compensate for natural drift. If using wifinina, it really only needs to be good enough for a decent timekeeping display until the next ntp sync. TIP: setting to a superhigh value is helpful for testing! e.g. 9000 will make it run 10x speed + + +///// Inputs ///// +//If using IMU motion sensor on Nano 33 IoT: +//To use, tilt clock: backward=Sel, forward=Alt, left=Down, right=Up +//This is mutually exclusive with the button/rotary controls. +#define INPUT_IMU +//Which side of the IMU/Arduino faces clock front/side? 0=bottom, 1=top, 2=left side, 3=right side, 4=USB end, 5=butt end +#define IMU_FRONT 0 //(UNDB: 0) +#define IMU_TOP 4 //(UNDB: 4) +#define IMU_DEBOUNCING 150 //ms + +//For all input types: +//How long (in ms) are the hold durations? +#define CTRL_HOLD_SHORT_DUR 1000 //for entering setting mode, or hold-setting at low velocity (x1) +#define CTRL_HOLD_LONG_DUR 3000 //for entering settings menu, or hold-setting at high velocity (x10) +#define CTRL_HOLD_VERYLONG_DUR 5000 //for wifi info / admin start (Nano IoT without Alt only) +#define CTRL_HOLD_SUPERLONG_DUR 10000 //for wifi disconnect (Nano IoT) or EEPROM reset on startup +//What are the timeouts for setting and temporarily-displayed functions? up to 65535 sec +#define SETTING_TIMEOUT 300 //sec +#define FN_TEMP_TIMEOUT 5 //sec +#define FN_PAGE_TIMEOUT 3 //sec + +///// Display ///// +//If using 8x32 LED matrix: +//Requires LedControl library by Eberhard Farle to be installed in IDE. (http://wayoda.github.io/LedControl) +#define DISP_MAX7219 +#define NUM_MAX 4 //How many modules? 3 for 8x24 (4 digit, untested) or 4 for 8x32 (6 digit) +#define ROTATE 90 +#define BRIGHTNESS_FULL 7 //out of 0-15 +#define BRIGHTNESS_DIM 0 +//I've found that 7 (or 15?) and 0 make the least noise +//Which output pins? +#define CLK_PIN 2 //D2, pin 20 +#define CS_PIN 3 //D3, pin 21 +#define DIN_PIN 4 //D4, pin 22 +//and GND and VCC 5V + +//For all display types: +#define DISPLAY_SIZE 6 //number of digits in display module: 6 or 4 +#define UNOFF_DUR 10 //sec - when display is off, an input will illuminate for how long? +#define SCROLL_SPEED 100 //ms - "frame rate" of digit scrolling, e.g. date at :30 option + + +///// Other Outputs ///// + +//What are the pins for each signal type? -1 to disable that signal type +#define PIEZO_PIN -1 //Drives a piezo beeper +#define SWITCH_PIN -1 //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 SWITCH_DUR. (A3 for UNDB v9) +#define PULSE_PIN -1 //Simple pulses to control an intermittent signaling device like a solenoid or indicator lamp. Specify pulse duration in RELAY_PULSE. Pulse frequency behaves like the piezo signal. +//Default signal type for each function: +//0=piezo, 1=switch, 2=pulse +#define ALARM_SIGNAL 0 +#define TIMER_SIGNAL 0 +#define CHIME_SIGNAL 0 +#define SIGNAL_DUR 180 //sec - when piezo/pulse signal is going, it's pulsed once/sec for this period (e.g. 180 = 3min) +#define SWITCH_DUR 7200 //sec - when alarm triggers switch signal, it's switched on for this period (e.g. 7200 = 2hr) +#define PULSE_LENGTH 200 //ms - length of pulse signal's individual pulses (e.g. to drive a solenoid to ring a bell) + +//Soft power switches +#define ENABLE_SOFT_ALARM_SWITCH 1 +// 1 = yes. Alarm can be switched on and off when clock is displaying the alarm time (FN_ALARM). +// 0 = no. Alarm will be permanently on. Use with switch signal if the appliance has its own switch on this circuit (and note that, if another signal type(s) is available and selected for the alarm, the user won't be able to switch it off). Also disables skip feature. Note that the instructions do not reflect this option. +#define ENABLE_SOFT_POWER_SWITCH 1 //switch signal only +// 1 = yes. Switch signal can be toggled on and off directly with Alt button at any time (except in settings 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 circuit, e.g. some Sony Digimatic clock radios) or does not need to be manually switched. In this case (and/or if there is no switch signal option, and if no Wi-Fi support) Alt will act as a function preset. Note that the instructions do not reflect this option. + +//Backlighting control +#define BACKLIGHT_PIN -1 // -1 to disable feature; 9 if equipped (UNDB v9) +#define BACKLIGHT_FADE 0 +// 0 = no fading; simply switches on and off. +// >0 = backlight fades on and off via PWM (must use PWM pin and PWM-supportive lighting, such as LEDs). This value is the amount the PWM is increased/decreased per loop cycle. 10 is a good starting choice. + + +#endif \ No newline at end of file diff --git a/arduino-clock/configs/undb-v5.h b/arduino-clock/configs/undb-v5.h new file mode 100644 index 0000000..d96b0ea --- /dev/null +++ b/arduino-clock/configs/undb-v5.h @@ -0,0 +1,117 @@ +//UNDB v5, 6-tube display + +#ifndef CONFIG +#define CONFIG + + +///// Functionality ///// + +// Which functionality is enabled in this clock? +// Related settings will also be enabled in the settings menu. +// The operating instructions assume all of these are enabled except temp and tubetest. +#define ENABLE_DATE_FN true // Date function, optionally including pages below +#define ENABLE_DATE_COUNTER true // Adds date page with an anniversary counter +#define ENABLE_DATE_RISESET true // Adds date pages with sunrise/sunset times. Requires Dusk2Dawn library by DM Kichi to be installed in IDE. +#define ENABLE_ALARM_FN true +#define ENABLE_ALARM_AUTOSKIP true +#define ENABLE_ALARM_FIBONACCI true +#define ENABLE_TIMER_FN true +#define ENABLE_TIME_CHIME true +#define ENABLE_SHUTOFF_NIGHT true // If disabled, tubes will be full brightness all the time. +#define ENABLE_SHUTOFF_AWAY true // Requires night shutoff. +#define ENABLE_TEMP_FN false //Temperature per DS3231 - will read high – leave false for production +#define ENABLE_TUBETEST_FN false //Cycles through all tubes – leave false for production + + +///// Real-Time Clock ///// +//If using DS3231 (via I2C): +//Requires Wire library (standard Arduino) +//Requires DS3231 library by NorthernWidget to be installed in your IDE. +#define RTC_DS3231 + + +///// Inputs ///// + +//If using buttons for Select and optionally Alt: +#define INPUT_BUTTONS +#define CTRL_SEL A2 //UNDB S6/PL9 +#define CTRL_ALT -1 //if not using Alt, set to -1 + +//If using buttons for Up and Down: +#define INPUT_UPDN_BUTTONS +#define CTRL_UP A1 //UNDB S2/PL5 +#define CTRL_DN A0 //UNDB S3/PL6 + +//For all input types: +//How long (in ms) are the hold durations? +#define CTRL_HOLD_SHORT_DUR 1000 //for entering setting mode, or hold-setting at low velocity (x1) +#define CTRL_HOLD_LONG_DUR 3000 //for entering settings menu, or hold-setting at high velocity (x10) +#define CTRL_HOLD_VERYLONG_DUR 5000 //for wifi info / admin start (Nano IoT without Alt only) +#define CTRL_HOLD_SUPERLONG_DUR 10000 //for wifi disconnect (Nano IoT) or EEPROM reset on startup +//What are the timeouts for setting and temporarily-displayed functions? up to 65535 sec +#define SETTING_TIMEOUT 300 //sec +#define FN_TEMP_TIMEOUT 5 //sec +#define FN_PAGE_TIMEOUT 3 //sec + + +///// Display ///// +//These are mutually exclusive + +//If using nixie array: +#define DISP_NIXIE +#define CLEAN_SPEED 200 //ms - "frame rate" of tube cleaning +//Which output pins? +//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] +#define OUT_A1 2 +#define OUT_A2 3 +#define OUT_A3 4 +#define OUT_A4 5 +#define OUT_B1 6 +#define OUT_B2 7 +#define OUT_B3 8 +#define OUT_B4 9 +//3 pins out to anode channel switches +#define ANODE_1 11 +#define ANODE_2 12 +#define ANODE_3 13 + +//For all display types: +#define DISPLAY_SIZE 6 //number of digits in display module: 6 or 4 +#define UNOFF_DUR 10 //sec - when display is off, an input will illuminate for how long? +#define SCROLL_SPEED 100 //ms - "frame rate" of digit scrolling, e.g. date at :30 option + + +///// Other Outputs ///// + +//What are the pins for each signal type? -1 to disable that signal type +#define PIEZO_PIN 10 //Drives a piezo beeper +#define SWITCH_PIN -1 //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 SWITCH_DUR. (A3 for UNDB v9) +#define PULSE_PIN -1 //Simple pulses to control an intermittent signaling device like a solenoid or indicator lamp. Specify pulse duration in RELAY_PULSE. Pulse frequency behaves like the piezo signal. +//Default signal type for each function: +//0=piezo, 1=switch, 2=pulse +#define ALARM_SIGNAL 0 +#define TIMER_SIGNAL 0 +#define CHIME_SIGNAL 0 +#define SIGNAL_DUR 180 //sec - when piezo/pulse signal is going, it's pulsed once/sec for this period (e.g. 180 = 3min) +#define SWITCH_DUR 7200 //sec - when alarm triggers switch signal, it's switched on for this period (e.g. 7200 = 2hr) +#define PULSE_LENGTH 200 //ms - length of pulse signal's individual pulses (e.g. to drive a solenoid to ring a bell) + +//Soft power switches +#define ENABLE_SOFT_ALARM_SWITCH 1 +// 1 = yes. Alarm can be switched on and off when clock is displaying the alarm time (FN_ALARM). +// 0 = no. Alarm will be permanently on. Use with switch signal if the appliance has its own switch on this circuit (and note that, if another signal type(s) is available and selected for the alarm, the user won't be able to switch it off). Also disables skip feature. Note that the instructions do not reflect this option. +#define ENABLE_SOFT_POWER_SWITCH 1 //switch signal only +// 1 = yes. Switch signal can be toggled on and off directly with Alt button at any time (except in settings 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 circuit, e.g. some Sony Digimatic clock radios) or does not need to be manually switched. In this case (and/or if there is no switch signal option, and if no Wi-Fi support) Alt will act as a function preset. Note that the instructions do not reflect this option. + +//Backlighting control +#define BACKLIGHT_PIN -1 // -1 to disable feature; 9 if equipped (UNDB v9) +#define BACKLIGHT_FADE 0 +// 0 = no fading; simply switches on and off. +// >0 = backlight fades on and off via PWM (must use PWM pin and PWM-supportive lighting, such as LEDs). This value is the amount the PWM is increased/decreased per loop cycle. 10 is a good starting choice. + + +#endif \ No newline at end of file diff --git a/arduino-clock/configs/undb-v8-simplified.h b/arduino-clock/configs/undb-v8-simplified.h new file mode 100644 index 0000000..c483006 --- /dev/null +++ b/arduino-clock/configs/undb-v8-simplified.h @@ -0,0 +1,125 @@ +//Unmodified UNDB v8 with LED and relay disabled, and buttons as labeled, with 6-digit display. + +#ifndef CONFIG +#define CONFIG + + +///// Functionality ///// + +// Which functionality is enabled in this clock? +// Related settings will also be enabled in the settings menu. +// The operating instructions assume all of these are enabled except temp and tubetest. +#define ENABLE_DATE_FN true // Date function, optionally including pages below +#define ENABLE_DATE_COUNTER false // Adds date page with an anniversary counter +#define ENABLE_DATE_RISESET false // Adds date pages with sunrise/sunset times. Requires DM Kichi's Dusk2Dawn library to be installed in IDE. +#define ENABLE_ALARM_FN true +#define ENABLE_ALARM_AUTOSKIP false +#define ENABLE_ALARM_FIBONACCI false +#define ENABLE_TIMER_FN false +#define ENABLE_TIME_CHIME true +#define ENABLE_SHUTOFF_NIGHT true // If disabled, tubes will be full brightness all the time. +#define ENABLE_SHUTOFF_AWAY false // Requires night shutoff. +#define ENABLE_TEMP_FN false //Temperature per DS3231 - will read high – leave false for production +#define ENABLE_TUBETEST_FN false //Cycles through all tubes – leave false for production + + +///// Real-Time Clock ///// +//These are mutually exclusive + +//If using DS3231 (via I2C): +//Requires Wire library (standard Arduino) +//Requires DS3231 library by NorthernWidget to be installed in your IDE. +#define RTC_DS3231 + + +///// Inputs ///// + +//If using buttons for Select and optionally Alt: +#define INPUT_BUTTONS +#define CTRL_SEL A1 //UNDB S2/PL5 +#define CTRL_ALT A0 //UNDB S3/PL6 - if not using Alt, set to -1 + +//Up and Down can be buttons OR a rotary control: + +//If using buttons for Up and Down: +#define INPUT_UPDN_BUTTONS +#define CTRL_UP A2 //UNDB S6/PL9 +#define CTRL_DN A3 //UNDB S5/PL8 + +//For all input types: +//How long (in ms) are the hold durations? +#define CTRL_HOLD_SHORT_DUR 1000 //for entering setting mode, or hold-setting at low velocity (x1) +#define CTRL_HOLD_LONG_DUR 3000 //for entering settings menu, or hold-setting at high velocity (x10) +#define CTRL_HOLD_VERYLONG_DUR 5000 //for wifi info / admin start (Nano IoT without Alt only) +#define CTRL_HOLD_SUPERLONG_DUR 10000 //for wifi disconnect (Nano IoT) or EEPROM reset on startup +//What are the timeouts for setting and temporarily-displayed functions? up to 65535 sec +#define SETTING_TIMEOUT 300 //sec +#define FN_TEMP_TIMEOUT 5 //sec +#define FN_PAGE_TIMEOUT 3 //sec + +//Unused inputs +//A7 //UNDB S7/PL14 +//A6 //UNDB S4/PL7 + + +///// Display ///// +//These are mutually exclusive + +//If using nixie array: +#define DISP_NIXIE +#define CLEAN_SPEED 200 //ms - "frame rate" of tube cleaning +//Which output pins? +//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] +#define OUT_A1 2 +#define OUT_A2 3 +#define OUT_A3 4 +#define OUT_A4 5 +#define OUT_B1 6 +#define OUT_B2 7 +#define OUT_B3 8 +#define OUT_B4 9 +//3 pins out to anode channel switches +#define ANODE_1 11 +#define ANODE_2 12 +#define ANODE_3 13 + +//For all display types: +#define DISPLAY_SIZE 6 //number of digits in display module: 6 or 4 +#define UNOFF_DUR 10 //sec - when display is off, an input will illuminate for how long? +#define SCROLL_SPEED 100 //ms - "frame rate" of digit scrolling, e.g. date at :30 option + + +///// Other Outputs ///// + +//What are the pins for each signal type? -1 to disable that signal type +#define PIEZO_PIN 10 //Drives a piezo beeper +#define SWITCH_PIN -1 //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 SWITCH_DUR. (A3 for UNDB v9) +#define PULSE_PIN -1 //Simple pulses to control an intermittent signaling device like a solenoid or indicator lamp. Specify pulse duration in RELAY_PULSE. Pulse frequency behaves like the piezo signal. +//Default signal type for each function: +//0=piezo, 1=switch, 2=pulse +#define ALARM_SIGNAL 0 +#define TIMER_SIGNAL 0 +#define CHIME_SIGNAL 0 +#define SIGNAL_DUR 180 //sec - when piezo/pulse signal is going, it's pulsed once/sec for this period (e.g. 180 = 3min) +#define SWITCH_DUR 7200 //sec - when alarm triggers switch signal, it's switched on for this period (e.g. 7200 = 2hr) +#define PULSE_LENGTH 200 //ms - length of pulse signal's individual pulses (e.g. to drive a solenoid to ring a bell) + +//Soft power switches +#define ENABLE_SOFT_ALARM_SWITCH 1 +// 1 = yes. Alarm can be switched on and off when clock is displaying the alarm time (FN_ALARM). +// 0 = no. Alarm will be permanently on. Use with switch signal if the appliance has its own switch on this circuit (and note that, if another signal type(s) is available and selected for the alarm, the user won't be able to switch it off). Also disables skip feature. Note that the instructions do not reflect this option. +#define ENABLE_SOFT_POWER_SWITCH 1 //switch signal only +// 1 = yes. Switch signal can be toggled on and off directly with Alt button at any time (except in settings 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 circuit, e.g. some Sony Digimatic clock radios) or does not need to be manually switched. In this case (and/or if there is no switch signal option, and if no Wi-Fi support) Alt will act as a function preset. Note that the instructions do not reflect this option. + +//Backlighting control +#define BACKLIGHT_PIN -1 // -1 to disable feature; 9 if equipped (UNDB v9) +#define BACKLIGHT_FADE 0 +// 0 = no fading; simply switches on and off. +// >0 = backlight fades on and off via PWM (must use PWM pin and PWM-supportive lighting, such as LEDs). This value is the amount the PWM is increased/decreased per loop cycle. 10 is a good starting choice. + + +#endif \ No newline at end of file diff --git a/arduino-clock/configs/undb-v8.h b/arduino-clock/configs/undb-v8.h new file mode 100644 index 0000000..f2a520f --- /dev/null +++ b/arduino-clock/configs/undb-v8.h @@ -0,0 +1,125 @@ +//Unmodified UNDB v8 with LED and relay disabled, and buttons as labeled, with 6-digit display. + +#ifndef CONFIG +#define CONFIG + + +///// Functionality ///// + +// Which functionality is enabled in this clock? +// Related settings will also be enabled in the settings menu. +// The operating instructions assume all of these are enabled except temp and tubetest. +#define ENABLE_DATE_FN true // Date function, optionally including pages below +#define ENABLE_DATE_COUNTER true // Adds date page with an anniversary counter +#define ENABLE_DATE_RISESET true // Adds date pages with sunrise/sunset times. Requires Dusk2Dawn library by DM Kichi to be installed in IDE. +#define ENABLE_ALARM_FN true +#define ENABLE_ALARM_AUTOSKIP true +#define ENABLE_ALARM_FIBONACCI true +#define ENABLE_TIMER_FN true +#define ENABLE_TIME_CHIME true +#define ENABLE_SHUTOFF_NIGHT true // If disabled, tubes will be full brightness all the time. +#define ENABLE_SHUTOFF_AWAY true // Requires night shutoff. +#define ENABLE_TEMP_FN false //Temperature per DS3231 - will read high – leave false for production +#define ENABLE_TUBETEST_FN false //Cycles through all tubes – leave false for production + + +///// Real-Time Clock ///// +//These are mutually exclusive + +//If using DS3231 (via I2C): +//Requires Wire library (standard Arduino) +//Requires DS3231 library by NorthernWidget to be installed in your IDE. +#define RTC_DS3231 + + +///// Inputs ///// + +//If using buttons for Select and optionally Alt: +#define INPUT_BUTTONS +#define CTRL_SEL A1 //UNDB S2/PL5 +#define CTRL_ALT A0 //UNDB S3/PL6 - if not using Alt, set to -1 + +//Up and Down can be buttons OR a rotary control: + +//If using buttons for Up and Down: +#define INPUT_UPDN_BUTTONS +#define CTRL_UP A2 //UNDB S6/PL9 +#define CTRL_DN A3 //UNDB S5/PL8 + +//For all input types: +//How long (in ms) are the hold durations? +#define CTRL_HOLD_SHORT_DUR 1000 //for entering setting mode, or hold-setting at low velocity (x1) +#define CTRL_HOLD_LONG_DUR 3000 //for entering settings menu, or hold-setting at high velocity (x10) +#define CTRL_HOLD_VERYLONG_DUR 5000 //for wifi info / admin start (Nano IoT without Alt only) +#define CTRL_HOLD_SUPERLONG_DUR 10000 //for wifi disconnect (Nano IoT) or EEPROM reset on startup +//What are the timeouts for setting and temporarily-displayed functions? up to 65535 sec +#define SETTING_TIMEOUT 300 //sec +#define FN_TEMP_TIMEOUT 5 //sec +#define FN_PAGE_TIMEOUT 3 //sec + +//Unused inputs +//A7 //UNDB S7/PL14 +//A6 //UNDB S4/PL7 + + +///// Display ///// +//These are mutually exclusive + +//If using nixie array: +#define DISP_NIXIE +#define CLEAN_SPEED 200 //ms - "frame rate" of tube cleaning +//Which output pins? +//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] +#define OUT_A1 2 +#define OUT_A2 3 +#define OUT_A3 4 +#define OUT_A4 5 +#define OUT_B1 6 +#define OUT_B2 7 +#define OUT_B3 8 +#define OUT_B4 9 +//3 pins out to anode channel switches +#define ANODE_1 11 +#define ANODE_2 12 +#define ANODE_3 13 + +//For all display types: +#define DISPLAY_SIZE 6 //number of digits in display module: 6 or 4 +#define UNOFF_DUR 10 //sec - when display is off, an input will illuminate for how long? +#define SCROLL_SPEED 100 //ms - "frame rate" of digit scrolling, e.g. date at :30 option + + +///// Other Outputs ///// + +//What are the pins for each signal type? -1 to disable that signal type +#define PIEZO_PIN 10 //Drives a piezo beeper +#define SWITCH_PIN -1 //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 SWITCH_DUR. (A3 for UNDB v9) +#define PULSE_PIN -1 //Simple pulses to control an intermittent signaling device like a solenoid or indicator lamp. Specify pulse duration in RELAY_PULSE. Pulse frequency behaves like the piezo signal. +//Default signal type for each function: +//0=piezo, 1=switch, 2=pulse +#define ALARM_SIGNAL 0 +#define TIMER_SIGNAL 0 +#define CHIME_SIGNAL 0 +#define SIGNAL_DUR 180 //sec - when piezo/pulse signal is going, it's pulsed once/sec for this period (e.g. 180 = 3min) +#define SWITCH_DUR 7200 //sec - when alarm triggers switch signal, it's switched on for this period (e.g. 7200 = 2hr) +#define PULSE_LENGTH 200 //ms - length of pulse signal's individual pulses (e.g. to drive a solenoid to ring a bell) + +//Soft power switches +#define ENABLE_SOFT_ALARM_SWITCH 1 +// 1 = yes. Alarm can be switched on and off when clock is displaying the alarm time (FN_ALARM). +// 0 = no. Alarm will be permanently on. Use with switch signal if the appliance has its own switch on this circuit (and note that, if another signal type(s) is available and selected for the alarm, the user won't be able to switch it off). Also disables skip feature. Note that the instructions do not reflect this option. +#define ENABLE_SOFT_POWER_SWITCH 1 //switch signal only +// 1 = yes. Switch signal can be toggled on and off directly with Alt button at any time (except in settings 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 circuit, e.g. some Sony Digimatic clock radios) or does not need to be manually switched. In this case (and/or if there is no switch signal option, and if no Wi-Fi support) Alt will act as a function preset. Note that the instructions do not reflect this option. + +//Backlighting control +#define BACKLIGHT_PIN -1 // -1 to disable feature; 9 if equipped (UNDB v9) +#define BACKLIGHT_FADE 0 +// 0 = no fading; simply switches on and off. +// >0 = backlight fades on and off via PWM (must use PWM pin and PWM-supportive lighting, such as LEDs). This value is the amount the PWM is increased/decreased per loop cycle. 10 is a good starting choice. + + +#endif \ No newline at end of file diff --git a/arduino-clock/configs/undb-v9-relay.h b/arduino-clock/configs/undb-v9-relay.h new file mode 100644 index 0000000..caa4c5e --- /dev/null +++ b/arduino-clock/configs/undb-v9-relay.h @@ -0,0 +1,126 @@ +//UNDB v9, relay enabled, buttons as labeled, with 6-digit display. +//Also for v8 modified to v9 spec (Sel/Alt on A6/A7, Up/Down on A0/A1, relay on A3, led on 9, and cathode B4 on A2) + +#ifndef CONFIG +#define CONFIG + + +///// Functionality ///// + +// Which functionality is enabled in this clock? +// Related settings will also be enabled in the settings menu. +// The operating instructions assume all of these are enabled except temp and tubetest. +#define ENABLE_DATE_FN true // Date function, optionally including pages below +#define ENABLE_DATE_COUNTER true // Adds date page with an anniversary counter +#define ENABLE_DATE_RISESET true // Adds date pages with sunrise/sunset times. Requires Dusk2Dawn library by DM Kichi to be installed in IDE. +#define ENABLE_ALARM_FN true +#define ENABLE_ALARM_AUTOSKIP true +#define ENABLE_ALARM_FIBONACCI true +#define ENABLE_TIMER_FN true +#define ENABLE_TIME_CHIME true +#define ENABLE_SHUTOFF_NIGHT true // If disabled, tubes will be full brightness all the time. +#define ENABLE_SHUTOFF_AWAY true // Requires night shutoff. +#define ENABLE_TEMP_FN false //Temperature per DS3231 - will read high – leave false for production +#define ENABLE_TUBETEST_FN false //Cycles through all tubes – leave false for production + + +///// Real-Time Clock ///// +//These are mutually exclusive + +//If using DS3231 (via I2C): +//Requires Wire library (standard Arduino) +//Requires DS3231 library by NorthernWidget to be installed in your IDE. +#define RTC_DS3231 + + +///// Inputs ///// + +//If using buttons for Select and optionally Alt: +#define INPUT_BUTTONS +#define CTRL_SEL A6 //UNDB S4/PL7 +#define CTRL_ALT A7 //UNDB S7/PL14 - if not using Alt, set to -1 + +//Up and Down can be buttons OR a rotary control: + +//If using buttons for Up and Down: +#define INPUT_UPDN_BUTTONS +#define CTRL_UP A0 //UNDB S3/PL6 +#define CTRL_DN A1 //UNDB S2/PL5 + +//For all input types: +//How long (in ms) are the hold durations? +#define CTRL_HOLD_SHORT_DUR 1000 //for entering setting mode, or hold-setting at low velocity (x1) +#define CTRL_HOLD_LONG_DUR 3000 //for entering settings menu, or hold-setting at high velocity (x10) +#define CTRL_HOLD_VERYLONG_DUR 5000 //for wifi info / admin start (Nano IoT without Alt only) +#define CTRL_HOLD_SUPERLONG_DUR 10000 //for wifi disconnect (Nano IoT) or EEPROM reset on startup +//What are the timeouts for setting and temporarily-displayed functions? up to 65535 sec +#define SETTING_TIMEOUT 300 //sec +#define FN_TEMP_TIMEOUT 5 //sec +#define FN_PAGE_TIMEOUT 3 //sec + +//Unused inputs +//A3 //UNDB S5/PL8 +//A2 //UNDB S6/PL9 + + +///// Display ///// +//These are mutually exclusive + +//If using nixie array: +#define DISP_NIXIE +#define CLEAN_SPEED 200 //ms - "frame rate" of tube cleaning +//Which output pins? +//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] +#define OUT_A1 2 +#define OUT_A2 3 +#define OUT_A3 4 +#define OUT_A4 5 +#define OUT_B1 6 +#define OUT_B2 7 +#define OUT_B3 8 +#define OUT_B4 16 //aka A2 +//3 pins out to anode channel switches +#define ANODE_1 11 +#define ANODE_2 12 +#define ANODE_3 13 + +//For all display types: +#define DISPLAY_SIZE 6 //number of digits in display module: 6 or 4 +#define UNOFF_DUR 10 //sec - when display is off, an input will illuminate for how long? +#define SCROLL_SPEED 100 //ms - "frame rate" of digit scrolling, e.g. date at :30 option + + +///// Other Outputs ///// + +//What are the pins for each signal type? -1 to disable that signal type +#define PIEZO_PIN 10 //Drives a piezo beeper +#define SWITCH_PIN A3 //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 SWITCH_DUR. (A3 for UNDB v9) +#define PULSE_PIN -1 //Simple pulses to control an intermittent signaling device like a solenoid or indicator lamp. Specify pulse duration in RELAY_PULSE. Pulse frequency behaves like the piezo signal. +//Default signal type for each function: +//0=piezo, 1=switch, 2=pulse +#define ALARM_SIGNAL 0 +#define TIMER_SIGNAL 0 +#define CHIME_SIGNAL 0 +#define SIGNAL_DUR 180 //sec - when piezo/pulse signal is going, it's pulsed once/sec for this period (e.g. 180 = 3min) +#define SWITCH_DUR 7200 //sec - when alarm triggers switch signal, it's switched on for this period (e.g. 7200 = 2hr) +#define PULSE_LENGTH 200 //ms - length of pulse signal's individual pulses (e.g. to drive a solenoid to ring a bell) + +//Soft power switches +#define ENABLE_SOFT_ALARM_SWITCH 1 +// 1 = yes. Alarm can be switched on and off when clock is displaying the alarm time (FN_ALARM). +// 0 = no. Alarm will be permanently on. Use with switch signal if the appliance has its own switch on this circuit (and note that, if another signal type(s) is available and selected for the alarm, the user won't be able to switch it off). Also disables skip feature. Note that the instructions do not reflect this option. +#define ENABLE_SOFT_POWER_SWITCH 1 //switch signal only +// 1 = yes. Switch signal can be toggled on and off directly with Alt button at any time (except in settings 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 circuit, e.g. some Sony Digimatic clock radios) or does not need to be manually switched. In this case (and/or if there is no switch signal option, and if no Wi-Fi support) Alt will act as a function preset. Note that the instructions do not reflect this option. + +//Backlighting control +#define BACKLIGHT_PIN 9 // -1 to disable feature; 9 if equipped (UNDB v9) +#define BACKLIGHT_FADE 0 +// 0 = no fading; simply switches on and off. +// >0 = backlight fades on and off via PWM (must use PWM pin and PWM-supportive lighting, such as LEDs). This value is the amount the PWM is increased/decreased per loop cycle. 10 is a good starting choice. + + +#endif \ No newline at end of file diff --git a/arduino-clock/configs/undb-v9-simplified.h b/arduino-clock/configs/undb-v9-simplified.h new file mode 100644 index 0000000..78c4cfc --- /dev/null +++ b/arduino-clock/configs/undb-v9-simplified.h @@ -0,0 +1,126 @@ +//UNDB v9, relay disabled, buttons as labeled, with 6-digit display. +//Also for v8 modified to v9 spec (Sel/Alt on A6/A7, Up/Down on A0/A1, relay on A3, led on 9, and cathode B4 on A2) + +#ifndef CONFIG +#define CONFIG + + +///// Functionality ///// + +// Which functionality is enabled in this clock? +// Related settings will also be enabled in the settings menu. +// The operating instructions assume all of these are enabled except temp and tubetest. +#define ENABLE_DATE_FN true // Date function, optionally including pages below +#define ENABLE_DATE_COUNTER false // Adds date page with an anniversary counter +#define ENABLE_DATE_RISESET false // Adds date pages with sunrise/sunset times. Requires DM Kichi's Dusk2Dawn library to be installed in IDE. +#define ENABLE_ALARM_FN true +#define ENABLE_ALARM_AUTOSKIP false +#define ENABLE_ALARM_FIBONACCI false +#define ENABLE_TIMER_FN false +#define ENABLE_TIME_CHIME true +#define ENABLE_SHUTOFF_NIGHT true // If disabled, tubes will be full brightness all the time. +#define ENABLE_SHUTOFF_AWAY false // Requires night shutoff. +#define ENABLE_TEMP_FN false //Temperature per DS3231 - will read high – leave false for production +#define ENABLE_TUBETEST_FN false //Cycles through all tubes – leave false for production + + +///// Real-Time Clock ///// +//These are mutually exclusive + +//If using DS3231 (via I2C): +//Requires Wire library (standard Arduino) +//Requires DS3231 library by NorthernWidget to be installed in your IDE. +#define RTC_DS3231 + + +///// Inputs ///// + +//If using buttons for Select and optionally Alt: +#define INPUT_BUTTONS +#define CTRL_SEL A6 //UNDB S4/PL7 +#define CTRL_ALT A7 //UNDB S7/PL14 - if not using Alt, set to -1 + +//Up and Down can be buttons OR a rotary control: + +//If using buttons for Up and Down: +#define INPUT_UPDN_BUTTONS +#define CTRL_UP A0 //UNDB S3/PL6 +#define CTRL_DN A1 //UNDB S2/PL5 + +//For all input types: +//How long (in ms) are the hold durations? +#define CTRL_HOLD_SHORT_DUR 1000 //for entering setting mode, or hold-setting at low velocity (x1) +#define CTRL_HOLD_LONG_DUR 3000 //for entering settings menu, or hold-setting at high velocity (x10) +#define CTRL_HOLD_VERYLONG_DUR 5000 //for wifi info / admin start (Nano IoT without Alt only) +#define CTRL_HOLD_SUPERLONG_DUR 10000 //for wifi disconnect (Nano IoT) or EEPROM reset on startup +//What are the timeouts for setting and temporarily-displayed functions? up to 65535 sec +#define SETTING_TIMEOUT 300 //sec +#define FN_TEMP_TIMEOUT 5 //sec +#define FN_PAGE_TIMEOUT 3 //sec + +//Unused inputs +//A3 //UNDB S5/PL8 +//A2 //UNDB S6/PL9 + + +///// Display ///// +//These are mutually exclusive + +//If using nixie array: +#define DISP_NIXIE +#define CLEAN_SPEED 200 //ms - "frame rate" of tube cleaning +//Which output pins? +//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] +#define OUT_A1 2 +#define OUT_A2 3 +#define OUT_A3 4 +#define OUT_A4 5 +#define OUT_B1 6 +#define OUT_B2 7 +#define OUT_B3 8 +#define OUT_B4 16 //aka A2 +//3 pins out to anode channel switches +#define ANODE_1 11 +#define ANODE_2 12 +#define ANODE_3 13 + +//For all display types: +#define DISPLAY_SIZE 6 //number of digits in display module: 6 or 4 +#define UNOFF_DUR 10 //sec - when display is off, an input will illuminate for how long? +#define SCROLL_SPEED 100 //ms - "frame rate" of digit scrolling, e.g. date at :30 option + + +///// Other Outputs ///// + +//What are the pins for each signal type? -1 to disable that signal type +#define PIEZO_PIN 10 //Drives a piezo beeper +#define SWITCH_PIN -1 //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 SWITCH_DUR. (A3 for UNDB v9) +#define PULSE_PIN -1 //Simple pulses to control an intermittent signaling device like a solenoid or indicator lamp. Specify pulse duration in RELAY_PULSE. Pulse frequency behaves like the piezo signal. +//Default signal type for each function: +//0=piezo, 1=switch, 2=pulse +#define ALARM_SIGNAL 0 +#define TIMER_SIGNAL 0 +#define CHIME_SIGNAL 0 +#define SIGNAL_DUR 180 //sec - when piezo/pulse signal is going, it's pulsed once/sec for this period (e.g. 180 = 3min) +#define SWITCH_DUR 7200 //sec - when alarm triggers switch signal, it's switched on for this period (e.g. 7200 = 2hr) +#define PULSE_LENGTH 200 //ms - length of pulse signal's individual pulses (e.g. to drive a solenoid to ring a bell) + +//Soft power switches +#define ENABLE_SOFT_ALARM_SWITCH 1 +// 1 = yes. Alarm can be switched on and off when clock is displaying the alarm time (FN_ALARM). +// 0 = no. Alarm will be permanently on. Use with switch signal if the appliance has its own switch on this circuit (and note that, if another signal type(s) is available and selected for the alarm, the user won't be able to switch it off). Also disables skip feature. Note that the instructions do not reflect this option. +#define ENABLE_SOFT_POWER_SWITCH 1 //switch signal only +// 1 = yes. Switch signal can be toggled on and off directly with Alt button at any time (except in settings 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 circuit, e.g. some Sony Digimatic clock radios) or does not need to be manually switched. In this case (and/or if there is no switch signal option, and if no Wi-Fi support) Alt will act as a function preset. Note that the instructions do not reflect this option. + +//Backlighting control +#define BACKLIGHT_PIN 9 // -1 to disable feature; 9 if equipped (UNDB v9) +#define BACKLIGHT_FADE 0 +// 0 = no fading; simply switches on and off. +// >0 = backlight fades on and off via PWM (must use PWM pin and PWM-supportive lighting, such as LEDs). This value is the amount the PWM is increased/decreased per loop cycle. 10 is a good starting choice. + + +#endif \ No newline at end of file diff --git a/arduino-clock/configs/undb-v9.h b/arduino-clock/configs/undb-v9.h new file mode 100644 index 0000000..036c3e9 --- /dev/null +++ b/arduino-clock/configs/undb-v9.h @@ -0,0 +1,126 @@ +//UNDB v9, relay disabled, buttons as labeled, with 6-digit display. +//Also for v8 modified to v9 spec (Sel/Alt on A6/A7, Up/Down on A0/A1, relay on A3, led on 9, and cathode B4 on A2) + +#ifndef CONFIG +#define CONFIG + + +///// Functionality ///// + +// Which functionality is enabled in this clock? +// Related settings will also be enabled in the settings menu. +// The operating instructions assume all of these are enabled except temp and tubetest. +#define ENABLE_DATE_FN true // Date function, optionally including pages below +#define ENABLE_DATE_COUNTER true // Adds date page with an anniversary counter +#define ENABLE_DATE_RISESET true // Adds date pages with sunrise/sunset times. Requires Dusk2Dawn library by DM Kichi to be installed in IDE. +#define ENABLE_ALARM_FN true +#define ENABLE_ALARM_AUTOSKIP true +#define ENABLE_ALARM_FIBONACCI true +#define ENABLE_TIMER_FN true +#define ENABLE_TIME_CHIME true +#define ENABLE_SHUTOFF_NIGHT true // If disabled, tubes will be full brightness all the time. +#define ENABLE_SHUTOFF_AWAY true // Requires night shutoff. +#define ENABLE_TEMP_FN false //Temperature per DS3231 - will read high – leave false for production +#define ENABLE_TUBETEST_FN false //Cycles through all tubes – leave false for production + + +///// Real-Time Clock ///// +//These are mutually exclusive + +//If using DS3231 (via I2C): +//Requires Wire library (standard Arduino) +//Requires DS3231 library by NorthernWidget to be installed in your IDE. +#define RTC_DS3231 + + +///// Inputs ///// + +//If using buttons for Select and optionally Alt: +#define INPUT_BUTTONS +#define CTRL_SEL A6 //UNDB S4/PL7 +#define CTRL_ALT A7 //UNDB S7/PL14 - if not using Alt, set to -1 + +//Up and Down can be buttons OR a rotary control: + +//If using buttons for Up and Down: +#define INPUT_UPDN_BUTTONS +#define CTRL_UP A0 //UNDB S3/PL6 +#define CTRL_DN A1 //UNDB S2/PL5 + +//For all input types: +//How long (in ms) are the hold durations? +#define CTRL_HOLD_SHORT_DUR 1000 //for entering setting mode, or hold-setting at low velocity (x1) +#define CTRL_HOLD_LONG_DUR 3000 //for entering settings menu, or hold-setting at high velocity (x10) +#define CTRL_HOLD_VERYLONG_DUR 5000 //for wifi info / admin start (Nano IoT without Alt only) +#define CTRL_HOLD_SUPERLONG_DUR 10000 //for wifi disconnect (Nano IoT) or EEPROM reset on startup +//What are the timeouts for setting and temporarily-displayed functions? up to 65535 sec +#define SETTING_TIMEOUT 300 //sec +#define FN_TEMP_TIMEOUT 5 //sec +#define FN_PAGE_TIMEOUT 3 //sec + +//Unused inputs +//A3 //UNDB S5/PL8 +//A2 //UNDB S6/PL9 + + +///// Display ///// +//These are mutually exclusive + +//If using nixie array: +#define DISP_NIXIE +#define CLEAN_SPEED 200 //ms - "frame rate" of tube cleaning +//Which output pins? +//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] +#define OUT_A1 2 +#define OUT_A2 3 +#define OUT_A3 4 +#define OUT_A4 5 +#define OUT_B1 6 +#define OUT_B2 7 +#define OUT_B3 8 +#define OUT_B4 16 //aka A2 +//3 pins out to anode channel switches +#define ANODE_1 11 +#define ANODE_2 12 +#define ANODE_3 13 + +//For all display types: +#define DISPLAY_SIZE 6 //number of digits in display module: 6 or 4 +#define UNOFF_DUR 10 //sec - when display is off, an input will illuminate for how long? +#define SCROLL_SPEED 100 //ms - "frame rate" of digit scrolling, e.g. date at :30 option + + +///// Other Outputs ///// + +//What are the pins for each signal type? -1 to disable that signal type +#define PIEZO_PIN 10 //Drives a piezo beeper +#define SWITCH_PIN -1 //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 SWITCH_DUR. (A3 for UNDB v9) +#define PULSE_PIN -1 //Simple pulses to control an intermittent signaling device like a solenoid or indicator lamp. Specify pulse duration in RELAY_PULSE. Pulse frequency behaves like the piezo signal. +//Default signal type for each function: +//0=piezo, 1=switch, 2=pulse +#define ALARM_SIGNAL 0 +#define TIMER_SIGNAL 0 +#define CHIME_SIGNAL 0 +#define SIGNAL_DUR 180 //sec - when piezo/pulse signal is going, it's pulsed once/sec for this period (e.g. 180 = 3min) +#define SWITCH_DUR 7200 //sec - when alarm triggers switch signal, it's switched on for this period (e.g. 7200 = 2hr) +#define PULSE_LENGTH 200 //ms - length of pulse signal's individual pulses (e.g. to drive a solenoid to ring a bell) + +//Soft power switches +#define ENABLE_SOFT_ALARM_SWITCH 1 +// 1 = yes. Alarm can be switched on and off when clock is displaying the alarm time (FN_ALARM). +// 0 = no. Alarm will be permanently on. Use with switch signal if the appliance has its own switch on this circuit (and note that, if another signal type(s) is available and selected for the alarm, the user won't be able to switch it off). Also disables skip feature. Note that the instructions do not reflect this option. +#define ENABLE_SOFT_POWER_SWITCH 1 //switch signal only +// 1 = yes. Switch signal can be toggled on and off directly with Alt button at any time (except in settings 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 circuit, e.g. some Sony Digimatic clock radios) or does not need to be manually switched. In this case (and/or if there is no switch signal option, and if no Wi-Fi support) Alt will act as a function preset. Note that the instructions do not reflect this option. + +//Backlighting control +#define BACKLIGHT_PIN 9 // -1 to disable feature; 9 if equipped (UNDB v9) +#define BACKLIGHT_FADE 0 +// 0 = no fading; simply switches on and off. +// >0 = backlight fades on and off via PWM (must use PWM pin and PWM-supportive lighting, such as LEDs). This value is the amount the PWM is increased/decreased per loop cycle. 10 is a good starting choice. + + +#endif \ No newline at end of file diff --git a/arduino-clock/configs/~sample.h b/arduino-clock/configs/~sample.h new file mode 100644 index 0000000..586144a --- /dev/null +++ b/arduino-clock/configs/~sample.h @@ -0,0 +1,163 @@ +//This config includes all options this project supports (displays, inputs, etc). +//You can make a copy and include only the portions relevant to your clock's hardware. + +#ifndef CONFIG +#define CONFIG + + +///// Functionality ///// + +// Which functionality is enabled in this clock? +// Related settings will also be enabled in the settings menu. +// The operating instructions assume all of these are enabled except temp and tubetest. +#define ENABLE_DATE_FN true // Date function, optionally including pages below +#define ENABLE_DATE_COUNTER true // Adds date page with an anniversary counter +#define ENABLE_DATE_RISESET true // Adds date pages with sunrise/sunset times. Requires Dusk2Dawn library by DM Kichi to be installed in IDE. +#define ENABLE_ALARM_FN true +#define ENABLE_ALARM_AUTOSKIP true +#define ENABLE_ALARM_FIBONACCI true +#define ENABLE_TIMER_FN true +#define ENABLE_TIME_CHIME true +#define ENABLE_SHUTOFF_NIGHT true // If disabled, tubes will be full brightness all the time. +#define ENABLE_SHUTOFF_AWAY true // Requires night shutoff. +#define ENABLE_TEMP_FN false //Temperature per DS3231 - will read high – leave false for production +#define ENABLE_TUBETEST_FN false //Cycles through all tubes – leave false for production + + +///// Real-Time Clock ///// +//These are mutually exclusive + +//If using DS3231 (via I2C): +//Requires Wire library (standard Arduino) +//Requires DS3231 library by NorthernWidget to be installed in your IDE. +// #define RTC_DS3231 + +//If using RTCZero on Nano 33 IoT: //TODO +// #define RTC_ZERO + +//If using no RTC (a fake RTC based on millis()): +#define RTC_MILLIS +#define ANTI_DRIFT 0 //msec to add/remove per second - or seconds to add/remove per day divided by 86.4 - to compensate for natural drift. If using wifinina, it really only needs to be good enough for a decent timekeeping display until the next ntp sync. TIP: setting to a superhigh value is helpful for testing! e.g. 9000 will make it run 10x speed + + +///// Inputs ///// + +//If using buttons for Select and optionally Alt: +// #define INPUT_BUTTONS +// #define CTRL_SEL A6 //UNDB S4/PL7 +// #define CTRL_ALT A7 //UNDB S7/PL14 - if not using Alt, set to -1 + +//Up and Down can be buttons OR a rotary control: + +//If using buttons for Up and Down: +// #define INPUT_UPDN_BUTTONS +// #define CTRL_UP A0 //UNDB S3/PL6 +// #define CTRL_DN A1 //UNDB S2/PL5 + +//If using rotary control for Up and Down: +//Requires Encoder library by Paul Stoffregen to be installed in IDE. +// #define INPUT_UPDN_ROTARY +// #define CTRL_R1 A2 +// #define CTRL_R2 A3 +// #define ROT_VEL_START 80 //If step rate falls below this, kick into high velocity set (x10) +// #define ROT_VEL_STOP 500 //If step rate rises above this, drop into low velocity set (x1) + +//If using IMU motion sensor on Nano 33 IoT: +//To use, tilt clock: backward=Sel, forward=Alt, left=Down, right=Up +//This is mutually exclusive with the button/rotary controls. TODO make it possible to use both together by renaming the functions or abstracting basic input functionality +#define INPUT_IMU +//Which side of the IMU/Arduino faces clock front/side? 0=bottom, 1=top, 2=left side, 3=right side, 4=USB end, 5=butt end +#define IMU_FRONT 0 //(UNDB: 0) +#define IMU_TOP 4 //(UNDB: 4) +#define IMU_DEBOUNCING 150 //ms + +//For all input types: +//How long (in ms) are the hold durations? +#define CTRL_HOLD_SHORT_DUR 1000 //for entering setting mode, or hold-setting at low velocity (x1) +#define CTRL_HOLD_LONG_DUR 3000 //for entering settings menu, or hold-setting at high velocity (x10) +#define CTRL_HOLD_VERYLONG_DUR 5000 //for wifi info / admin start (Nano IoT without Alt only) +#define CTRL_HOLD_SUPERLONG_DUR 10000 //for wifi disconnect (Nano IoT) or EEPROM reset on startup +//What are the timeouts for setting and temporarily-displayed functions? up to 65535 sec +#define SETTING_TIMEOUT 300 //sec +#define FN_TEMP_TIMEOUT 5 //sec +#define FN_PAGE_TIMEOUT 3 //sec + +//Unused inputs +//A3 //UNDB S5/PL8 +//A2 //UNDB S6/PL9 + + +///// Display ///// +//These are mutually exclusive + +//If using nixie array: +// #define DISP_NIXIE +// #define CLEAN_SPEED 200 //ms - "frame rate" of tube cleaning +// //Which output pins? +// //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] +// #define OUT_A1 2 +// #define OUT_A2 3 +// #define OUT_A3 4 +// #define OUT_A4 5 +// #define OUT_B1 6 +// #define OUT_B2 7 +// #define OUT_B3 8 +// #define OUT_B4 16 //aka A2 +// //3 pins out to anode channel switches +// #define ANODE_1 11 +// #define ANODE_2 12 +// #define ANODE_3 13 + +//If using 8x32 LED matrix: +//Requires LedControl library by Eberhard Farle to be installed in IDE. (http://wayoda.github.io/LedControl) +#define DISP_MAX7219 +#define NUM_MAX 4 //How many modules? 3 for 8x24 (4 digit, untested) or 4 for 8x32 (6 digit) +#define ROTATE 90 +#define BRIGHTNESS_FULL 7 //out of 0-15 +#define BRIGHTNESS_DIM 0 +//I've found that 7 (or 15?) and 0 make the least noise +//Which output pins? +#define CLK_PIN 2 //D2, pin 20 +#define CS_PIN 3 //D3, pin 21 +#define DIN_PIN 4 //D4, pin 22 + +//For all display types: +#define DISPLAY_SIZE 6 //number of digits in display module: 6 or 4 +#define UNOFF_DUR 10 //sec - when display is off, an input will illuminate for how long? +#define SCROLL_SPEED 100 //ms - "frame rate" of digit scrolling, e.g. date at :30 option + + +///// Other Outputs ///// + +//What are the pins for each signal type? -1 to disable that signal type +#define PIEZO_PIN -1 //Drives a piezo beeper +#define SWITCH_PIN -1 //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 SWITCH_DUR. (A3 for UNDB v9) +#define PULSE_PIN -1 //Simple pulses to control an intermittent signaling device like a solenoid or indicator lamp. Specify pulse duration in RELAY_PULSE. Pulse frequency behaves like the piezo signal. +//Default signal type for each function: +//0=piezo, 1=switch, 2=pulse +#define ALARM_SIGNAL 0 +#define TIMER_SIGNAL 0 +#define CHIME_SIGNAL 0 +#define SIGNAL_DUR 180 //sec - when piezo/pulse signal is going, it's pulsed once/sec for this period (e.g. 180 = 3min) +#define SWITCH_DUR 7200 //sec - when alarm triggers switch signal, it's switched on for this period (e.g. 7200 = 2hr) +#define PULSE_LENGTH 200 //ms - length of pulse signal's individual pulses (e.g. to drive a solenoid to ring a bell) + +//Soft power switches +#define ENABLE_SOFT_ALARM_SWITCH 1 +// 1 = yes. Alarm can be switched on and off when clock is displaying the alarm time (FN_ALARM). +// 0 = no. Alarm will be permanently on. Use with switch signal if the appliance has its own switch on this circuit (and note that, if another signal type(s) is available and selected for the alarm, the user won't be able to switch it off). Also disables skip feature. Note that the instructions do not reflect this option. +#define ENABLE_SOFT_POWER_SWITCH 1 //switch signal only +// 1 = yes. Switch signal can be toggled on and off directly with Alt button at any time (except in settings 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 circuit, e.g. some Sony Digimatic clock radios) or does not need to be manually switched. In this case (and/or if there is no switch signal option, and if no Wi-Fi support) Alt will act as a function preset. Note that the instructions do not reflect this option. + +//Backlighting control +#define BACKLIGHT_PIN -1 // -1 to disable feature; 9 if equipped (UNDB v9) +#define BACKLIGHT_FADE 0 +// 0 = no fading; simply switches on and off. +// >0 = backlight fades on and off via PWM (must use PWM pin and PWM-supportive lighting, such as LEDs). This value is the amount the PWM is increased/decreased per loop cycle. 10 is a good starting choice. + + +#endif \ No newline at end of file diff --git a/arduino-clock/dispMAX7219.cpp b/arduino-clock/dispMAX7219.cpp new file mode 100644 index 0000000..85f510c --- /dev/null +++ b/arduino-clock/dispMAX7219.cpp @@ -0,0 +1,138 @@ +#include +#include "arduino-clock.h" + +#ifdef DISP_MAX7219 //see arduino-clock.ino Includes section + +#include "dispMAX7219.h" +#include //Arduino - for SPI access to MAX7219 +#include //Eberhard Farle's LedControl library - http://wayoda.github.io/LedControl + +LedControl lc=LedControl(DIN_PIN,CLK_PIN,CS_PIN,NUM_MAX); + +int curBrightness = BRIGHTNESS_FULL; + +unsigned long displayBlinkStart = 0; //when nonzero, display should briefly blank + +void initDisplay(){ + for(int i=0; i3 && NUM_MAX<=3) return; //if 3 or fewer matrices, don't render digits 4 and 5 + col = //h tens at far left + (i>0? bignumWidth+1: 0)+ //h ones + (i>1? bignumWidth+2: 0)+ //m tens + (i>2? bignumWidth+1: 0)+ //m ones + (i>3? bignumWidth+1: 0)+ //s tens + (i>4? smallnumWidth+1: 0); //s ones + for(int j=0; j<(i<4? bignumWidth: smallnumWidth); j++){ //For each column of this number + ci = ((NUM_MAX*8)-1)-(col+j); //translate from our column count to MAX's column count + lc.setColumn( + (NUM_MAX-1)-(ci/8), //display index + ci%8, //display column index + (displayNext[i]==15?0: + (i<4? bignum[displayNext[i]*bignumWidth+j]: smallnum[displayNext[i]*smallnumWidth+j]) + ) + ); + } + } +} + +unsigned long setStartLast = 0; //to control flashing during start +void cycleDisplay(byte displayDim, byte fnSetPg){ + unsigned long now = millis(); + //MAX7219 handles its own cycling - just needs display data updates. + //But we do need to check if the blink should be over, and whether dim has changed. + if(displayBlinkStart){ + if((unsigned long)(now-displayBlinkStart)>=500){ displayBlinkStart = 0; sendToMAX7219(0,5); } + } + //Other display code decides whether we should dim per function or time of day + char dim = displayDim; //2=normal, 1=dim, 0=off + //But if we're setting, decide here to dim for every other 500ms since we started setting + if(fnSetPg>0) { + if(setStartLast==0) setStartLast = now; + dim = (1-(((unsigned long)(now-setStartLast)/500)%2))+1; + } else { + if(setStartLast>0) setStartLast=0; + } + if(curBrightness!=(dim==2? BRIGHTNESS_FULL: (dim==1? BRIGHTNESS_DIM: -1))){ + curBrightness = (dim==2? BRIGHTNESS_FULL: (dim==1? BRIGHTNESS_DIM: -1)); + if(curBrightness==-1) for(int i=0; i 0015) + word place; + for(byte i=0; i<=posEnd-posStart; i++){ + switch(i){ //because int(pow(10,1))==10 but int(pow(10,2))==99... + case 0: place=1; break; + 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))); + } + sendToMAX7219(posStart,posEnd); + //cycleDisplay(); //fixes brightness - can we skip this? +} +void blankDisplay(byte posStart, byte posEnd, byte fade){ + for(byte i=posStart; i<=posEnd; i++) { displayNext[i]=15; } + sendToMAX7219(posStart,posEnd); + //cycleDisplay(); //fixes brightness - can we skip this? +} + +//void startScroll() {} + +void displayBlink(){ + for(int i=0; i +#include "arduino-clock.h" + +#ifdef DISP_NIXIE //see arduino-clock.ino Includes section + +#include "dispNixie.h" + +// Display cycling code derived from http://arduinix.com/Main/Code/ANX-6Tube-Clock-Crossfade.txt + +//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] +byte binOutA[4] = {OUT_A1,OUT_A2,OUT_A3,OUT_A4}; +byte binOutB[4] = {OUT_B1,OUT_B2,OUT_B3,OUT_B4}; +//3 pins out to anode channel switches +byte anodes[3] = {ANODE_1,ANODE_2,ANODE_3}; + +const int fadeDur = 5; //ms - each multiplexed pair of digits appears for this amount of time per cycle +const int dimDur = 4; //ms - portion of fadeDur that is left dark during dim times +int fadeNextDur = 0; //ms - during fade, incoming digit's portion of fadeDur +int fadeLastDur = 0; //ms - during fade, outgoing digit's portion of fadeDur +unsigned long fadeStartLast = 0; //millis - when the last digit fade was started +unsigned long setStartLast = 0; //to control flashing during start + +unsigned long displayBlinkStart = 0; //when nonzero, display should briefly blank + +byte cleanRemain = 0; //anti-cathode-poisoning clean timeout counter, increments at CLEAN_SPEED ms (see loop()). Start at 11 to run at clock startup //TODO nixie only + +int8_t scrollRemain = 0; //"frames" of scroll – signed byte - 0=not scrolling, >0=coming in, <0=going out, -128=scroll out at next change. + +byte displayNext[6] = {15,15,15,15,15,15}; //Internal representation of display. Blank to start. Change this to change display. +byte displayLast[6] = {11,11,11,11,11,11}; //for noticing changes to displayNext and fading the display to it +byte scrollDisplay[6] = {15,15,15,15,15,15}; //For animating a value into displayNext from right, and out to left + +unsigned long pollCleanLast = 0; //every CLEAN_SPEED ms +unsigned long pollScrollLast = 0; //every SCROLL_SPEED ms + +void decToBin(bool binVal[], byte i){ //"private" + //binVal is a reference (modify in place) of a binary number bool[4] with values [1,2,4,8] + if(i<0 || i>15) i=15; //default value, turns tubes off + binVal[3] = int(i/8)%2; + binVal[2] = int(i/4)%2; + binVal[1] = int(i/2)%2; + binVal[0] = i%2; +} //end decToBin() + +void setCathodes(byte decValA, byte decValB){ //"private" + bool binVal[4]; //4-bit binary number with values [1,2,4,8] + decToBin(binVal,decValA); //have binary value of decVal set into binVal + for(byte i=0; i<4; i++) digitalWrite(binOutA[i],binVal[i]); //set bin inputs of SN74141 + decToBin(binVal,decValB); + for(byte i=0; i<4; i++) digitalWrite(binOutB[i],binVal[i]); //set bin inputs of SN74141 +} //end setCathodes() + +void initDisplay(){ + for(byte i=0; i<4; i++) { pinMode(binOutA[i],OUTPUT); pinMode(binOutB[i],OUTPUT); } + for(byte i=0; i<3; i++) { pinMode(anodes[i],OUTPUT); } +} + +void cycleDisplay(byte displayDim, byte fnSetPg){ + unsigned long now = millis(); + + if(displayBlinkStart){ + if((unsigned long)(now-displayBlinkStart)<250){ delay(fadeDur*3); return; } + // The delay is to make cycleDisplay take up the same amount of loop time it usually does. Not sure if necessary. + else displayBlinkStart = 0; + } + + //Other display code decides whether we should dim per function or time of day + bool dim = (displayDim==1?1:0); + //But if we're setting, decide here to dim for every other 500ms since we started setting + if(fnSetPg>0) { + if(setStartLast==0) setStartLast = now; + dim = 1-(((unsigned long)(now-setStartLast)/500)%2); + } 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 + + if(/*readEEPROM(20,false)==0 ||*/ dim) { //fading disabled or dim + //TODO needs to become a global var + if(fadeStartLast) fadeStartLast = 0; //cancel any fade currently going - dim state doesn't have enough steps to fade well + for(byte i=0; i<6; i++) if(displayNext[i] != displayLast[i]) displayLast[i] = displayNext[i]; + } + else { //fading enabled + if(fadeStartLast==0) { //not fading - time to fade? + for(byte i=0; i<6; i++) if(displayNext[i] != displayLast[i]) { fadeStartLast = now; break; } + } + if(fadeStartLast!=0) { //currently fading + //let the next digit steal some display time from the last digit + //ex: if fade time (from EEPROM) is 20ms, and fadeDur (next+last) is 6ms: + // at 0ms, next = (( 0*(6-1))/20)+1 = 1; last = (6-nextDur) = 5; + // at 10ms, next = ((10*(6-1))/20)+1 = 3; last = (6-nextDur) = 3; ... + // at 20ms, next = ((20*(6-1))/20)+1 = 6; next = total, so fade is over! + //TODO facilitate longer fades by writing a tweening function that smooths the frames, i.e. 111121222 - or use delayMicroseconds as below + //TODO does this have more problems with the millis rollover issue? + fadeNextDur = (((unsigned long)(now-fadeStartLast)*(fadeDur-1))/(5*10))+1; //readEEPROM(20,false) TODO needs to become a global var + if(fadeNextDur >= fadeLastDur) { //fade is over + fadeStartLast = 0; + fadeNextDur = 0; + fadeLastDur = fadeDur; + for(byte j=0; j<6; j++) displayLast[j] = displayNext[j]; + } //end fade is over + else { //shorten last digit display duration by subtracting next display duration from it + fadeLastDur = fadeLastDur - fadeNextDur; + } + } //end curently fading + } //end fading enabled + + //TODO consider using delayMicroseconds() which, with its tighter resolution, may give better control over fades and dim levels + if(displayDim>0) { //if other display code says to shut off entirely, skip this part + //Anode channel 0: tubes #2 (min x10) and #5 (sec x1) + setCathodes(displayLast[2],displayLast[5]); //Via d2b decoder chip, set cathodes to old digits + digitalWrite(anodes[0], HIGH); //Turn on tubes + delay(fadeLastDur);//-(dim?dimDur:0)); //Display for fade-out cycles + setCathodes(displayNext[2],displayNext[5]); //Switch cathodes to new digits + delay(fadeNextDur);//-(dim?dimDur:0)); //Display for fade-in cycles + digitalWrite(anodes[0], LOW); //Turn off tubes + + if(dim) delay(dimDur); + + //Anode channel 1: tubes #4 (sec x10) and #1 (hour x1) + setCathodes(displayLast[4],displayLast[1]); + digitalWrite(anodes[1], HIGH); + delay(fadeLastDur); + setCathodes(displayNext[4],displayNext[1]); + delay(fadeNextDur); + digitalWrite(anodes[1], LOW); + + if(dim) delay(dimDur); + + //Anode channel 2: tubes #0 (hour x10) and #3 (min x1) + setCathodes(displayLast[0],displayLast[3]); + digitalWrite(anodes[2], HIGH); + delay(fadeLastDur); + setCathodes(displayNext[0],displayNext[3]); + delay(fadeNextDur); + digitalWrite(anodes[2], LOW); + + if(dim) delay(dimDur); + } //end if displayDim>0 + //TODO why does it sometimes flicker while in the setting mode +} + +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 digit + //If number has more places than posEnd-posStart, the higher places are truncated off (e.g. 10015 on 4-tube display --> 0015) + word place; + for(byte i=0; i<=posEnd-posStart; i++){ + switch(i){ //because int(pow(10,1))==10 but int(pow(10,2))==99... + case 0: place=1; break; + 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))); + if(!fade) displayLast[posEnd-i] = displayNext[posEnd-i]; //cycleDisplay will be none the wiser + } +} //end editDisplay() + +void blankDisplay(byte posStart, byte posEnd, byte fade){ + for(byte i=posStart; i<=posEnd; i++) { displayNext[i]=15; if(!fade) displayLast[i]=15; } +} //end blankDisplay(); + +// void startScroll() { //To scroll a value in, call this after calling editDisplay as normal +// for(byte i=0; i<6; i++) scrollDisplay[i] = displayNext[i]; //cache the incoming value in scrollDisplay[] +// blankDisplay(0,5,true); +// scrollRemain = DISPLAY_SIZE+1; //this will trigger updateDisplay() to start scrolling. DISPLAY_SIZE+1 adds blank frame at front +// } //end startScroll() + +void displayBlink(){ + displayBlinkStart = millis(); +} + +// void checkEffects(bool force){ +// //control the cleaning/scrolling effects - similar to checkRTC but it has its own timings +// unsigned long now = millis(); +// //If we're running a tube cleaning, advance it every CLEAN_SPEED ms. +// if(cleanRemain && (unsigned long)(now-pollCleanLast)>=CLEAN_SPEED) { //account for rollover +// pollCleanLast=now; +// cleanRemain--; +// if(cleanRemain<1) calcSun(rtcGetYear(),rtcGetMonth(),rtcGetDate()); //take this opportunity to perform a calculation that blanks the display for a bit +// updateDisplay(); +// } +// //If we're scrolling an animation, advance it every SCROLL_SPEED ms. +// else if(scrollRemain!=0 && scrollRemain!=-128 && ((unsigned long)(now-pollScrollLast)>=SCROLL_SPEED || force)) { +// pollScrollLast=now; +// if(scrollRemain<0) { +// scrollRemain++; updateDisplay(); +// } else { +// scrollRemain--; updateDisplay(); +// if(scrollRemain==0) scrollRemain = -128; +// } +// } +// } + +#endif //DISP_NIXIE \ No newline at end of file diff --git a/arduino-clock/dispNixie.h b/arduino-clock/dispNixie.h new file mode 100644 index 0000000..348d257 --- /dev/null +++ b/arduino-clock/dispNixie.h @@ -0,0 +1,16 @@ +#ifndef DISP_NIXIE_H +#define DISP_NIXIE_H + +//Mutually exclusive with other disp options + +void decToBin(bool binVal[], byte i); +void setCathodes(byte decValA, byte decValB); +void initDisplay(); +void cycleDisplay(byte displayDim, byte fnSetPg); +void editDisplay(word n, byte posStart, byte posEnd, bool leadingZeros, bool fade); +void blankDisplay(byte posStart, byte posEnd, byte fade); +// void startScroll(); +void displayBlink(); +// void checkEffects(bool force); + +#endif //DISP_NIXIE_H \ No newline at end of file diff --git a/arduino-clock/input.cpp b/arduino-clock/input.cpp new file mode 100644 index 0000000..4905145 --- /dev/null +++ b/arduino-clock/input.cpp @@ -0,0 +1,246 @@ +#include +#include "arduino-clock.h" + +#include "input.h" + +//Needs access to RTC timestamps +#include "rtcDS3231.h" +#include "rtcMillis.h" + +#define HOLDSET_SLOW_RATE 125 +#define HOLDSET_FAST_RATE 20 + +//#include "Arduino.h" //not necessary, since these get compiled as part of the main sketch +#ifdef INPUT_UPDN_ROTARY + #include //Paul Stoffregen - install in your Arduino IDE + Encoder rot(CTRL_R1,CTRL_R2); //TODO may need to reverse +#endif +#ifdef INPUT_IMU + #include + //If we don't already have inputs defined for Sel/Alt/Up/Dn, use some bogus ones + #ifndef CTRL_SEL + #define CTRL_SEL 100 + #endif + #ifndef CTRL_ALT + #define CTRL_ALT 101 + #endif + #ifndef CTRL_UP + #define CTRL_UP 102 + #endif + #ifndef CTRL_DN + #define CTRL_DN 103 + #endif + //IMU "debouncing" + int imuRoll = 0; //the state we're reporting (-1, 0, 1) + int imuRollLast = 0; //when we saw it change + int imuPitch = 0; //the state we're reporting (-1, 0, 1) + int imuPitchLast = 0; //when we saw it change + //int imuLastRead = 0; //for debug + void readIMU(){ + float x, y, z; + IMU.readAcceleration(x,y,z); + int imuState; + + //Assumes Arduino is oriented with components facing back of clock, and USB port facing up. TODO add support for other orientations + + //Roll + if((unsigned long)(millis()-imuRollLast)>=IMU_DEBOUNCING){ //don't check within a period from the last change + if(y<=-0.5) imuState = 1; + else if(y>= 0.5) imuState = -1; + else if(y>-0.3 && y<0.3) imuState = 0; + else imuState = imuRoll; //if it's borderline, treat it as "same" + if(imuRoll != imuState){ imuRoll = imuState; imuRollLast = millis(); } //TODO maybe add audible feedback + } + + //Pitch + if((unsigned long)(millis()-imuPitchLast)>=IMU_DEBOUNCING){ //don't check within a period from the last change + if(z<=-0.5) imuState = 1; + else if(z>= 0.5) imuState = -1; + else if(z>-0.3 && z<0.3) imuState = 0; + else imuState = imuPitch; //if it's borderline, treat it as "same" + if(imuPitch != imuState){ imuPitch = imuState; imuPitchLast = millis(); } + } + + } +#endif + +byte inputCur = 0; //Momentary button (or IMU position) currently in use - only one allowed at a time +byte inputCurHeld = 0; //Button hold thresholds: 0=none, 1=unused, 2=short, 3=long, 4=verylong, 5=superlong, 10=set by inputStop() + +unsigned long inputLast = 0; //When an input last took place, millis() +int inputLastTODMins = 0; //When an input last took place, time of day. Used in paginated functions so they all reflect the time of day when the input happened. + +bool initInputs(){ + //TODO are there no "loose" pins left floating after this? per https://electronics.stackexchange.com/q/37696/151805 + #ifdef INPUT_BUTTONS + pinMode(CTRL_SEL, INPUT_PULLUP); + if(CTRL_ALT>0){ //preprocessor directives don't seem to work for this when e.g. "A7" + pinMode(CTRL_ALT, INPUT_PULLUP); + } + #ifdef INPUT_UPDN_BUTTONS + pinMode(CTRL_UP, INPUT_PULLUP); + pinMode(CTRL_DN, INPUT_PULLUP); + #endif + #endif + #ifdef INPUT_UPDN_ROTARY + //rotary needs no init here + #endif + #ifdef INPUT_IMU + //if(!IMU.begin()){ Serial.println(F("Failed to initialize IMU!")); while(1); } + IMU.begin(); + //Serial.println(F("IMU initialized")); + #endif + //Check to see if CTRL_SEL is held at init - facilitates version number display and EEPROM hard init + delay(100); //prevents the below from firing in the event there's a capacitor stabilizing the input, which can read low falsely + if(readBtn(CTRL_SEL)){ inputCur = CTRL_SEL; return true; } + else return false; +} + +bool readBtn(byte btn){ + //Reads momentary button and/or IMU position, as equipped + //Returns true if one or both are "pressed" + bool btnPressed = false; + bool imuPressed = false; + #ifdef INPUT_BUTTONS + if(btn!=0){ //skip disabled alt + if(btn==A6 || btn==A7) btnPressed = analogRead(btn)<100; //analog-only pins + else btnPressed = !(digitalRead(btn)); //false (low) when pressed + } + #endif + #ifdef INPUT_IMU + switch(btn){ + //Assumes Arduino is oriented with components facing back of clock, and USB port facing up + //TODO support other orientations + case CTRL_SEL: imuPressed = imuPitch>0; break; //clock tilted dial up + case CTRL_ALT: imuPressed = imuPitch<0; break; //clock tilted dial down + case CTRL_DN: imuPressed = imuRoll<0; break; //clock tilted left + case CTRL_UP: imuPressed = imuRoll>0; break; //clock tilted right + default: break; + } + #endif + return (btnPressed || imuPressed); +} + +unsigned long holdLast; +void checkBtn(byte btn){ + //Polls for changes in momentary buttons (or IMU positioning), LOW = pressed. + //When a button event has occurred, will call ctrlEvt in main code. + //Only called by checkInputs() and only for inputs configured as button and/or IMU. + bool bnow = readBtn(btn); + unsigned long now = millis(); + //If the button has just been pressed, and no other buttons are in use... + if(inputCur==0 && bnow) { + // Serial.print(F("Btn ")); + // Serial.print(btn,DEC); + // Serial.println(F(" pressed")); + inputCur = btn; inputCurHeld = 0; inputLast = now; inputLastTODMins = rtcGetTOD(); + //Serial.println(); Serial.println(F("ich now 0 per press")); + ctrlEvt(btn,1,inputCurHeld); //hey, the button has been pressed + //Serial.print(F("ich now ")); Serial.print(inputCurHeld,DEC); Serial.println(F(" after press > ctrlEvt")); + } + //If the button is being held... + if(inputCur==btn && bnow) { + //If the button has passed a hold duration threshold... (ctrlEvt will only act on these for Sel/Alt) + if((unsigned long)(now-inputLast)>=CTRL_HOLD_SUPERLONG_DUR && inputCurHeld < 5){ + ctrlEvt(btn,5,inputCurHeld); if(inputCurHeld<10) inputCurHeld = 5; + //Serial.print(F("ich now ")); Serial.print(inputCurHeld,DEC); Serial.println(F(" after 5 hold > ctrlEvt")); + } + else if((unsigned long)(now-inputLast)>=CTRL_HOLD_VERYLONG_DUR && inputCurHeld < 4){ + ctrlEvt(btn,4,inputCurHeld); if(inputCurHeld<10) inputCurHeld = 4; + //Serial.print(F("ich now ")); Serial.print(inputCurHeld,DEC); Serial.println(F(" after 4 hold > ctrlEvt")); + } + else if((unsigned long)(now-inputLast)>=CTRL_HOLD_LONG_DUR && inputCurHeld < 3){ + ctrlEvt(btn,3,inputCurHeld); if(inputCurHeld<10) inputCurHeld = 3; + //Serial.print(F("ich now ")); Serial.print(inputCurHeld,DEC); Serial.println(F(" after 3 hold > ctrlEvt")); + } + else if((unsigned long)(now-inputLast)>=CTRL_HOLD_SHORT_DUR && inputCurHeld < 2) { + //Serial.print(F("ich was ")); Serial.println(inputCurHeld,DEC); + ctrlEvt(btn,2,inputCurHeld); if(inputCurHeld<10) inputCurHeld = 2; + //Serial.print(F("ich now ")); Serial.print(inputCurHeld,DEC); Serial.println(F(" after 2 hold > ctrlEvt")); + holdLast = now; //starts the repeated presses code going + } + //While Up/Dn are being held, send repeated presses to ctrlEvt + #if defined(INPUT_UPDN_BUTTONS) || defined(INPUT_IMU) + if((btn==CTRL_UP || btn==CTRL_DN) && inputCurHeld >= 2){ + if((unsigned long)(now-holdLast)>=(inputCurHeld>=3?HOLDSET_FAST_RATE:HOLDSET_SLOW_RATE)){ //could make it nonlinear? + holdLast = now; + ctrlEvt(btn,1,inputCurHeld); + } + } + #endif + } + //If the button has just been released... + if(inputCur==btn && !bnow) { + inputCur = 0; + //Only act if the button hasn't been stopped + if(inputCurHeld<10) ctrlEvt(btn,0,inputCurHeld); //hey, the button was released after inputCurHeld + //Serial.print(F("ich now ")); Serial.print(inputCurHeld,DEC); Serial.println(F(" then 0 after release > ctrlEvt")); + inputCurHeld = 0; + } +} +void inputStop(){ + //In some cases, when handling btn evt 1/2/3/4/5, we may call this so following events 2/3/4/5/0 won't cause unintended behavior (e.g. after a fn change, or going in or out of set) + inputCurHeld = 10; + //Serial.println(F("ich now 10 per inputStop")); +} + +bool rotVel = 0; //high velocity setting (x10 rather than x1) +#ifdef INPUT_UPDN_ROTARY +unsigned long rotLastStep = 0; //timestamp of last completed step (detent) +int rotLastVal = 0; +void checkRot(){ + //Changes in rotary encoder. When rotation(s) occur, will call ctrlEvt to simulate btn presses. During setting, ctrlEvt will take rotVel into account. + int rotCurVal = rot.read(); + if(rotCurVal!=rotLastVal){ //we've sensed a state change + rotLastVal = rotCurVal; + if(rotCurVal>=4 || rotCurVal<=-4){ //we've completed a step of 4 states (this library doesn't seem to drop states much, so this is reasonably reliable) + unsigned long now = millis(); + inputLast = now; inputLastTODMins = rtcGetTOD(); + if((unsigned long)(now-rotLastStep)<=ROT_VEL_START) rotVel = 1; //kick into high velocity setting (x10) + else if((unsigned long)(now-rotLastStep)>=ROT_VEL_STOP) rotVel = 0; //fall into low velocity setting (x1) + rotLastStep = now; + while(rotCurVal>=4) { rotCurVal-=4; ctrlEvt(CTRL_UP,1,inputCurHeld,rotVel); } + while(rotCurVal<=-4) { rotCurVal+=4; ctrlEvt(CTRL_DN,1,inputCurHeld,rotVel); } + rot.write(rotCurVal); + } + } +} //end checkRot() +#endif + +void checkInputs(){ + //TODO potential issue: if user only means to rotate or push encoder but does both? + #ifdef INPUT_IMU + readIMU(); //captures IMU state for checkBtn/readBtn to look at + #endif + //checkBtn calls readBtn which will read button and/or IMU as equipped + //We just need to only call checkBtn if one or the other is equipped + #if defined(INPUT_BUTTONS) || defined(INPUT_IMU) + checkBtn(CTRL_SEL); + if(CTRL_ALT>0){ //preprocessor directives don't seem to work for this when e.g. "A7" + checkBtn(CTRL_ALT); + } + #if defined(INPUT_UPDN_BUTTONS) || defined(INPUT_IMU) + checkBtn(CTRL_UP); + checkBtn(CTRL_DN); + #endif + #endif + #ifdef INPUT_UPDN_ROTARY + checkRot(); + #endif +} + +void setInputLast(unsigned long increment){ + //Called when other code changes the displayed fn, as though the human user had done it + //(which in many cases they did, but indirectly, such as via settings page - but also automatic date display) + //If increment is specified, we just want to move inputLast up a bit + if(increment) inputLast+=increment; + //else we want to set both inputLast and the TODMins snapshot to now + inputLast = millis(); inputLastTODMins = rtcGetTOD(); +} +unsigned long getInputLast(){ + return inputLast; +} +int getInputLastTODMins(){ + //Used to ensure paged displays (e.g. calendar) use the same TOD for all pages + return inputLastTODMins; +} \ No newline at end of file diff --git a/arduino-clock/input.h b/arduino-clock/input.h new file mode 100644 index 0000000..79e11a6 --- /dev/null +++ b/arduino-clock/input.h @@ -0,0 +1,35 @@ +#ifndef INPUT_H +#define INPUT_H + +#ifdef INPUT_IMU + //If we don't already have inputs defined for Sel/Alt/Up/Dn, use some bogus ones + #ifndef CTRL_SEL + #define CTRL_SEL 100 + #endif + #ifndef CTRL_ALT + #define CTRL_ALT 101 + #endif + #ifndef CTRL_UP + #define CTRL_UP 102 + #endif + #ifndef CTRL_DN + #define CTRL_DN 103 + #endif +#endif + +//#ifdef INPUT_IMU +void readIMU(); +//#endif +bool initInputs(); +bool readBtn(byte btn); +void checkBtn(byte btn); +void inputStop(); +//#ifdef INPUT_UPDN_ROTARY +void checkRot(); +//#endif +void checkInputs(); +void setInputLast(unsigned long increment=0); +unsigned long getInputLast(); +int getInputLastTODMins(); + +#endif //INPUT_H \ No newline at end of file diff --git a/arduino-clock/network.cpp b/arduino-clock/network.cpp new file mode 100644 index 0000000..692f75a --- /dev/null +++ b/arduino-clock/network.cpp @@ -0,0 +1,914 @@ +#include +#include "arduino-clock.h" + +#ifndef __AVR__ //TODO better sensor +//do stuff for wifinina + +#include "network.h" +#include +#include +//Needs to be able to control the display +#include "dispMAX7219.h" +#include "dispNixie.h" +//Needs to be able to control the RTC +#include "rtcDS3231.h" +#include "rtcMillis.h" +//Needs to be able to save to persistent storage +#include "storage.h" + +//Volatile vars that back up the wifi creds in EEPROM +// 55-86 Wi-Fi SSID (32 bytes) +// 87-150 Wi-Fi WPA passphrase/key or WEP key (64 bytes) +// 151 Wi-Fi WEP key index (1 byte) +//If the EEPROM ssid/pass are a full 32/64 chars, there won't be room for the termination character '\0', we'll add that when reading +//TODO consider making these char arrays - but the memory usage on SAMD isn't bad as-is +String wssid = ""; +String wpass = ""; //wpa pass or wep key +byte wki = 0; //wep key index - 0 if using wpa + +unsigned int localPort = 2390; // local port to listen for UDP packets +#define NTP_PACKET_SIZE 48 // NTP time stamp is in the first 48 bytes of the message +byte packetBuffer[ NTP_PACKET_SIZE]; //buffer to hold incoming and outgoing packets +WiFiUDP Udp; // A UDP instance to let us send and receive packets over UDP + +WiFiServer server(80); + +#define ADMIN_TIMEOUT 3600000 //120000; //two minutes +#define NTP_TIMEOUT 1000 //how long to wait for a request to finish - the longer it takes, the less reliable the result is +#define NTP_MINFREQ 5000 //how long to enforce a wait between request starts (NIST requires at least 4sec between requests or will ban the client) +#define NTPOK_THRESHOLD 3600000 //if no sync within 60 minutes, the time is considered stale + +bool networkSupported(){ return true; } + +void initNetwork(){ + //Serial.println(F("Hello world from network.cpp")); + //Check status of wifi module up front + //if(WiFi.status()==WL_NO_MODULE){ Serial.println(F("Communication with WiFi module failed!")); while(true); } + //else if(WiFi.firmwareVersion()86400000){ + ntpSyncLast = (unsigned long)(now-86400000); + if(!ntpSyncLast) ntpSyncLast = -1; //never let it be zero + } + return (unsigned long)(now-ntpSyncLast); +} + +void cueNTP(){ + // We don't want to let other code startNTP() directly since it's normally asynchronous, and that other code may delay the time until we can check the result. Exception is forced call from admin page, which calls startNTP() synchronously. + if(readEEPROM(9,false)) ntpCued = true; +} + +int startNTP(bool synchronous){ //Called at intervals to check for ntp time + //synchronous is for forced call from admin page, so we can return an error code, or 0 on successful sync + if(wssid==F("")) return -1; //don't try to connect if there's no creds + if(WiFi.status()!=WL_CONNECTED && WiFi.status()!=WL_AP_CONNECTED && WiFi.status()!=WL_AP_LISTENING) networkStartWiFi(); //in case the wifi dropped. Don't try if currently offering an access point. + if(WiFi.status()!=WL_CONNECTED) return -2; + if(ntpGoing || ntpTime) return -3; //if request going, or waiting to set to apply TODO epoch issue + if((unsigned long)(millis()-ntpStartLast) < NTP_MINFREQ) return -4; //if a previous request is going, do not start another until at least NTP_MINFREQ later + //Serial.print(millis(),DEC); Serial.println(F("NTP starting")); + ntpGoing = 1; + ntpStartLast = millis(); if(!ntpStartLast) ntpStartLast = -1; //never let it be zero + Udp.flush(); //in case of old data + //Udp.stop() was formerly here + //Serial.println(); Serial.print(millis()); Serial.println(F(" Sending UDP packet to NTP server.")); + memset(packetBuffer, 0, NTP_PACKET_SIZE); // set all bytes in the buffer to 0 + // Initialize values needed to form NTP request + packetBuffer[0] = 0b11100011; // LI, Version, Mode + packetBuffer[1] = 0; // Stratum, or type of clock + packetBuffer[2] = 6; // Polling Interval + packetBuffer[3] = 0xEC; // Peer Clock Precision + // 8 bytes of zero for Root Delay & Root Dispersion + packetBuffer[12] = 49; + packetBuffer[13] = 0x4E; + packetBuffer[14] = 49; + packetBuffer[15] = 52; + //Serial.println(F("time to read IP")); + //Serial.print(readEEPROM(51,false),DEC); Serial.print(F(".")); Serial.print(readEEPROM(52,false),DEC); Serial.print(F(".")); Serial.print(readEEPROM(53,false),DEC); Serial.print(F(".")); Serial.println(readEEPROM(54,false),DEC); + IPAddress timeServer(readEEPROM(51,false),readEEPROM(52,false),readEEPROM(53,false),readEEPROM(54,false)); + Udp.beginPacket(timeServer, 123); //NTP requests are to port 123 + Udp.write(packetBuffer, NTP_PACKET_SIZE); + Udp.endPacket(); + if(synchronous){ + bool success = false; + while(!success && (unsigned long)(millis()-ntpStartLast)=NTP_TIMEOUT) ntpGoing = 0; //time out + return false; + } + // We've received a packet, read the data from it + ntpSyncLast = millis(); if(!ntpSyncLast) ntpSyncLast = -1; //never let it be zero + unsigned int requestTime = ntpSyncLast-ntpStartLast; + Udp.read(packetBuffer, NTP_PACKET_SIZE); // read the packet into the buffer + + //https://forum.arduino.cc/index.php?topic=526792.0 + //epoch in earlier bits? needed after 2038 + //TODO leap second notification in earlier bits? + ntpTime = (packetBuffer[40] << 24) | (packetBuffer[41] << 16) | (packetBuffer[42] << 8) | packetBuffer[43]; + unsigned long ntpFrac = (packetBuffer[44] << 24) | (packetBuffer[45] << 16) | (packetBuffer[46] << 8) | packetBuffer[47]; + ntpMils = (int32_t)(((float)ntpFrac / UINT32_MAX) * 1000); + + //Account for the request time + ntpMils += requestTime/2; + if(ntpMils>=1000) { ntpMils -= 1000; ntpTime++; } + + // Serial.print(F("NTP time: ")); + // Serial.print(ntpTime,DEC); + // Serial.print(F(".")); + // Serial.print(ntpMils,DEC); + // Serial.print(F(" ±")); + // Serial.print(requestTime,DEC); + + //Unless the mils are bang on, we'll wait to set the clock until the next full second. + if(ntpMils>0) ntpTime++; + + // Serial.print(F(" - set to ")); + // Serial.print(ntpTime,DEC); + // if(ntpMils==0) Serial.print(F(" immediately")); + // else { Serial.print(F(" after ")); Serial.print(1000-ntpMils,DEC); } + // Serial.println(); + + Udp.flush(); //in case of extraneous(?) data + //Udp.stop() was formerly here + ntpGoing = 0; //next if{} block will handle this + } + if(!ntpGoing){ + //If we are waiting to start, do it (asynchronously) + if(ntpCued){ startNTP(false); ntpCued=false; return false; } + //If we are not waiting to set, do nothing + if(!ntpTime) return false; + //If we are waiting to set, but it's not time, wait for the next cycle + //but return true since we successfully got a time to set to + if(ntpMils!=0 && (unsigned long)(millis()-ntpSyncLast)<(1000-ntpMils)) return true; + //else it's time! + //Serial.print(millis(),DEC); Serial.println(F("NTP complete")); + + //Convert unix timestamp to UTC date/time + //TODO this assumes epoch 0, which is only good til 2038, I think! + ntpTime -= 3155673600; //from 1900 to 2000, assuming epoch 0 + unsigned long ntpPart = ntpTime; + int y = 2000; + while(1){ //iterate to find year + unsigned long yearSecs = daysInYear(y)*86400; + if(ntpPart > yearSecs){ + ntpPart-=yearSecs; y++; + } else break; + } + byte m = 1; + while(1){ //iterate to find month + unsigned long monthSecs = daysInMonth(y,m)*86400; + if(ntpPart > monthSecs){ + ntpPart-=monthSecs; m++; + } else break; + } + byte d = 1+(ntpPart/86400); ntpPart %= 86400; + int hm = ntpPart/60; //mins from midnight + byte s = ntpPart%60; + + //Take UTC date/time and apply standard offset + //which involves checking for date rollover + //eeprom loc 14 is UTC offset in quarter-hours plus 100 - range is 52 (-12h or -48qh, US Minor Outlying Islands) to 156 (+14h or +56qh, Kiribati) + int utcohm = (readEEPROM(14,false)-100)*15; //utc offset in mins from midnight + if(hm+utcohm<0){ //date rolls backward + hm = hm+utcohm+1440; //e.g. -1 to 1439 which is 23:59 + d--; if(d<1){ m--; if(m<1){ y--; m=12; } d=daysInMonth(y,m); } //month or year rolls backward + } else if(hm+utcohm>1439){ //date rolls forward + hm = (hm+utcohm)%1440; //e.g. 1441 to 1 which is 00:01 + d++; if(d>daysInMonth(y,m)){ m++; if(m>12){ y++; m=1; } d=1; } //month or year rolls forward + } else hm += utcohm; + + //then check DST at that time (setting DST flag), and add an hour if necessary + //which involves checking for date rollover again (forward only) + //TODO this may behave unpredictably from 1–2am on fallback day since that occurs twice - check to see whether it has been applied already per the difference from utc + if(isDSTByHour(y,m,d,hm/60,true)){ + if(hm+60>1439){ //date rolls forward + hm = (hm+60)%1440; //e.g. 1441 to 1 which is 00:01 + d++; if(d>daysInMonth(y,m)){ m++; if(m>12){ y++; m=1; } d=1; } //month or year rolls forward + } else hm += 60; + } + + //finally set the rtc + rtcSetDate(y, m, d, dayOfWeek(y,m,d)); + rtcSetTime(hm/60,hm%60,s); + calcSun(); + + // Serial.print(F("RTC set to ")); + // Serial.print(rtcGetYear(),DEC); Serial.print(F("-")); + // if(rtcGetMonth()<10) Serial.print(F("0")); Serial.print(rtcGetMonth(),DEC); Serial.print(F("-")); + // if(rtcGetDate()<10) Serial.print(F("0")); Serial.print(rtcGetDate(),DEC); Serial.print(F(" ")); + // if(rtcGetHour()<10) Serial.print(F("0")); Serial.print(rtcGetHour(),DEC); Serial.print(F(":")); + // if(rtcGetMinute()<10) Serial.print(F("0")); Serial.print(rtcGetMinute(),DEC); Serial.print(F(":")); + // if(rtcGetSecond()<10) Serial.print(F("0")); Serial.print(rtcGetSecond(),DEC); + // Serial.println(); + + ntpTime = 0; ntpMils = 0; //no longer waiting to set + updateDisplay(); + return true; //successfully got a time and set to it + } +} //end fn checkNTP + +void clearNTPSyncLast(){ + //called when other code divorces displayed time from NTP sync + ntpSyncLast = 0; +} + +unsigned long adminInputLast = 0; //for noticing when the admin page hasn't been interacted with in 2 minutes, so we can time it (and AP if applicable) out + +void networkStartAdmin(){ + adminInputLast = millis(); + if(WiFi.status()!=WL_CONNECTED){ + networkStartAP(); + tempDisplay(7777); //display to user + //Serial.println(F("Admin started at 7.7.7.7")); + } else { //use existing wifi + IPAddress theip = WiFi.localIP(); + tempDisplay(theip[0],theip[1],theip[2],theip[3]); //display to user + //Serial.print(F("Admin started at ")); + //Serial.println(theip); + } + updateDisplay(); +} +void networkStopAdmin(){ + //Serial.println(F("stopping admin")); + adminInputLast = 0; //TODO use a different flag from adminInputLast + if(WiFi.status()!=WL_CONNECTED) networkStartWiFi(); +} + +//unsigned long debugLast = 0; +void checkClients(){ + // if((unsigned long)(millis()-debugLast)>=1000) { debugLast = millis(); + // Serial.print("Hello "); + // Serial.println(WiFi.status()); + // } + //if(WiFi.status()!=WL_CONNECTED && WiFi.status()!=WL_AP_CONNECTED) return; + if(adminInputLast && (unsigned long)(millis()-adminInputLast)>=ADMIN_TIMEOUT) networkStopAdmin(); + WiFiClient client = server.available(); + if(client) { + if(adminInputLast==0) { + client.flush(); client.stop(); + //Serial.print(F("Got a client but ditched it because last admin input was over ")); Serial.print(ADMIN_TIMEOUT); Serial.println(F("ms ago.")); + return; + } + else { + //Serial.print(F("Last admin input was ")); Serial.print(millis()-adminInputLast); Serial.print(F("ms ago which is under the limit of ")); Serial.print(ADMIN_TIMEOUT); Serial.println(F("ms.")); + } + + adminInputLast = millis(); + + String currentLine = ""; //we'll read the data from the client one line at a time + int requestType = 0; + bool newlineSeen = false; + + if(client.connected()){ + while(client.available()){ //if there's bytes to read from the client + char c = client.read(); + //Serial.write(c); //DEBUG + + if(c=='\n') newlineSeen = true; + else { + if(newlineSeen){ currentLine = ""; newlineSeen = false; } //if we see a newline and then something else: clear current line + currentLine += c; + } + + //Find the request type and path from the first line. + if(!requestType){ + if(currentLine=="GET / ") { requestType = 1; break; } //Read no more. We'll render out the page. + if(currentLine=="POST / ") requestType = 2; //We'll keep reading til the last line. + if(c=='\n') break; //End of first line without matching the above: invalid request, return nothing. + } + + } //end whie client available + } //end if client connected + + if(requestType){ + // HTTP headers always start with a response code (e.g. HTTP/1.1 200 OK) + // and a content-type so the client knows what's coming, then a blank line: + client.println("HTTP/1.1 200 OK"); + client.println("Content-type:text/html"); + client.println("Access-Control-Allow-Origin:*"); + client.println(); + if(requestType==1){ //get + + //Re hiding irrelevant settings/options, see also fnOptScroll in main code + + client.print(F("Clock Settings

Clock Settings

Loading…

If page doesn't appear in a few seconds, refresh.

")); + //client.print(F("")); + } //end get + else { //requestType==2 - handle what was POSTed + bool clientReturn = false; //Mark true when sending an error. If none, "ok" is sent at end. If nothing sent (crash), client displays generic error. + //client.print(currentLine); + //syncfreq=hr + //syncfreq=min + if(currentLine.startsWith(F("wssid="))){ //wifi change + //e.g. wssid=Network Name&wpass=qwertyuiop&wki=1 + //TODO since the values are not html-entitied (due to the difficulty of de-entiting here), this will fail if the ssid contains "&wpass=" or pass contains "&wki=" + int startPos = 6; + int endPos = currentLine.indexOf(F("&wpass="),startPos); + wssid = currentLine.substring(startPos,endPos); + startPos = endPos+7; + endPos = currentLine.indexOf(F("&wki="),startPos); + wpass = currentLine.substring(startPos,endPos); + startPos = endPos+5; + wki = currentLine.substring(startPos).toInt(); + //Persistent storage - see wssid/wpass definitions above + for(byte i=0; i<97; i++) writeEEPROM(55+i,0,false,false); //Clear out the old values (32+64+1) + for(byte i=0; i-1? currentLine.substring(startPos,endPos).toInt(): currentLine.substring(startPos).toInt()); + ntpip[i] = octet; startPos = endPos+1; + if(ntpip[i]!=octet){ parseOK = false; break; } + } + if(!parseOK) { clientReturn = true; client.print(F("Error: invalid format")); } + else for(byte i=0; i<4; i++) writeEEPROM(51+i,ntpip[i],false); + //Serial.print(F("IP should be ")); Serial.print(ntpip[0],DEC); Serial.print(F(".")); Serial.print(ntpip[1],DEC); Serial.print(F(".")); Serial.print(ntpip[2],DEC); Serial.print(F(".")); Serial.println(ntpip[3],DEC); + //Serial.print(F("IP saved as ")); Serial.print(readEEPROM(51,false),DEC); Serial.print(F(".")); Serial.print(readEEPROM(52,false),DEC); Serial.print(F(".")); Serial.print(readEEPROM(53,false),DEC); Serial.print(F(".")); Serial.println(readEEPROM(54,false),DEC); + } else if(currentLine.startsWith(F("syncnow"))){ + //TODO this doesn't seem to return properly if the wifi was changed after the clock was booted - it syncs, but just hangs + int ntpCode = startNTP(true); + switch(ntpCode){ + case -1: client.print(F("Error: no Wi-Fi credentials.")); break; + case -2: client.print(F("Error: not connected to Wi-Fi.")); break; + case -3: client.print(F("Error: NTP response pending. Please try again shortly.")); break; //should never see this one on the web since it's synchronous and the client blocks + case -4: client.print(F("Error: too many sync requests in the last ")); client.print(NTP_MINFREQ/1000,DEC); client.print(F(" seconds. Please try again shortly.")); break; + case -5: client.print(F("Error: no NTP response received. Please confirm server.")); break; + case 0: client.print(F("synced")); break; + default: client.print(F("Error: unhandled NTP code")); break; + } + clientReturn = true; + } else if(currentLine.startsWith(F("curtod"))){ + int tod = currentLine.substring(7).toInt(); + rtcSetTime(tod/60,tod%60,0); + ntpSyncLast = 0; + goToFn(FN_TOD); + } else if(currentLine.startsWith(F("curdatey"))){ + rtcSetDate(currentLine.substring(9).toInt(), rtcGetMonth(), rtcGetDate(), dayOfWeek(currentLine.substring(9).toInt(), rtcGetMonth(), rtcGetDate())); //TODO what about month exceed + ntpSyncLast = 0; + goToFn(FN_CAL,254); + } else if(currentLine.startsWith(F("curdatem"))){ + rtcSetDate(rtcGetYear(), currentLine.substring(9).toInt(), rtcGetDate(), dayOfWeek(rtcGetYear(), currentLine.substring(9).toInt(), rtcGetDate())); //TODO what about month exceed + goToFn(FN_CAL,254); + } else if(currentLine.startsWith(F("curdated"))){ + rtcSetDate(rtcGetYear(), rtcGetMonth(), currentLine.substring(9).toInt(), dayOfWeek(rtcGetYear(), rtcGetMonth(), currentLine.substring(9).toInt())); //TODO what about month exceed + goToFn(FN_CAL,254); + } else if(currentLine.startsWith(F("almtime"))){ + writeEEPROM(0,currentLine.substring(7).toInt(),true); + goToFn(FN_ALARM); + } else if(currentLine.startsWith(F("alm"))){ //two settings (alarm on, alarm skip) with one control. Compare to switchAlarmState() + setAlarmState(currentLine.substring(4).toInt()); + goToFn(FN_ALARM); + } else if(currentLine.startsWith(F("runout"))){ + char runout = currentLine.substring(7).toInt(); + setTimerState(3,runout/2); //chrono bit + setTimerState(2,runout%2); //restart bit + } else if(currentLine.startsWith(F("nighttod"))){ + writeEEPROM(28,currentLine.substring(9).toInt(),true); + } else if(currentLine.startsWith(F("morntod"))){ + writeEEPROM(30,currentLine.substring(8).toInt(),true); + } else if(currentLine.startsWith(F("worktod"))){ + writeEEPROM(35,currentLine.substring(8).toInt(),true); + } else if(currentLine.startsWith(F("hometod"))){ + writeEEPROM(37,currentLine.substring(8).toInt(),true); + } else { + //standard eeprom saves by type/loc + bool isInt = currentLine.startsWith(F("i")); //or b for byte + int eqPos = currentLine.indexOf(F("=")); + int key = currentLine.substring(1,eqPos).toInt(); + int val = currentLine.substring(eqPos+1).toInt(); + writeEEPROM(key,val,isInt); + //do special stuff for some of them + switch(key){ + case 4: case 5: case 6: //day counter + //in lieu of actually switching to FN_CAL, so that only this value is seen - compare to ino + if(readEEPROM(4,false)) tempDisplay(dateComp(rtcGetYear(),rtcGetMonth(),rtcGetDate(), readEEPROM(5,false),readEEPROM(6,false),readEEPROM(4,false)-1)); + findFnAndPageNumbers(); //to include or exclude the day counter from the calendar function + break; + case 14: //utc offset + cueNTP(); break; + case 17: //date format + goToFn(FN_CAL,254); break; + case 22: //auto dst + isDSTByHour(rtcGetYear(),rtcGetMonth(),rtcGetDate(),rtcGetHour(),true); break; + case 39: case 47: //alarm pitch/pattern + goToFn(FN_ALARM); break; + case 40: case 48: //timer pitch/pattern + goToFn(FN_TIMER); break; + case 41: case 49: //strike pitch/pattern + goToFn(FN_TOD); break; + default: break; + } + if(key==39 || key==40 || key==41){ //play beeper pitch sample - compare to updateDisplay() + quickBeep(val); + } + if(key==47 || key==48 || key==49){ //play beeper pattern sample - compare to updateDisplay() + quickBeepPattern((key==49?FN_TOD:(key==48?FN_TIMER:FN_ALARM)),val); + } + } + updateDisplay(); + if(!clientReturn) client.print(F("ok")); + } //end post + } //end if requestType + + client.stop(); + //Serial.println(""); + //Serial.println("client disconnected"); + delay(500); //for client to get the message TODO why is this necessary + + if(requestType==3) { //wifi was changed - restart the admin + networkStartWiFi(); //try to connect to wifi with new settings + networkStartAdmin(); //will set up AP if wifi isn't connected + } + } +} + +#else + +bool networkSupported(){ return false; } + +#endif //__AVR__ (network supported) \ No newline at end of file diff --git a/arduino-clock/network.h b/arduino-clock/network.h new file mode 100644 index 0000000..b193aad --- /dev/null +++ b/arduino-clock/network.h @@ -0,0 +1,20 @@ +#ifndef NETWORK_H +#define NETWORK_H + +bool networkSupported(); +void checkForWiFiStatusChange(); +void networkStartWiFi(); +void networkStartAP(); +void networkDisconnectWiFi(); +unsigned long ntpSyncAgo(); +void cueNTP(); +int startNTP(bool synchronous); +bool checkNTP(); +void clearNTPSyncLast(); +void networkStartAdmin(); +void networkStopAdmin(); +void checkClients(); +void initNetwork(); +void cycleNetwork(); + +#endif //NETWORK_H \ No newline at end of file diff --git a/arduino-clock/rtcDS3231.cpp b/arduino-clock/rtcDS3231.cpp new file mode 100644 index 0000000..11fcd93 --- /dev/null +++ b/arduino-clock/rtcDS3231.cpp @@ -0,0 +1,51 @@ +#include +#include "arduino-clock.h" + +#ifdef RTC_DS3231 //see arduino-clock.ino Includes section + +#include "rtcDS3231.h" +#include //Arduino - GNU LPGL - for I2C access to DS3231 +#include //NorthernWidget - The Unlicense - install in your Arduino IDE + +//RTC objects +DS3231 ds3231; //an object to access the ds3231 specifically (temp, etc) +RTClib rtc; //an object to access a snapshot of the ds3231 via rtc.now() +DateTime tod; //stores the rtc.now() snapshot for several functions to use +byte todW; //stores the day of week (read separately from ds3231 dow counter) + +void rtcInit(){ + Wire.begin(); +} +void rtcSetTime(byte h, byte m, byte s){ + ds3231.setHour(h); + ds3231.setMinute(m); + ds3231.setSecond(s); + millisReset(); +} +void rtcSetDate(int y, byte m, byte d, byte w){ + ds3231.setYear(y%100); //TODO: should 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(m); + ds3231.setDate(d); + ds3231.setDoW(w+1); //ds3231 weekday is 1-index +} +void rtcSetHour(byte h){ //used for DST forward/backward + ds3231.setHour(h); +} + +void rtcTakeSnap(){ + //rtcGet functions pull from this snapshot - to ensure that code works off the same timestamp + tod = rtc.now(); + todW = ds3231.getDoW()-1; //ds3231 weekday is 1-index +} +int rtcGetYear(){ return tod.year(); } +byte rtcGetMonth(){ return tod.month(); } +byte rtcGetDate(){ return tod.day(); } +byte rtcGetWeekday(){ return todW; } +int rtcGetTOD(){ return tod.hour()*60+tod.minute(); } +byte rtcGetHour(){ return tod.hour(); } +byte rtcGetMinute(){ return tod.minute(); } +byte rtcGetSecond(){ return tod.second(); } + +byte rtcGetTemp(){ return ds3231.getTemperature()*100; } + +#endif //RTC_DS3231 \ No newline at end of file diff --git a/arduino-clock/rtcDS3231.h b/arduino-clock/rtcDS3231.h new file mode 100644 index 0000000..85f842d --- /dev/null +++ b/arduino-clock/rtcDS3231.h @@ -0,0 +1,24 @@ +#ifndef RTC_DS3231_H +#define RTC_DS3231_H + +//Mutually exclusive with other rtc options + +void rtcInit(); +void rtcSetTime(byte h, byte m, byte s); +void rtcSetDate(int y, byte m, byte d, byte w); +void rtcSetHour(byte h); + +void rtcTakeSnap(); + +int rtcGetYear(); +byte rtcGetMonth(); +byte rtcGetDate(); +byte rtcGetWeekday(); +int rtcGetTOD(); +byte rtcGetHour(); +byte rtcGetMinute(); +byte rtcGetSecond(); + +byte rtcGetTemp(); + +#endif \ No newline at end of file diff --git a/arduino-clock/rtcMillis.cpp b/arduino-clock/rtcMillis.cpp new file mode 100644 index 0000000..0f3363e --- /dev/null +++ b/arduino-clock/rtcMillis.cpp @@ -0,0 +1,67 @@ +#include +#include "arduino-clock.h" + +#ifdef RTC_MILLIS //see arduino-clock.ino Includes section + +#include "rtcMillis.h" + +////////// FAKE RTC using millis ////////// + +//snapshot of time of day +unsigned long todMils = 0; //time of day in milliseconds +int todY = 2020; +byte todM = 1; +byte todD = 1; +byte todW = 0; +unsigned long millisAtTOD = 0; //reflects millis at snapshot + +void rtcInit(){} +void rtcSetTime(byte h, byte m, byte s){ + todMils = (h*3600000)+(m*60000)+(s*1000); + millisAtTOD = millis(); + millisReset(); +} +void rtcSetDate(int y, byte m, byte d, byte w){ + todY = y; todM = m; todD = d; todW = w; +} +void rtcSetHour(byte h){ + todMils = (h*3600000)+(todMils%60000); + millisAtTOD = millis(); +} + +void rtcTakeSnap(){ + unsigned long millisNow = millis(); + //Increment todMils per the change in millis + todMils += millisNow-millisAtTOD; + + //Apply anti-drift + unsigned long drift = (abs(ANTI_DRIFT) * (millisNow-millisAtTOD))/1000; + if(ANTI_DRIFT>0) todMils += drift; + if(ANTI_DRIFT<0) todMils -= drift; + //any issues with data types/truncation here? + + //Update the millis snap + millisAtTOD = millisNow; + //Handle midnight rollover + //This may behave erratically if rtcTakeSnap() is not called for long enough that todMils rolls over, + //but this should not happen, because except for short hangs (eg wifi connect) we should be calling it at least 1/sec + if(todMils >= 86400000){ + while(todMils >= 86400000) todMils = todMils - 86400000; //while is just to ensure it's below 86400000 + if(todD==daysInMonth(todY,todM)){ todD = 1; todM++; if(todM==13){ todM==1; todY++; } } + else todD++; + todW++; if(todW>6) todW=0; + } +} + +int rtcGetYear(){ return todY; } +byte rtcGetMonth(){ return todM; } +byte rtcGetDate(){ return todD; } +byte rtcGetWeekday(){ return todW; } +int rtcGetTOD(){ return (todMils/1000)/60; } +byte rtcGetHour(){ return (todMils/1000)/3600; } +byte rtcGetMinute(){ return ((todMils/1000)/60)%60; } +byte rtcGetSecond(){ return (todMils/1000)%60; } + +byte rtcGetTemp(){ return 1000; } //a fake response - ten degrees (1000 hundredths) forever + +#endif //RTC_MILLIS \ No newline at end of file diff --git a/arduino-clock/rtcMillis.h b/arduino-clock/rtcMillis.h new file mode 100644 index 0000000..169cbe5 --- /dev/null +++ b/arduino-clock/rtcMillis.h @@ -0,0 +1,24 @@ +#ifndef RTC_MILLIS_H +#define RTC_MILLIS_H + +//Mutually exclusive with other rtc options + +void rtcInit(); +void rtcSetTime(byte h, byte m, byte s); +void rtcSetDate(int y, byte m, byte d, byte w); +void rtcSetHour(byte h); + +void rtcTakeSnap(); + +int rtcGetYear(); +byte rtcGetMonth(); +byte rtcGetDate(); +byte rtcGetWeekday(); +int rtcGetTOD(); +byte rtcGetHour(); +byte rtcGetMinute(); +byte rtcGetSecond(); + +byte rtcGetTemp(); + +#endif \ No newline at end of file diff --git a/arduino-clock/storage.cpp b/arduino-clock/storage.cpp new file mode 100644 index 0000000..d5642fe --- /dev/null +++ b/arduino-clock/storage.cpp @@ -0,0 +1,84 @@ +// Persistent storage + +// This project was originally written to use the AVR Arduino's EEPROM for persistent storage, and would frequently read it directly during runtime (not just at startup). I wanted to abstract that away, partly to add support for SAMD flash memory, and partly to protect against runtime errors due to EEPROM/flash failure. +// This code serves those values out of a volatile array of bytes, which are backed by EEPROM/flash for the sole purpose of recovery after a power failure. It reads them from EEPROM/flash at startup, and sets them when changed. +// Flash support is via cmaglie's FlashStorage library. It offers an EEPROM emulation mode, which I'm currently using, but I don't like that it writes the entire "EEPROM" data to flash (not just the value being updated) with every commit() – I think it will wear out unnecessarily. TODO address this. +// Note that flash data is necessarily wiped out when the sketch is (re)uploaded. + +#include +#include "arduino-clock.h" + +#include "storage.h" + +#ifdef __AVR__ + #include //Arduino - GNU LPGL +#else //SAMD - is there a better way to detect EEPROM availability? TODO + #define FLASH_AS_EEPROM + //cmaglie's FlashStorage library - https://github.com/cmaglie/FlashStorage/ + #include //EEPROM mode + //#include //regular mode +#endif + +#define STORAGE_SPACE 152 //number of bytes +byte storageBytes[STORAGE_SPACE]; //the volatile array of bytes +#define COMMIT_TO_EEPROM 1 //1 for production + +void initStorage(){ + //If this is SAMD, write starting values if unused + #ifdef FLASH_AS_EEPROM + //Serial.println(F("Hello world from storage.cpp")); + //Serial.print(F("valid=")); Serial.println(EEPROM.isValid(),DEC); + //Serial.print(F("16=")); Serial.println(EEPROM.read(16)); + if(!EEPROM.isValid() || EEPROM.read(16)==0 || EEPROM.read(16)==255){ //invalid eeprom, wipe it out + for(byte i=0; i