Digital clock with Arduino Pro Mini

main
Joshua R. T. Purba 2021-12-02 00:16:50 +07:00
commit 4f20066d2a
10 changed files with 884 additions and 0 deletions

5
.gitignore vendored Normal file
View File

@ -0,0 +1,5 @@
.pio
.vscode/.browse.c_cpp.db*
.vscode/c_cpp_properties.json
.vscode/launch.json
.vscode/ipch

7
.vscode/extensions.json vendored Normal file
View File

@ -0,0 +1,7 @@
{
// See http://go.microsoft.com/fwlink/?LinkId=827846
// for the documentation about the extensions.json format
"recommendations": [
"platformio.platformio-ide"
]
}

21
LICENSE Normal file
View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2021 Joshua Rocky Tuahta Purba
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

30
README.md Normal file
View File

@ -0,0 +1,30 @@
# Digital Clock
Arduino based digital clock.
To be able to compile this, you need to modify the [LcdMenu](https://github.com/forntoh/LcdMenu/tree/2.1.0) library. Edit the [LcdMenu.h](https://github.com/forntoh/LcdMenu/blob/1a498223515e81810ba711f54d4a8edbb16b8def/src/LcdMenu.h#L767) from the LcdMenu library, find this:
```cpp
/**
* Toggle backlight
*/
void toggleBacklight() {
MenuItem* item = &currentMenuTable[cursorPosition];
if (item->getType() == MENU_ITEM_TOGGLE) {
lcd->setBacklight(item->isOn);
}
}
```
Replace it to this:
```cpp
/**
* Toggle backlight
*/
void toggleBacklight() {
#ifndef USE_STANDARD_LCD
MenuItem* item = &currentMenuTable[cursorPosition];
if (item->getType() == MENU_ITEM_TOGGLE) {
lcd->setBacklight(item->isOn);
}
#endif
}
```

39
include/README Normal file
View File

@ -0,0 +1,39 @@
This directory is intended for project header files.
A header file is a file containing C declarations and macro definitions
to be shared between several project source files. You request the use of a
header file in your project source file (C, C++, etc) located in `src` folder
by including it, with the C preprocessing directive `#include'.
```src/main.c
#include "header.h"
int main (void)
{
...
}
```
Including a header file produces the same results as copying the header file
into each source file that needs it. Such copying would be time-consuming
and error-prone. With a header file, the related declarations appear
in only one place. If they need to be changed, they can be changed in one
place, and programs that include the header file will automatically use the
new version when next recompiled. The header file eliminates the labor of
finding and changing all the copies as well as the risk that a failure to
find one copy will result in inconsistencies within a program.
In C, the usual convention is to give header files names that end with `.h'.
It is most portable to use only letters, digits, dashes, and underscores in
header file names, and at most one dot.
Read more about using header files in official GCC documentation:
* Include Syntax
* Include Operation
* Once-Only Headers
* Computed Includes
https://gcc.gnu.org/onlinedocs/cpp/Header-Files.html

46
lib/README Normal file
View File

@ -0,0 +1,46 @@
This directory is intended for project specific (private) libraries.
PlatformIO will compile them to static libraries and link into executable file.
The source code of each library should be placed in a an own separate directory
("lib/your_library_name/[here are source files]").
For example, see a structure of the following two libraries `Foo` and `Bar`:
|--lib
| |
| |--Bar
| | |--docs
| | |--examples
| | |--src
| | |- Bar.c
| | |- Bar.h
| | |- library.json (optional, custom build options, etc) https://docs.platformio.org/page/librarymanager/config.html
| |
| |--Foo
| | |- Foo.c
| | |- Foo.h
| |
| |- README --> THIS FILE
|
|- platformio.ini
|--src
|- main.c
and a contents of `src/main.c`:
```
#include <Foo.h>
#include <Bar.h>
int main (void)
{
...
}
```
PlatformIO Library Dependency Finder will find automatically dependent
libraries scanning project source files.
More information about PlatformIO Library Dependency Finder
- https://docs.platformio.org/page/librarymanager/ldf.html

23
platformio.ini Normal file
View File

@ -0,0 +1,23 @@
; PlatformIO Project Configuration File
;
; Build options: build flags, source filter
; Upload options: custom upload port, speed and extra flags
; Library options: dependencies, extra library storages
; Advanced options: extra scripting
;
; Please visit documentation for the other options and examples
; https://docs.platformio.org/page/projectconf.html
[env:pro16MHzatmega328]
platform = atmelavr
board = pro16MHzatmega328
framework = arduino
monitor_speed = 115200
lib_deps =
arduino-libraries/LiquidCrystal @ ^1.0.7
forntoh/LcdMenu @ ^2.1.0
makuna/RTC @ ^2.3.5
adafruit/DHT sensor library @ ^1.4.3
adafruit/Adafruit Unified Sensor @ ^1.1.4
teckel12/toneAC @ ^1.5.0
rlogiacco/Analog Buttons @ ^1.2.1

30
src/DebugUtil.h Normal file
View File

@ -0,0 +1,30 @@
/*
DebugUtils.h - Simple debugging utilities.
https://forum.arduino.cc/t/serial-debug-macro/64259/3
Posted: 2015-05-08 10:48 PM, by unknown author.
With modification by Joshua Purba.
*/
#ifndef DEBUGUTILS_H
#define DEBUGUTILS_H
#ifdef DEBUG
#ifndef DEBUG_PRINT
#define DEBUG_PRINT(...) Serial.print(__VA_ARGS__)
#endif
#ifndef DEBUG_PRINTLN
#define DEBUG_PRINTLN(...) Serial.println(__VA_ARGS__)
#endif
#else
#ifndef DEBUG_PRINT
#define DEBUG_PRINT(...)
#endif
#ifndef DEBUG_PRINTLN
#define DEBUG_PRINTLN(...)
#endif
#ifndef UNUSED
#define UNUSED(v) (void) (v)
#endif
#endif
#endif

672
src/main.cpp Normal file
View File

@ -0,0 +1,672 @@
//#define DEBUG /* Uncomment this line to enable Serial debug output. */
#include <AnalogButtons.h>
#define USE_STANDARD_LCD
#include <LcdMenu.h>
#include <ThreeWire.h>
#include <RtcDS1302.h>
#ifdef DEBUG
#define DHT_DEBUG
#endif
#include <DHT.h>
#include <toneAC.h> /* This means only pin 9 and 10 can be used by the buzzer. */
#include "DebugUtil.h"
/* Command key for each button. This signifies which button is currently pressed. */
#define CMD_IGNORE 0 /* This means no button is pressed. */
#define CMD_ENTER 1 /* Enter button is pressed. */
#define CMD_BACK 2 /* Back button is pressed. */
#define CMD_UP 3 /* Up button is pressed. */
#define CMD_DOWN 4 /* Down button is pressed. */
#define CMD_LEFT 5 /* Left button is pressed. */
#define CMD_RIGHT 6 /* Right button is pressed. */
#define SCREEN_CLOCK 0 /* Currently showing clock. */
#define SCREEN_MENU 1 /* Currently in the menu system. */
#define SCREEN_SET_TIME 2 /* Currently executing menu item "Set Time". */
#define SCREEN_SET_ALARM_TIME 3 /* Currently executing menu item "Set Alarm Time". */
#define SCREEN_TRANSITION 250 /* Screen transition delay in millisecond. */
/* Edit positions. Used in SCREEN_SET_TIME and SCREEN_SET_ALARM_TIME. */
#define EDIT_YEAR 0 /* Editing year. */
#define EDIT_MONTH 1 /* Editing month. */
#define EDIT_DAY 2 /* Editing day of month.*/
#define EDIT_HOUR 3 /* Editing hour. */
#define EDIT_MINUTE 4 /* Editing minute. */
#define EDIT_SECOND 5 /* Editing second. */
void cbBtnEnter();
void cbBtnLeft();
void cbBtnUp();
void cbBtnDown();
void cbBtnRight();
void cbBtnBack();
void cbBtnNone();
void cbMenuSetTime();
void cbMenuToggleAlarm(uint8_t);
void cbMenuSetAlarm();
void cbMenuSetAlarmDuration(uint8_t);
void screenClock();
void screenMenu();
void screenSetTime();
void screenSetAlarmTime();
void alarm();
void resetAlarmRun();
void showClock(const RtcDateTime&, uint8_t, uint8_t, uint8_t, uint8_t);
void showDHT();
void showSetAlarmTime(uint8_t, uint8_t);
#define MAD_ALARM_ENABLE (uint8_t) 0 /* RTC memory address for "Alarm Enable/Disable" flag. */
#define MAD_ALARM_HOUR (uint8_t) 1 /* RTC memory address for "Alarm Time (Hour)". */
#define MAD_ALARM_MINUTE (uint8_t) 2 /* RTC memory address for "Alarm Time (Minute)". */
#define MAD_ALARM_DURATION (uint8_t) 3 /* RTC memory address for "Alarm Duration" (in minute). */
void restoreAlarm();
uint8_t rollOver(uint8_t, uint8_t, uint8_t, boolean);
#define BTN_HOLD_DURATION 250 /* Button hold duration in millisecond. */
#define BTN_HOLD_INTERVAL 100 /* Button hold interval in millisecond. */
AnalogButtons analogButtons(PIN_A0, INPUT, 1, 26);
Button btnEnter = Button(883, &cbBtnEnter, NULL, BTN_HOLD_DURATION, BTN_HOLD_INTERVAL);
Button btnLeft = Button(738, &cbBtnLeft , NULL, BTN_HOLD_DURATION, BTN_HOLD_INTERVAL);
Button btnUp = Button(584, &cbBtnUp , NULL, BTN_HOLD_DURATION, BTN_HOLD_INTERVAL);
Button btnDown = Button(439, &cbBtnDown , NULL, BTN_HOLD_DURATION, BTN_HOLD_INTERVAL);
Button btnRight = Button(293, &cbBtnRight, NULL, BTN_HOLD_DURATION, BTN_HOLD_INTERVAL);
Button btnBack = Button(144, &cbBtnBack , NULL, BTN_HOLD_DURATION, BTN_HOLD_INTERVAL);
/* alarmOn and alarmOff is only to prevent the compiler warning: ISO C++ forbids converting a string constant to 'char*' */
char alarmOn[] = "Yes";
char alarmOff[] = "No";
String alarmRunStr[] = { "1m", "5m", "10m", "15m" }; /* Array of allowable alarm runs (displayed in menu). */
uint8_t alarmRunMin[] = { 1, 5, 10, 15 }; /* Array of allowable alarm runs (actual minutes). */
MenuItem mainMenu[] = {
ItemHeader(),
ItemCommand("Date/Time", cbMenuSetTime),
ItemToggle("Alarm", alarmOn, alarmOff, cbMenuToggleAlarm),
ItemCommand("Alarm Time", cbMenuSetAlarm),
ItemList("Alarm Run", alarmRunStr, 4, cbMenuSetAlarmDuration),
ItemFooter()
};
#define LCD_ROWS 2
#define LCD_COLS 16
LcdMenu menu(LCD_ROWS, LCD_COLS);
ThreeWire rtcWire(7, 8, 6); // DAT, CLK, RST
RtcDS1302<ThreeWire> rtc(rtcWire);
RtcDateTime clockTime; /* The currently displayed clock time. */
uint8_t prevSec = 61; /* Pick a value that's not 0 to 60. */
uint8_t cmdKey = CMD_IGNORE; /* Which button is currently pressed. */
uint8_t currScreen = SCREEN_CLOCK; /* Which screen is currenly active. */
boolean editTimeInit = false; /* Is editTime already initialized? */
RtcDateTime editTime; /* The time shown when in SCREEN_SET_TIME and SCREEN_SET_ALARM_TIME mode. */
boolean alarmActive = false; /* Whether the alarm is currently sounding or not. */
unsigned long alarmActiveStart = 0; /* Record the millis() start of alarmActive. */
boolean alarmEnabled = false; /* Trigger the alarm when the alarmHour and alarmMinute is reached. */
uint8_t alarmHour = 0; /* Alarm time hour. */
uint8_t alarmMinute = 0; /* Alarm time minute. */
uint8_t alarmRunIdx = 0; /* Index of alarmRunMin. */
uint8_t editPos = EDIT_YEAR; /* Which field is being edited. */
boolean editChanged = false; /* If time has been edited, this is the flag to allow repainting. */
LiquidCrystal* lcd = NULL; /* Hold reference to menu.lcd .*/
#define DHT11_INIT 255 /* Value outside DHT11 range (and also not NaN) to signify "please read the sensor value". */
DHT dht(PIN_A2, DHT11);
float temperature = DHT11_INIT;
float humidity = DHT11_INIT;
void setup() {
#ifdef DEBUG
Serial.begin(115200, SERIAL_8N1);
#endif
menu.setupLcdWithMenu(12, 11, 5, 4, 3, 2, mainMenu);
menu.hide();
lcd = menu.lcd;
rtc.Begin();
if (rtc.GetIsWriteProtected()) {
rtc.SetIsWriteProtected(false);
}
if (!rtc.GetIsRunning()) {
rtc.SetIsRunning(true);
}
restoreAlarm();
dht.begin();
analogButtons.add(btnEnter);
analogButtons.add(btnLeft);
analogButtons.add(btnUp);
analogButtons.add(btnDown);
analogButtons.add(btnRight);
analogButtons.add(btnBack);
}
void loop() {
clockTime = rtc.GetDateTime(); /* Used by the clock display and the alarm trigger. */
analogButtons.check();
if (currScreen == SCREEN_CLOCK) {
screenClock();
} else if (currScreen == SCREEN_MENU) {
screenMenu();
} else if (currScreen == SCREEN_SET_TIME) {
screenSetTime();
} else if (currScreen == SCREEN_SET_ALARM_TIME) {
screenSetAlarmTime();
}
alarm();
cmdKey = CMD_IGNORE; /* Must be reset at the end of each loop to prevent button press "lock". */
}
/* Callback when the user pressed the ENTER button. */
void cbBtnEnter() {
cmdKey = CMD_ENTER;
DEBUG_PRINTLN(F("Pressed ENTER"));
}
/* Callback when the user pressed the LEFT button. */
void cbBtnLeft() {
cmdKey = CMD_LEFT;
DEBUG_PRINTLN(F("Pressed LEFT"));
}
/* Callback when the user pressed the UP button. */
void cbBtnUp() {
cmdKey = CMD_UP;
DEBUG_PRINTLN(F("Pressed UP"));
}
/* Callback when the user pressed the DOWN button. */
void cbBtnDown() {
cmdKey = CMD_DOWN;
DEBUG_PRINTLN(F("Pressed DOWN"));
}
/* Callback when the user pressed the RIGHT button. */
void cbBtnRight() {
cmdKey = CMD_RIGHT;
DEBUG_PRINTLN(F("Pressed RIGHT"));
}
/* Callback when the user pressed the BACK button. */
void cbBtnBack() {
cmdKey = CMD_BACK;
DEBUG_PRINTLN(F("Pressed BACK"));
}
/* Callback when the user activates the "Set Time" menu item. */
void cbMenuSetTime() {
menu.hide();
currScreen = SCREEN_SET_TIME;
delay(SCREEN_TRANSITION);
}
/* Callback when the user toggles the "Alarm" enable/disable menu item. */
void cbMenuToggleAlarm(const uint8_t yesNo) {
alarmEnabled = yesNo == (uint8_t) 1;
rtc.SetMemory(MAD_ALARM_ENABLE, alarmEnabled ? (uint8_t) 1 : (uint8_t) 0);
/* If the alarm is currently sounding, disabling alarm will stop it. */
if (!alarmEnabled && alarmActive) {
alarmActive = false;
noToneAC();
DEBUG_PRINTLN(F("Alarm stopped"));
}
}
/* Callback when the user activates the "Set Alarm Time" menu item. */
void cbMenuSetAlarm() {
menu.hide();
currScreen = SCREEN_SET_ALARM_TIME;
delay(SCREEN_TRANSITION);
}
/* Callback when the user commits (pressed ENTER) the alarm duration in the menu system. */
void cbMenuSetAlarmDuration(const uint8_t pos) {
alarmRunIdx = pos;
rtc.SetMemory(MAD_ALARM_DURATION, alarmRunIdx);
DEBUG_PRINT(F("Alarm duration pos = "));
DEBUG_PRINT(pos);
DEBUG_PRINT(F(", str = "));
DEBUG_PRINTLN(alarmRunStr[pos]);
}
/* The clock screen. This displays the current date/time and DHT sensor readings. */
void screenClock() {
if (cmdKey == CMD_ENTER) {
currScreen = SCREEN_MENU;
lcd->clear(); /* Only necessary to make the transition uniform. */
delay(SCREEN_TRANSITION);
menu.show();
return;
} else if (cmdKey == CMD_BACK) {
if (alarmActive) {
alarmActive = false;
noToneAC();
DEBUG_PRINTLN(F("Alarm stopped"));
}
return;
}
if (clockTime.IsValid()) {
const uint8_t sec = clockTime.Second();
if (sec != prevSec) { /* Only need to redraw when something changed. */
prevSec = sec;
showClock(clockTime, 0, 0, 0, 1);
}
} else {
lcd->setCursor(0, 0);
lcd->print(F("RTC err "));
lcd->setCursor(0, 1);
lcd->print(F("Check batt "));
}
showDHT();
}
/* The menu screen. */
void screenMenu() {
if (cmdKey == CMD_ENTER) {
DEBUG_PRINTLN(F("Menu ENTER"));
menu.enter();
} else if (cmdKey == CMD_BACK) {
DEBUG_PRINTLN(F("Menu BACK"));
resetAlarmRun();
menu.hide();
temperature = DHT11_INIT; /* If not reset, it will not be redrawn. */
humidity = DHT11_INIT; /* If not reset, it will not be redrawn. */
currScreen = SCREEN_CLOCK;
delay(SCREEN_TRANSITION);
} else if (cmdKey == CMD_UP) {
DEBUG_PRINTLN(F("Menu UP"));
resetAlarmRun();
menu.up();
} else if (cmdKey == CMD_DOWN) {
DEBUG_PRINTLN(F("Menu DOWN"));
resetAlarmRun();
menu.down();
} else if (cmdKey == CMD_LEFT) {
DEBUG_PRINTLN(F("Menu LEFT"));
menu.left();
} else if (cmdKey == CMD_RIGHT) {
DEBUG_PRINTLN(F("Menu RIGHT"));
menu.right();
}
}
/* The "Set Time" screen. */
void screenSetTime() {
if (cmdKey == CMD_ENTER) { /* Apply date/time adjustments. */
rtc.SetDateTime(editTime);
}
if (cmdKey == CMD_ENTER || cmdKey == CMD_BACK) { /* Exit this screen. */
lcd->noBlink();
editTimeInit = false;
editChanged = false;
currScreen = SCREEN_MENU;
lcd->clear(); /* Only necessary to make the transition uniform. */
delay(SCREEN_TRANSITION);
menu.show();
return;
}
boolean posChanged = false;
if (!editTimeInit) {
editTimeInit = true;
editTime = clockTime;
lcd->blink();
editChanged = true;
editPos = EDIT_YEAR;
posChanged = true;
}
if (editPos == EDIT_YEAR) {
if (cmdKey == CMD_UP || cmdKey == CMD_DOWN) {
const uint8_t year = rollOver(editTime.Year() - c_OriginYear, 0, 99, cmdKey == CMD_UP);
editTime = RtcDateTime(year, editTime.Month(), editTime.Day(), editTime.Hour(), editTime.Minute(), editTime.Second());
editChanged = true;
} else if (cmdKey == CMD_LEFT) {
editPos = EDIT_SECOND;
posChanged = true;
} else if (cmdKey == CMD_RIGHT) {
editPos = EDIT_MONTH;
posChanged = true;
}
} else if (editPos == EDIT_MONTH) {
if (cmdKey == CMD_UP || cmdKey == CMD_DOWN) {
const uint8_t month = rollOver(editTime.Month(), 1, 12, cmdKey == CMD_UP);
editTime = RtcDateTime(editTime.Year(), month, editTime.Day(), editTime.Hour(), editTime.Minute(), editTime.Second());
editChanged = true;
} else if (cmdKey == CMD_LEFT) {
editPos = EDIT_YEAR;
posChanged = true;
} else if (cmdKey == CMD_RIGHT) {
editPos = EDIT_DAY;
posChanged = true;
}
} else if (editPos == EDIT_DAY) {
if (cmdKey == CMD_UP || cmdKey == CMD_DOWN) {
const uint8_t dayOfMonth = rollOver(editTime.Day(), 1, 31, cmdKey == CMD_UP);
editTime = RtcDateTime(editTime.Year(), editTime.Month(), dayOfMonth, editTime.Hour(), editTime.Minute(), editTime.Second());
editChanged = true;
} else if (cmdKey == CMD_LEFT) {
editPos = EDIT_MONTH;
posChanged = true;
} else if (cmdKey == CMD_RIGHT) {
editPos = EDIT_HOUR;
posChanged = true;
}
} else if (editPos == EDIT_HOUR) {
if (cmdKey == CMD_UP || cmdKey == CMD_DOWN) {
const uint8_t hour = rollOver(editTime.Hour(), 0, 23, cmdKey == CMD_UP);
editTime = RtcDateTime(editTime.Year(), editTime.Month(), editTime.Day(), hour, editTime.Minute(), editTime.Second());
editChanged = true;
} else if (cmdKey == CMD_LEFT) {
editPos = EDIT_DAY;
posChanged = true;
} else if (cmdKey == CMD_RIGHT) {
editPos = EDIT_MINUTE;
posChanged = true;
}
} else if (editPos == EDIT_MINUTE) {
if (cmdKey == CMD_UP || cmdKey == CMD_DOWN) {
const uint8_t minute = rollOver(editTime.Minute(), 0, 59, cmdKey == CMD_UP);
editTime = RtcDateTime(editTime.Year(), editTime.Month(), editTime.Day(), editTime.Hour(), minute, editTime.Second());
editChanged = true;
} else if (cmdKey == CMD_LEFT) {
editPos = EDIT_HOUR;
posChanged = true;
} else if (cmdKey == CMD_RIGHT) {
editPos = EDIT_SECOND;
posChanged = true;
}
} else { // editPos == EDIT_SECOND
if (cmdKey == CMD_UP || cmdKey == CMD_DOWN) {
const uint8_t second = rollOver(editTime.Second(), 0, 59, cmdKey == CMD_UP);
editTime = RtcDateTime(editTime.Year(), editTime.Month(), editTime.Day(), editTime.Hour(), editTime.Minute(), second);
editChanged = true;
} else if (cmdKey == CMD_LEFT) {
editPos = EDIT_MINUTE;
posChanged = true;
} else if (cmdKey == CMD_RIGHT) {
editPos = EDIT_YEAR;
posChanged = true;
}
}
if (editChanged) { /* In the "Set Time" screen, the LCD only need to be updated when something has been changed. */
uint8_t dayMax = 31;
while (!editTime.IsValid()) { /* Needed to prevent edge case such as 2020-02-31. */
dayMax--;
const boolean inc = editPos == EDIT_DAY ? cmdKey == CMD_UP : false;
const uint8_t dayOfMonth = rollOver(editTime.Day(), 1, dayMax, inc);
editTime = RtcDateTime(editTime.Year(), editTime.Month(), dayOfMonth, editTime.Hour(), editTime.Minute(), editTime.Second());
}
showClock(editTime, 3, 0, 4, 1); /* This changes cursor position, so the posChanged need to be adjusted. */
editChanged = false;
posChanged = true;
}
if (posChanged) { /* Change cursor coordinate to the new edit position. */
uint8_t lcdX, lcdY;
if (editPos == EDIT_YEAR) {
lcdX = 6;
lcdY = 0;
} else if (editPos == EDIT_MONTH) {
lcdX = 9;
lcdY = 0;
} else if (editPos == EDIT_DAY) {
lcdX = 12;
lcdY = 0;
} else if (editPos == EDIT_HOUR) {
lcdX = 5;
lcdY = 1;
} else if (editPos == EDIT_MINUTE) {
lcdX = 8;
lcdY = 1;
} else { // editPos == EDIT_SECOND
lcdX = 11;
lcdY = 1;
}
lcd->setCursor(lcdX, lcdY);
}
}
/* The "Set Alarm Time" screen. */
void screenSetAlarmTime() {
if (cmdKey == CMD_ENTER) { /* Write alarm time adjustments to RTC memory. */
rtc.SetMemory(MAD_ALARM_HOUR, alarmHour);
rtc.SetMemory(MAD_ALARM_MINUTE, alarmMinute);
} else if (cmdKey == CMD_BACK) { /* Reset alarm time to the one already set in RTC memory. */
alarmHour = rtc.GetMemory(MAD_ALARM_HOUR);
alarmMinute = rtc.GetMemory(MAD_ALARM_MINUTE);
if (alarmHour > 23 || alarmMinute > 59) {
/* Garbage from RTC RAM, so we set default alarm: 7 o'clock. */
alarmHour = 7;
alarmMinute = 0;
}
}
if (cmdKey == CMD_ENTER || cmdKey == CMD_BACK) { /* Exit this screen. */
lcd->noBlink();
editTimeInit = false;
editChanged = false;
currScreen = SCREEN_MENU;
lcd->clear(); /* Only necessary to make the transition uniform. */
delay(SCREEN_TRANSITION);
menu.show();
return;
}
boolean posChanged = false;
if (!editTimeInit) {
editTimeInit = true;
lcd->blink();
editChanged = true;
editPos = EDIT_HOUR;
posChanged = true;
}
if (editPos == EDIT_HOUR) {
if (cmdKey == CMD_UP || cmdKey == CMD_DOWN) {
alarmHour = rollOver(alarmHour, 0, 23, cmdKey == CMD_UP);
editChanged = true;
} else if (cmdKey == CMD_LEFT || cmdKey == CMD_RIGHT) {
editPos = EDIT_MINUTE;
posChanged = true;
}
} else { // editPos == EDIT_MINUTE
if (cmdKey == CMD_UP || cmdKey == CMD_DOWN) {
alarmMinute = rollOver(alarmMinute, 0, 59, cmdKey == CMD_UP);
editChanged = true;
} else if (cmdKey == CMD_LEFT || cmdKey == CMD_RIGHT) {
editPos = EDIT_HOUR;
posChanged = true;
}
}
if (editChanged) { /* In the "Set Alarm Time" screen, the LCD only need to be updated when something has been changed. */
showSetAlarmTime(alarmHour, alarmMinute); /* This changes cursor position, so the posChanged need to be adjusted. */
editChanged = false;
posChanged = true;
}
if (posChanged) { /* Change cursor coordinate to the new edit position. */
uint8_t lcdX, lcdY;
if (editPos == EDIT_HOUR) {
lcdX = 7;
lcdY = 0;
} else { // editPos == EDIT_MINUTE
lcdX = 10;
lcdY = 0;
}
lcd->setCursor(lcdX, lcdY);
}
}
/*
Start the alarm according to the alarmEnabled, alarmHour, and alarmMinute settings.
The alarm tone is played continuously until the user stopped the alarm, or the alarm runtime is over.
*/
void alarm() {
boolean stopAlarm = false;
const unsigned long now = millis();
if (alarmEnabled) {
if (clockTime.Hour() == alarmHour && clockTime.Minute() == alarmMinute && clockTime.Second() >= 00 && clockTime.Second() <= 1) {
alarmActive = true;
alarmActiveStart = now;
DEBUG_PRINTLN(F("Alarm start"));
}
} else {
if (alarmActive) {
stopAlarm = true;
}
}
if (alarmActive) {
if (now - alarmActiveStart <= 1000UL * 60 * (unsigned long) alarmRunMin[alarmRunIdx]) {
/*
The alarm tone is alternating between 2400Hz and 2850Hz, each played for 250ms.
The tone is one of the "General Purpose" from here:
https://www.eaton.com/gb/en-gb/products/safety-security-emergency-communications/alarms-and-signaling-devices/fire-rated/fire-alarm-tones-and-sounds.html
*/
toneAC((now / 250UL) % 2 == 0 ? 2400 : 2850, 10, 0, true); /* Play tone continuously in background. */
} else {
stopAlarm = true;
}
}
if (stopAlarm) {
alarmActive = false;
noToneAC();
DEBUG_PRINTLN(F("Alarm stopped"));
}
}
/*
Reset the menu if the selected alarm position is not committed yet.
Resetting is necessary because the menu system has no indication for committed/uncommitted list position.
This should be called when in the menu screen and the user pressed: BACK, UP, or RIGHT buttons.
*/
void resetAlarmRun() {
if (mainMenu[4].itemIndex != alarmRunIdx) {
mainMenu[4].itemIndex = alarmRunIdx;
}
}
/* Display the current RTC time to the LCD. */
void showClock(const RtcDateTime& dt, const uint8_t dateX, const uint8_t dateY, const uint8_t timeX, const uint8_t timeY) {
const size_t len = 11; /* 10 chars max + 1 null terminator. */
char dateString[len];
snprintf_P(dateString, len, PSTR("%04u-%02u-%02u"), dt.Year(), dt.Month(), dt.Day());
lcd->setCursor(dateX, dateY);
lcd->print(dateString);
snprintf_P(dateString, len, PSTR("%02u:%02u:%02u"), dt.Hour(), dt.Minute(), dt.Second());
lcd->setCursor(timeX, timeY);
lcd->print(dateString);
}
/*
Read the DHT sensor and show the readings to the LCD.
Unlike RTC, the DHT info is only needed on the main screen.
So there is no point in sampling the sensor when not in the main screen.
*/
void showDHT() {
char str[6]; /* 5 char max + 1 null terminator */
float val = dht.readTemperature();
if (val != temperature) { /* Only need to refresh the LCD when the reading has changed. */
temperature = val;
lcd->setCursor(11, 0);
if (isnan(temperature)) {
lcd->print(F("T err"));
} else {
dtostrf(temperature, 4, 1, str);
lcd->print(str);
lcd->print(F("C"));
}
}
val = dht.readHumidity();
if (val != humidity) { /* Only need to refresh the LCD when the reading has changed. */
humidity = val;
lcd->setCursor(11, 1);
if (isnan(humidity)) {
lcd->print(F("H err"));
} else {
dtostrf(humidity, 4, 1, str);
lcd->print(str);
lcd->print(F("%"));
}
}
}
/* Show the alarm time being edited to the LCD. */
void showSetAlarmTime(const uint8_t hour, const uint8_t minute) {
const size_t len = 6; /* 5 chars max + 1 null terminator. */
char dateString[len];
snprintf_P(dateString, len, PSTR("%02u:%02u"), hour, minute);
lcd->setCursor(6, 0);
lcd->print(dateString);
}
/* Restore the alarm settings from the RTC RAM. */
void restoreAlarm() {
const size_t count = 4;
uint8_t rtcRam[count];
const uint8_t got = rtc.GetMemory(rtcRam, count);
alarmEnabled = rtcRam[MAD_ALARM_ENABLE] == (uint8_t) 1;
alarmHour = rtcRam[MAD_ALARM_HOUR];
alarmMinute = rtcRam[MAD_ALARM_MINUTE];
alarmRunIdx = rtcRam[MAD_ALARM_DURATION];
if (alarmHour > 23 || alarmMinute > 59) {
/* Garbage from RTC RAM, so we set default alarm: 7 o'clock. */
alarmHour = 7;
alarmMinute = 0;
}
if (alarmRunIdx >= sizeof(alarmRunMin) / sizeof(alarmRunMin[0])) {
/* Garbage from RTC RAM, so we set default alarm duration: 1 minute. */
alarmRunIdx = 0;
}
mainMenu[2].isOn = alarmEnabled;
mainMenu[4].itemIndex = alarmRunIdx;
#ifdef DEBUG
if (got != count) {
DEBUG_PRINT(F("RTC RAM problem, count = "));
DEBUG_PRINT(count);
DEBUG_PRINT(", got = ");
DEBUG_PRINTLN(got);
}
DEBUG_PRINT(F("RTC RAM read ("));
DEBUG_PRINT(got);
DEBUG_PRINT(F(") = alarm "));
DEBUG_PRINT(alarmEnabled ? F("enabled") : F("disabled"));
DEBUG_PRINT(F(", alarm time = "));
DEBUG_PRINT(alarmHour);
DEBUG_PRINT(F(":"));
DEBUG_PRINT(alarmMinute);
DEBUG_PRINT(F(", alarm run = "));
DEBUG_PRINTLN(alarmRunStr[alarmRunIdx]);
#else
UNUSED(got);
#endif
}
/* Roll over a numeric value when the user incremented it up or down. */
uint8_t rollOver(const uint8_t now, const uint8_t min, const uint8_t max, const boolean increment) {
if (increment) {
return now < max ? now + 1 : min;
} else {
return now > min ? now - 1 : max;
}
}

11
test/README Normal file
View File

@ -0,0 +1,11 @@
This directory is intended for PlatformIO Unit Testing and project tests.
Unit Testing is a software testing method by which individual units of
source code, sets of one or more MCU program modules together with associated
control data, usage procedures, and operating procedures, are tested to
determine whether they are fit for use. Unit testing finds problems early
in the development cycle.
More information about PlatformIO Unit Testing:
- https://docs.platformio.org/page/plus/unit-testing.html