/*
ERTA Timer - www.erta.com.ar
by Zotarelli and Lovato
v0.94 standalone- 21/9/2023
Standalone version: no remote control
RemoteXY: bluetooth control via remotexy platform/library/app
ref https://forum.arduino.cc/t/a-demo-code-explaining-the-switch-case-state-machine-and-how-to-do-things-almost-in-parallel/888172
Parts of code reused and inspired from https://forum.arduino.cc/t/a-demo-code-explaining-the-switch-case-state-machine-and-how-to-do-things-almost-in-parallel/888172/6
many thanks to https://forum.arduino.cc/u/StefanL38 for his input and feedback
Display library: https://github.com/jasonacox/TM1637TinyDisplay/tree/master
Bluetooth control: https://remotexy.com
Hardware:
TM1637 4-digit(or more) LED module, preferably without colon
active buzzer
2 push buttons
standalone version: any arduino, ESP32 to be tested
remoteXY version: ESP32
***changelog***
v0.9 - Initial PoC 11/7/2023
Standalone full functionality implemented
v0.91
borrado varible beeping, fix triple beep con 2s
v0.92
agregado boton de modo corto/largo
cambio constantes pines a byte
renombrado pines boton e instancia botones debounce
v0.93 8/8
aclaración en comments, renombrado de algunas variables para que sean mas claras
agregado estado modo_practica para mostrar mensaje de modo práctica, similar a standby
reordenado llamadas a timestamps previo al final de cada sección
v0.94 21/9
renombrado algunas variables para extra consistencia, comments y strings
Cambio lógica en cambio de modo, se evalúa todo en el case standby ahora
v0.94b 26/9
Cambio a buzzer activo
*/
// DEBUG MACROS
#define dbg(myFixedText, variableName) \
Serial.print(F(#myFixedText " " #variableName "=")); \
Serial.println(variableName);
// usage: dbg("1:my fixed text",myVariable);
// myVariable can be any variable or expression that is defined in scope
#define dbgi(myFixedText, variableName, timeInterval) \
{ \
static unsigned long intervalStartTime; \
if (millis() - intervalStartTime >= timeInterval) { \
intervalStartTime = millis(); \
Serial.print(F(#myFixedText " " #variableName "=")); \
Serial.println(variableName); \
} \
}
//END DEBUG MACROS
//includes
#include <Bounce2.h>
#include <TM1637TinyDisplay.h>
// the states for the state-machine
const byte standby = 0;
const byte comienzo_10 = 1;
const byte turno_a = 2;
const byte intermedio_10 = 3;
const byte turno_b = 4;
const byte triple_beep = 5;
const byte modo_practica = 6;
byte myState = standby; //estado inicial
//constantes
const int duracion_turno_practica = 4; //240s nominal
const int duracion_turno_torneo = 10; //180s nominal
const int duracion_preparacion = 10; //10 segundos nominal
const byte pin_boton_start = 2; //Pin boton start/reset, active low, conectar entre GND y PIN
const byte pin_boton_modo = 7; //Pin boton start/reset, active low, conectar entre GND y PIN
const byte pin_clock = 4; //Pin CLOCK display TM1637
const byte pin_data = 3; //Pin DATA display TM1637
const byte pin_buzzer = 8; //pin buzzer
const PROGMEM char standbystring[] = "MODO TORNEO - ERTA TIMER v0.94 - ZOTARELLI-LOVATO"; //String standby
const PROGMEM char stringpractica[] = "MODO PRACTICA - ERTA TIMER v0.94 - ZOTARELLI-LOVATO -"; //String para modo práctica
//vars
int duracion_turno = duracion_turno_torneo; //cargar variable con constante inicial, variable intermedia usada de
byte tiempo_preparacion = duracion_preparacion; //cargar variable con constante inicial
int tiempo_turno = duracion_turno; //variable contador tiempo de cada turno, tiene que ser int o falla la función display TM1637
unsigned long waiting10_timer; //timestamp inicio de 10s pausa
unsigned long waitingturno_timer; //timestamp inicio de turno
unsigned long beeptimestamp; //timestamp inicio de beeper
unsigned long triplebeeptimestamp; //timestamp inicio de triple beep final
//unsigned long modo_timer; //timestamp
bool beep_req = false; //beep requested
byte beepcount; //para 3 beeps finales
//instancias
Bounce boton_start = Bounce(); //instancing bounce
Bounce boton_modo = Bounce(); //instancing bounce
TM1637TinyDisplay display(pin_clock, pin_data); //instancing display
void setup() {
boton_start.attach(pin_boton_start, INPUT_PULLUP); // Attach the debouncer to a pin with INPUT_PULLUP mode
boton_modo.attach(pin_boton_modo, INPUT_PULLUP); // Attach the debouncer to a pin with INPUT_PULLUP mode
boton_start.interval(25); // Use a debounce interval of 25 milliseconds
boton_modo.interval(25); // Use a debounce interval of 25 milliseconds
pinMode(LED_BUILTIN, OUTPUT); // Setup the LED
pinMode(pin_buzzer, OUTPUT); // Setup buzzer output
display.begin(); // init display
display.clear();
Serial.begin(115200);
Serial.println("ERTA TIMER v0.94 - ZOTARELLI-LOVATO "); //poner la version correcta
}
void loop() {
boton_start.update(); // Update the Bounce instance boton start
if (boton_start.fell()) { // Call code if button transitions from HIGH to LOW
if (myState == standby or myState == modo_practica) {
Serial.println("boton presionado, iniciar");
beep_req = true;
tiempo_preparacion = duracion_preparacion; //init preparacion
beepcount = 0;
display.stopAnimation();
display.clear();
display.showString("PR", 2, 0); //para mostrar el tiempo de inicio antes de decrementarlo
display.showNumber(tiempo_preparacion, false, 2, 2); //para mostrar el tiempo de inicio antes de decrementarlo
waiting10_timer = millis(); //timestamp para inicio de preparación
beeptimestamp = millis(); //timestamp beeper
myState = comienzo_10;
} else if (myState >= 1) { //boton presionado con ciclo en progreso, forced stop/reset
beep_req = 1;
Serial.println("Abort/reset");
display.clear();
beeptimestamp = millis(); //timestamp beeper
triplebeeptimestamp = millis(); //timestamp triple beeper
myState = triple_beep;
}
}
switch (myState) {
case standby:
//display scroll text
if (!display.Animate()) {
if (duracion_turno == duracion_turno_torneo) { //Definir mensaje a scrollear en base al modo
display.startStringScroll_P(standbystring, 250);
} else if (duracion_turno == duracion_turno_practica) {
display.startStringScroll_P(stringpractica, 250);
}
}
boton_modo.update(); // Update the Bounce instance boton modo
if (boton_modo.fell()) { //cambio modo presionado
display.stopAnimation();
display.clear();
//display.showString("PREP", 4, 0);
if (duracion_turno == duracion_turno_torneo) { //toggle de modo
duracion_turno = duracion_turno_practica;
//myState = modo_practica;
//modo_timer = millis(); //timestamp para scroll texto cambio modo
} else {
duracion_turno = duracion_turno_torneo;
}
Serial.println("cambio de modo");
}
break;
case modo_practica:
//display scroll text
if (!display.Animate()) {
display.startStringScroll_P(stringpractica, 250);
}
//if (TimePeriodIsOver(waiting10_timer, 1000)) {
// }
break;
case comienzo_10:
// timer de preparacion linea tiro inicial
if (TimePeriodIsOver(waiting10_timer, 1000)) {
// tick cada 1 segundo
tiempo_preparacion--;
dbg("Tiempo restante preparación", tiempo_preparacion);
display.showString("PR", 2, 0);
display.showNumber(tiempo_preparacion, false, 2, 2);
if (tiempo_preparacion == 0) {
beep_req = true; //pedir beep
tiempo_turno = duracion_turno; //init duración turno
display.showString("A", 1, 0); //para mostrar el tiempo de inicio antes de decrementarlo
display.showNumber(tiempo_turno, false, 3, 1); //para mostrar el tiempo de inicio antes de decrementarlo
beeptimestamp = millis(); //timestamp beeper
waitingturno_timer = millis(); //timestamp turno A
myState = turno_a;
}
}
break;
case turno_a:
// ciclo turno A
if (TimePeriodIsOver(waitingturno_timer, 1000)) {
// tick cada 1 segundo
tiempo_turno--;
dbg("Tiempo restante A", tiempo_turno);
display.showString("A", 1, 0);
display.showNumber(tiempo_turno, false, 3, 1);
if (tiempo_turno == 0) {
beep_req = true;
tiempo_preparacion = duracion_preparacion; //init duración intermedio
dbg("Tiempo restante intermedio", tiempo_preparacion);
display.showString("PR", 2, 0); //para mostrar el tiempo de inicio antes de decrementarlo
display.showNumber(tiempo_preparacion, false, 2, 2); //para mostrar el tiempo de inicio antes de decrementarlo
beeptimestamp = millis(); //timestamp beeper
waiting10_timer = millis(); //timestamp para inicio de preparación
myState = intermedio_10;
}
}
break;
case intermedio_10:
// timer de preparacion linea tiro cambio de turno
if (TimePeriodIsOver(waiting10_timer, 1000)) {
// tick cada 1 segundo
tiempo_preparacion--;
dbg("Tiempo restante intermedio", tiempo_preparacion);
display.showString("PR", 2, 0);
display.showNumber(tiempo_preparacion, false, 2, 2);
if (tiempo_preparacion == 0) {
beep_req = true;
tiempo_turno = duracion_turno; //init duración turno
display.showString("B", 1, 0); //para mostrar el tiempo de inicio antes de decrementarlo
display.showNumber(tiempo_turno, false, 3, 1); //para mostrar el tiempo de inicio antes de decrementarlo
beeptimestamp = millis(); //timestamp beeper
waitingturno_timer = millis(); //timestamp turno B
myState = turno_b;
}
}
break;
case turno_b:
// ciclo turno B
if (TimePeriodIsOver(waitingturno_timer, 1000)) {
// tick cada 1 segundo
tiempo_turno--;
dbg("Tiempo restante B", tiempo_turno);
display.showString("B", 1, 0);
display.showNumber(tiempo_turno, false, 3, 1);
if (tiempo_turno == 0) {
beep_req = true;
display.clear();
beeptimestamp = millis(); //timestamp beeper
triplebeeptimestamp = millis(); //timestamp triple beeper
myState = triple_beep;
}
}
break;
case triple_beep:
if (TimePeriodIsOver(triplebeeptimestamp, 2000)) {
beep_req = true;
dbg("conteo beeps", beepcount);
dbg("triple beep_req end", millis());
beepcount++;
beeptimestamp = millis();
if (beepcount >= 2) { //2 beeps ya que el primer beep es llamado antes del cambio de estado
myState = standby;
// if (duracion_turno == duracion_turno_torneo) {
// myState = standby;
// } else {
// myState = modo_practica;
break;
}
}
break; // end
}
beeperv2(); //funcion recurrente beeper
}
// easy to use helper-function for non-blocking timing
boolean TimePeriodIsOver(unsigned long& startOfPeriod, unsigned long TimePeriod) {
unsigned long currentMillis = millis();
if (currentMillis - startOfPeriod >= TimePeriod) {
// more time than TimePeriod has elapsed since last time if-condition was true
startOfPeriod = currentMillis; // a new period starts right here so set new starttime
return true;
} else return false; // actual TimePeriod is NOT yet over
}
// beeper function, 1s on, 1s off
void beeperv2() {
if (beep_req == true) {
digitalWrite(LED_BUILTIN, HIGH);
digitalWrite(pin_buzzer, HIGH);
//beeping = true;
//tone(pin_buzzer, 400, 1000);
dbgi("beep_req start", beeptimestamp, 100);
beep_req = false;
} else if (TimePeriodIsOver(beeptimestamp, 1000)) {
digitalWrite(LED_BUILTIN, LOW);
digitalWrite(pin_buzzer, LOW);
// beeping = false;
}
}