#pragma clang diagnostic push #pragma ide diagnostic ignored "hicpp-multiway-paths-covered" #pragma ide diagnostic ignored "bugprone-branch-clone" #include #include "countdown.h" #include "beeper.h" #include "buzzer.h" #include "display.h" #include "remote.h" #define COUNTDOWN_END_SEQUENCE_STEPS 5 #define COUNTDOWN_END_SEQUENCE_REPEAT 3 #define HALF_BEEP_DURATION 500 #define LAST_10_BEEP_DURATION 100 enum CountdownState { CONFIG, READY, RUNNING, PAUSED, END }; CountdownState countdownState = CONFIG; bool countdownConfigDirty = false; long countdownMillis = 6 * 60 * 1000; long countdownRest = countdownMillis; unsigned long countdownLast = 0; int countdownEndSequenceStep = 0; unsigned long countdownEndSequenceDelay = 0; unsigned long countdownEndSequenceLast = 0; bool countdownHalfTimeBeep = false; long countdownBeepSeconds = -1; void countdownConfigLoad() { if (LittleFS.begin(true)) { Serial.println("Filesystem mounted."); if (LittleFS.exists("/countdownMillis")) { File file = LittleFS.open("/countdownMillis"); if (file) { const String content = file.readString(); if (content) { countdownMillis = content.toInt(); Serial.printf("Read countdownMillis from file: countdownMillis=%lu, file=%s\n", countdownMillis, file.path()); } else { Serial.printf("WARN: Cannot parse content of: %s\n", file.path()); } file.close(); } else { Serial.printf("WARN: Cannot open file for READ: %s\n", file.path()); } } LittleFS.end(); Serial.println("Filesystem unmounted."); } else { Serial.println("ERROR: Failed to mount filesystem."); } } void countdownConfigWrite() { if (LittleFS.begin(true)) { Serial.println("Filesystem mounted."); File file = LittleFS.open("/countdownMillis", "w", true); if (file) { file.printf("%lu", countdownMillis); Serial.printf("Wrote countdownMillis to file: countdownMillis=%lu, file=%s\n", countdownMillis, file.path()); file.close(); countdownConfigDirty = false; } else { Serial.printf("WARN: Cannot open file for WRITE: %s\n", file.path()); } LittleFS.end(); Serial.println("Filesystem unmounted."); } else { Serial.println("ERROR: failed to mount filesystem"); } } void setState(const CountdownState newState) { if (countdownState == newState) { return; } countdownState = newState; const char *name; bool buzzer = false; switch (countdownState) { case CONFIG: countdownRest = countdownMillis; name = "CONFIG"; break; case READY: if (countdownConfigDirty) { countdownConfigWrite(); } countdownRest = countdownMillis; countdownHalfTimeBeep = false; countdownBeepSeconds = -1; name = "READY"; break; case RUNNING: countdownLast = millis(); name = "RUNNING"; break; case PAUSED: name = "PAUSED"; break; case END: buzzer = true; countdownEndSequenceDelay = 0; countdownEndSequenceStep = 0; name = "END"; break; default: name = "[???]"; break; } buzzerEnable(buzzer); Serial.printf("Mode: %s\n", name); } void drawSequence() { const auto now = max(1UL, millis()); if (countdownEndSequenceLast != 0 && now - countdownEndSequenceLast < countdownEndSequenceDelay) { return; } switch (countdownEndSequenceStep % COUNTDOWN_END_SEQUENCE_STEPS) { case 0: case 2: drawAll(WHITE); countdownEndSequenceDelay = 100; break; case 1: case 3: drawBlack(); countdownEndSequenceDelay = 100; break; default: drawDashes(); countdownEndSequenceDelay = 500; break; } countdownEndSequenceStep++; countdownEndSequenceLast = now; if (countdownEndSequenceStep >= COUNTDOWN_END_SEQUENCE_STEPS * COUNTDOWN_END_SEQUENCE_REPEAT) { setState(READY); } } void countdownDraw() { switch (countdownState) { case CONFIG: drawMillis(countdownRest, MAGENTA); break; case READY: drawMillis(countdownRest, GREEN); break; case RUNNING: { const auto color = countdownRest >= 60000 ? WHITE : (countdownRest >= 10000 ? YELLOW : RED); drawMillis(countdownRest, color); break; } case PAUSED: drawMillis(countdownRest, BLUE); break; case END: drawSequence(); break; } } void countdownSetup() { countdownConfigLoad(); setState(READY); } void updateTime() { if (countdownState != RUNNING) { return; } const auto now = max(1UL, millis()); if (countdownLast != 0) { const auto diff = now - countdownLast; countdownRest -= static_cast(diff); if (countdownRest <= countdownMillis / 2) { if (!countdownHalfTimeBeep) { beep(HALF_BEEP_DURATION); countdownHalfTimeBeep = true; } } const auto seconds = static_cast(ceil(countdownRest / 1000.0)); if (seconds <= 10 && seconds > 0) { if (countdownBeepSeconds != seconds) { beep(LAST_10_BEEP_DURATION); countdownBeepSeconds = seconds; } } if (countdownRest <= 0) { countdownRest = 0; setState(END); } } countdownLast = now; } void countdownLoop() { updateTime(); countdownDraw(); } void buttonShortPressed() { switch (countdownState) { case CONFIG: { const auto seconds = (countdownMillis / 30000) * 30 % (15 * 60) + 30; countdownMillis = seconds * 1000; countdownRest = countdownMillis; countdownConfigDirty = true; break; } case READY: setState(RUNNING); break; case RUNNING: setState(PAUSED); break; case PAUSED: setState(RUNNING); break; case END: setState(READY); break; } } void buttonLongPressed() { switch (countdownState) { case CONFIG: setState(READY); break; case RUNNING: setState(READY); break; case READY: setState(CONFIG); break; case PAUSED: setState(READY); break; case END: setState(READY); break; } } void remoteCmd(uint16_t cmd) { switch (countdownState) { case CONFIG: switch (cmd) { case IR_CMD_UP: countdownMillis = min(15 * 60000L, countdownMillis + 60000); countdownRest = countdownMillis; countdownConfigDirty = true; break; case IR_CMD_DOWN: countdownMillis = max(0L, countdownMillis - 60000); countdownRest = countdownMillis; countdownConfigDirty = true; break; case IR_CMD_RIGHT: countdownMillis = min(15 * 60000L, countdownMillis + 1000); countdownRest = countdownMillis; countdownConfigDirty = true; break; case IR_CMD_LEFT: countdownMillis = max(0L, countdownMillis - 1000); countdownRest = countdownMillis; countdownConfigDirty = true; break; case IR_CMD_C: case IR_CMD_X: setState(READY); break; } break; case READY: if (cmd == IR_CMD_A) { setState(CONFIG); } case RUNNING: case PAUSED: case END: switch (cmd) { case IR_CMD_X: setState(countdownState == RUNNING ? PAUSED : RUNNING); break; case IR_CMD_C: setState(READY); break; } break; } } #pragma clang diagnostic pop