Sporttafel/src/countdown.cpp

315 lines
7.4 KiB
C++

#pragma clang diagnostic push
#pragma ide diagnostic ignored "hicpp-multiway-paths-covered"
#pragma ide diagnostic ignored "bugprone-branch-clone"
#include <LittleFS.h>
#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<long>(diff);
if (countdownRest <= countdownMillis / 2) {
if (!countdownHalfTimeBeep) {
beep(HALF_BEEP_DURATION);
countdownHalfTimeBeep = true;
}
}
const auto seconds = static_cast<long>(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