@@ -1,13 +1,18 @@
// ROFLS+ Arena Controller
# include <WiFi.h>
# include <esp_wifi.h> // Required for wifi_tx_info_t
# include <esp_now.h> // automatically installed for ESP32 boards, I think?
# include <Preferences.h> // automatically installed for ESP32 boards
# include <FastLED.h> // https://fastled.io/
# include <avdweb_Switch.h> // https://github.com/avdwebLibraries/avdweb_Switch
# include <CountDown.h> // https://github.com/RobTillaart/CountDown
# include <StopWatch.h> // https://github.com/RobTillaart/StopWatch_RT
// set Arena, switches the MAC addresses out
// 1 = Arena A (Ant)
// 2 = Arena B (Beetle)
# define ARENA 1
// Hardware connections
// Buttons:
# define START_BTN_PIN 1
@@ -26,7 +31,7 @@
// Relays:
# define PIT_RELEASE_PIN 37
# define LIGHT_PIN 39
# define UNUSED_RELAY3 _PIN 35
# define LIGHT _PIN2 35
# define UNUSED_RELAY4_PIN 33
const byte relayOnState = LOW ;
@@ -58,10 +63,10 @@ bool buttonBLUETEAMtapout = false;
unsigned long PITopenTimestamp = 0 ;
bool PITreleased = false ;
const long PITopenTime = 500 ; // activate solenoid for 500ms
const int countdownTIME = 180 ; // countdown timer length in seconds, actual countdown for the fight
const int countdownToFightTIME = 3 ; // countdown timer length in seconds, "ready" countdown
const int PITreleaseTime = 90 ; // automatic pit release time in seconds until end of countdown
const long PITopenTime = 500 ; // default: 500 activate solenoid for 500ms
const int countdownTIME = 180 ; // default: 180 countdown timer length in seconds, actual countdown for the fight
const int countdownToFightTIME = 3 ; // default: 3 countdown timer length in seconds, "ready" countdown
const int PITreleaseTime = 90 ; // default: 90 automatic pit release time in seconds until end of countdown
bool countdownPAUSED = false ;
CountDown FightCountDown ( CountDown : : SECONDS ) ;
CountDown ReadyCountDown ( CountDown : : SECONDS ) ;
@@ -69,7 +74,7 @@ CountDown ReadyCountDown(CountDown::SECONDS);
// Rumble stopwatch
StopWatch rumbleTIME ( StopWatch : : SECONDS ) ;
int CLOCK_LED_BRIGHTNESS = 32 ; // 64 is okay
int CLOCK_LED_BRIGHTNESS = 128 ; // 64 is okay
int BLINK_COUNTER_REDTEAM = 0 ;
int BLINK_COUNTER_BLUETEAM = 0 ;
@@ -79,11 +84,21 @@ int BLINK_INTERVAL = 200;
bool ARENA_READY = false ;
bool REDTEAM_READY = false ;
bool BLUETEAM_READY = false ;
bool resumeFight = false ;
//------------------------------------------------------------------------------------
// ESP-NOW config
// send config, Clock:
uint8_t broadcastAddressClock [ ] = { 0x48 , 0x27 , 0xE2 , 0x5D , 0xB6 , 0x84 } ;
# if ARENA == 1
// A Arena
uint8_t broadcastAddressClock1 [ ] = { 0x48 , 0x27 , 0xE2 , 0x5D , 0xB6 , 0x84 } ;
uint8_t broadcastAddressClock2 [ ] = { 0xD8 , 0x3B , 0xDA , 0xC9 , 0x49 , 0xC6 } ;
# elif ARENA == 2
// B Arena
uint8_t broadcastAddressClock1 [ ] = { 0xD8 , 0x3B , 0xDA , 0xC9 , 0x49 , 0xC6 } ;
uint8_t broadcastAddressClock2 [ ] = { 0xD8 , 0x3B , 0xDA , 0xC8 , 0xFF , 0xFA } ;
# endif
// struct for clock data
typedef struct struct_message_Clock {
int sendMinutes ;
@@ -98,8 +113,15 @@ typedef struct struct_message_Clock {
struct_message_Clock sendClockDATA ;
// send config, pilot buttons:
# if ARENA == 1
// A Arena
uint8_t broadcastAddressREDTEAMbutton [ ] = { 0x84 , 0xFC , 0xE6 , 0xC7 , 0x23 , 0x14 } ;
uint8_t broadcastAddressBLUETEAMbutton [ ] = { 0x84 , 0xFC , 0xE6 , 0xC7 , 0x1A , 0x02 } ;
# elif ARENA == 2
// B Arena
uint8_t broadcastAddressREDTEAMbutton [ ] = { 0xD8 , 0x3B , 0xDA , 0xC8 , 0x95 , 0x58 } ;
uint8_t broadcastAddressBLUETEAMbutton [ ] = { 0xD8 , 0x3B , 0xDA , 0xC8 , 0x95 , 0x1C } ;
# endif
// Structure for sending data
typedef struct struct_message_send {
@@ -111,7 +133,16 @@ struct_message_send sendToREDTEAMbutton;
struct_message_send sendToBLUETEAMbutton ;
// ESP-Now stuff for the Pit controller
# if ARENA == 1
// A Arena
uint8_t broadcastAddressPitController [ ] = { 0x84 , 0xFC , 0xE6 , 0xC7 , 0x19 , 0xDE } ;
# elif ARENA == 2
// B Arena
uint8_t broadcastAddressPitController [ ] = { 0xDE , 0xAD , 0xBE , 0xEF , 0xB0 , 0x0B } ;
# endif
// Test Pit controller
//uint8_t broadcastAddressPitController[] = {0x94, 0xA9, 0x90, 0x0B, 0x21, 0x64};
// Structure for sending data
typedef struct struct_message_pit {
bool PIT ; // LED state
@@ -122,7 +153,7 @@ esp_now_peer_info_t peerInfo;
// callback when data is sent
void OnDataSent ( const uint8_t * mac_addr , esp_now_send_status_t status ) {
void OnDataSent ( const wifi_tx_info_t * tx_info , esp_now_send_status_t status ) {
Serial . print ( " \r \n Last Packet Send Status: \t " ) ;
Serial . println ( status = = ESP_NOW_SEND_SUCCESS ? " Delivery Success " : " Delivery Fail " ) ;
}
@@ -163,13 +194,31 @@ void OnDataRecv(const uint8_t * mac, const uint8_t *incomingData, int len) {
break ;
case 1 :
// RED team button
// ignore button input if in rumble mode
if ( ! switchRUMBLE . on ( ) ) {
// ignore button press while in fight
if ( ! FightCountDown . isRunning ( ) ) {
buttonREDTEAMvar = receiveDATA . buttonREDTEAM ;
}
// ignore tapout while not in fight
if ( FightCountDown . isRunning ( ) ) {
buttonREDTEAMtapout = receiveDATA . buttonREDTEAMtapout ;
}
}
break ;
case 2 :
// BLUE team button
// ignore button input if in rumble mode
if ( ! switchRUMBLE . on ( ) ) {
// ignore button press while in fight
if ( ! FightCountDown . isRunning ( ) ) {
buttonBLUETEAMvar = receiveDATA . buttonBLUETEAM ;
}
// ignore tapout while not in fight
if ( FightCountDown . isRunning ( ) ) {
buttonBLUETEAMtapout = receiveDATA . buttonBLUETEAMtapout ;
}
}
break ;
}
}
@@ -184,11 +233,30 @@ void sendTimeDisplay(int MINUTES, int SECONDS, int RED, int GREEN, int BLUE, int
sendClockDATA . sendGREENchannel = GREEN ;
sendClockDATA . sendBLUEchannel = BLUE ;
sendClockDATA . sendBrightness = BRIGHTNESS ;
// actually send it
esp_err_t result = esp_now_send ( broadcastAddressClock , ( uint8_t * ) & sendClockDATA , sizeof ( sendClockDATA ) ) ;
// actually send it to first clock
esp_err_t result1 = esp_now_send ( broadcastAddressClock1 , ( uint8_t * ) & sendClockDATA , sizeof ( sendClockDATA ) ) ;
// actually send it to second clock
esp_err_t result2 = esp_now_send ( broadcastAddressClock2 , ( uint8_t * ) & sendClockDATA , sizeof ( sendClockDATA ) ) ;
}
}
// Global Vars for tap out states
bool redTapOutActive = false ;
unsigned long redTapOutStartTime = 0 ;
int redTapOutStage = 0 ;
bool blueTapOutActive = false ;
unsigned long blueTapOutStartTime = 0 ;
int blueTapOutStage = 0 ;
// Global Vars for Fight end
bool fightStarted = false ;
bool fightEnded = false ;
const int END_BLINK_COUNT = 3 ;
const unsigned long END_BLINK_INTERVAL = 500 ; // in milliseconds
int endBlinkTransitions = 0 ;
bool endBlinkState = false ;
unsigned long lastEndBlinkTime = 0 ;
//------------------------------------------------------------------------------------
@@ -199,13 +267,13 @@ void setup() {
digitalWrite ( PIT_RELEASE_PIN , relayOffState ) ;
pinMode ( LIGHT_PIN , OUTPUT ) ;
digitalWrite ( LIGHT_PIN , LOW ) ; // have it by default on, to prevent flickering, needs a better fix tho
pinMode ( UNUSED_RELAY3 _PIN, OUTPUT ) ;
digitalWrite ( UNUSED_RELAY3 _PIN, relayOffState ) ;
pinMode ( LIGHT _PIN2 , OUTPUT ) ;
digitalWrite ( LIGHT _PIN2 , LOW ) ; // have it by default on, to prevent flickering, needs a better fix tho
pinMode ( UNUSED_RELAY4_PIN , OUTPUT ) ;
digitalWrite ( UNUSED_RELAY4_PIN , relayOffState ) ;
// set status LED outputs:
pinMode ( LIGHT_STATUS_LED , OUTPUT ) ;
digitalWrite ( LIGHT_PIN , HIGH ) ; // have it by default on, to prevent flickering, needs a better fix tho
digitalWrite ( LIGHT_STATUS_LED , HIGH ) ;
pinMode ( AUTOPIT_STATUS_LED , OUTPUT ) ;
digitalWrite ( AUTOPIT_STATUS_LED , LOW ) ;
pinMode ( MODE_STATUS_LED , OUTPUT ) ;
@@ -225,17 +293,27 @@ void setup() {
// get the status of Transmitted packet
esp_now_register_send_cb ( OnDataSent ) ;
// Register clock peer
memcpy ( peerInfo . peer_addr , broadcastAddressClock , 6 ) ;
// Register first clock peer
memcpy ( peerInfo . peer_addr , broadcastAddressClock1 , 6 ) ;
peerInfo . channel = 0 ;
peerInfo . encrypt = false ;
// Add peer
if ( esp_now_add_peer ( & peerInfo ) ! = ESP_OK ) {
Serial . println ( " Failed to add peer " ) ;
Serial . println ( " Failed to add Clock1 peer " ) ;
return ;
}
esp_err_t result = esp_now_send ( broadcastAddressClock , ( uint8_t * ) & sendClockDATA , sizeof ( sendClockDATA ) ) ;
// 2) Register second clock peer
memcpy ( peerInfo . peer_addr , broadcastAddressClock2 , 6 ) ;
// peerInfo.channel & peerInfo.encrypt already set above
if ( esp_now_add_peer ( & peerInfo ) ! = ESP_OK ) {
Serial . println ( " Failed to add Clock2 peer " ) ;
return ;
}
// send initial data (I think that doesn't work but meh...)
esp_err_t result1 = esp_now_send ( broadcastAddressClock1 , ( uint8_t * ) & sendClockDATA , sizeof ( sendClockDATA ) ) ;
esp_err_t result2 = esp_now_send ( broadcastAddressClock2 , ( uint8_t * ) & sendClockDATA , sizeof ( sendClockDATA ) ) ;
// Register Red Team Button peer
memcpy ( peerInfo . peer_addr , broadcastAddressREDTEAMbutton , 6 ) ;
@@ -280,81 +358,85 @@ void setup() {
esp_now_register_recv_cb ( esp_now_recv_cb_t ( OnDataRecv ) ) ;
}
void loop ( ) {
// poll all the switch/button inputs
pollInput ( ) ;
// deactivate solenoids if needed
checkPIT ( ) ;
// Arena Light
arenaLIGHT ( ) ;
// update status LEDs
statusLEDs ( ) ;
// update Teambutton LEDs
updateTEAMLEDs ( ) ;
//----------------------------------------------------------------------------------------
// start button logic
// Handler for the START button logic.
void handleStartButton ( ) {
if ( buttonSTARTvar ) {
buttonSTARTvar = false ;
// start logic
// Fresh start: no countdown is running, and the fight hasn't been paused.
if ( ! FightCountDown . isRunning ( ) & & ! rumbleTIME . isRunning ( ) & & countdownPAUSED = = false ) {
if ( switchRUMBLE . on ( ) ) {
buttonREDTEAMvar = false ;
buttonBLUETEAMvar = false ;
//rumbleTIME.start();
ARENA_READY = true ;
REDTEAM_LED ( true ) ;
BLUETEAM_LED ( true ) ;
ReadyCountDown . start ( countdownToFightTIME ) ;
} else if ( buttonREDTEAMvar & & buttonBLUETEAMvar & & ! switchRUMBLE . on ( ) ) {
}
else if ( buttonREDTEAMvar & & buttonBLUETEAMvar & & ! switchRUMBLE . on ( ) ) {
buttonREDTEAMvar = false ;
buttonBLUETEAMvar = false ;
//FightCountDown.start(countdownTIME);
ARENA_READY = true ;
REDTEAM_LED ( true ) ;
BLUETEAM_LED ( true ) ;
ReadyCountDown . start ( countdownToFightTIME ) ;
}
}
else {
// r esume logic
// R esume (unpause) branch: instead of calling FightCountDown.resume() immediately,
// we want to display ReadyCountDown first.
if ( buttonREDTEAMvar & & buttonBLUETEAMvar & & ! switchRUMBLE . on ( ) ) {
buttonREDTEAMvar = false ;
buttonBLUETEAMvar = false ;
resumeFight = true ; // Mark that we want to resume later.
countdownPAUSED = false ;
FightCountDown . resume ( ) ;
} else if ( switchRUMBLE . on ( ) ) {
ARENA_READY = true ;
REDTEAM_LED ( true ) ;
BLUETEAM_LED ( true ) ;
ReadyCountDown . start ( countdownToFightTIME ) ;
}
else if ( switchRUMBLE . on ( ) ) {
buttonREDTEAMvar = false ;
buttonBLUETEAMvar = false ;
resumeFight = true ;
countdownPAUSED = false ;
rumbleTIME . start ( ) ;
ARENA_READY = true ;
REDTEAM_LED ( true ) ;
BLUETEAM_LED ( true ) ;
ReadyCountDown . start ( countdownToFightTIME ) ;
}
}
}
// forced start of countdown
}
// Handler for the forced start button logic.
void handleForcedStartButton ( ) {
if ( buttonSTARTforced ) {
buttonSTARTforced = false ;
if ( ! FightCountDown . isRunning ( ) & & ! rumbleTIME . isRunning ( ) & & countdownPAUSED = = false ) {
buttonREDTEAMvar = false ;
buttonBLUETEAMvar = false ;
if ( ! switchRUMBLE . on ( ) ) {
//FightCountDown.start(countdownTIME);
ARENA_READY = true ;
REDTEAM_LED ( true ) ;
BLUETEAM_LED ( true ) ;
ReadyCountDown . start ( countdownToFightTIME ) ;
} else if ( switchRUMBLE . on ( ) ) {
//rumbleTIME.start();
ARENA_READY = true ;
ReadyCountDown . start ( countdownToFightTIME ) ;
}
}
else {
// forced resume logic
buttonREDTEAMvar = false ;
buttonBLUETEAMvar = false ;
resumeFight = true ;
countdownPAUSED = false ;
if ( ! switchRUMBLE . on ( ) ) {
FightCountDown . resume ( ) ;
} else if ( switchRUMBLE . on ( ) ) {
rumbleTIME . start ( ) ;
ARENA_READY = true ;
REDTEAM_LED ( true ) ;
BLUETEAM_LED ( true ) ;
ReadyCountDown . start ( countdownToFightTIME ) ;
}
}
}
// pause button logic
// Handler for the pause button logic.
void handlePauseButton ( ) {
if ( buttonPAUSEvar ) {
buttonPAUSEvar = false ;
if ( FightCountDown . isRunning ( ) ) {
@@ -366,90 +448,230 @@ void loop() {
rumbleTIME . stop ( ) ;
}
}
// pit button logic
}
// Handler for the pit button logic.
void handlePitButton ( ) {
if ( buttonPITvar ) {
buttonPITvar = false ;
buttonPIThold = false ;
openPITmanually ( ) ;
}
// automatic pit release
if ( FightCountDown . remaining ( ) < = PITreleaseTime & & FightCountDown . remaining ( ) ! = 0 & & ! switchRUMBLE . on ( ) & & switchPIT . on ( ) & & buttonPIThold = = false ) {
}
// check for automatic pit release.
void handleAutoPitRelease ( ) {
// Only release the pit if it hasn't already been released.
if ( ! PITreleased ) {
if ( FightCountDown . remaining ( ) < = PITreleaseTime & & FightCountDown . remaining ( ) ! = 0 & &
! switchRUMBLE . on ( ) & & switchPIT . on ( ) & & buttonPIThold = = false ) {
openPIT ( ) ;
}
if ( rumbleTIME . elapsed ( ) > = PITreleaseTime & & switchRUMBLE . on ( ) & & switchPIT . on ( ) & & buttonPIThold = = false ) {
else if ( rumbleTIME . elapsed ( ) > = PITreleaseTime & & switchRUMBLE . on ( ) & &
switchPIT . on ( ) & & buttonPIThold = = false ) {
openPIT ( ) ;
}
// tap out logic
if ( buttonREDTEAMtapout & & ! switchRUMBLE . on ( ) ) {
countdownPAUSED = true ;
FightCountDown . stop ( ) ;
REDTEAM_LED ( true ) ;
sendTimeDisplay ( 99 , 0 , 255 , 0 , 0 , CLOCK_LED_BRIGHTNESS ) ;
delay ( 1500 ) ;
sendTimeDisplay ( 0 , 99 , 255 , 0 , 0 , CLOCK_LED_BRIGHTNESS ) ;
delay ( 1500 ) ;
sendTimeDisplay ( ( FightCountDown . remaining ( ) / 60 % 60 ) , ( FightCountDown . remaining ( ) % 60 ) , 255 , 0 , 0 , CLOCK_LED_BRIGHTNESS ) ;
delay ( 7000 ) ;
}
if ( buttonBLUETEAMtapout & & ! switchRUMBLE . on ( ) ) {
countdownPAUSED = true ;
FightCountDown . stop ( ) ;
BLUETEAM_LED ( true ) ;
sendTimeDisplay ( 99 , 0 , 0 , 0 , 255 , CLOCK_LED_BRIGHTNESS ) ;
delay ( 1500 ) ;
sendTimeDisplay ( 0 , 99 , 0 , 0 , 255 , CLOCK_LED_BRIGHTNESS ) ;
delay ( 1500 ) ;
sendTimeDisplay ( ( FightCountDown . remaining ( ) / 60 % 60 ) , ( FightCountDown . remaining ( ) % 60 ) , 0 , 0 , 255 , CLOCK_LED_BRIGHTNESS ) ;
delay ( 1500 ) ;
}
// reset button logic
if ( buttonRESETvar ) {
buttonRESETvar = false ;
PITreleased = false ;
ESP . restart ( ) ;
}
// Ready Countdown to Fight Countdown transition
// Handler for transitioning from ReadyCountDown to the fight (or rumble) countdown.
void handleCountdownTransition ( ) {
if ( ReadyCountDown . remaining ( ) = = 0 & & ARENA_READY ) {
ARENA_READY = false ;
REDTEAM_LED ( false ) ;
BLUETEAM_LED ( false ) ;
if ( ! switchRUMBLE . on ( ) ) {
FightCountDown . start ( countdownTIME ) ;
} else if ( switchRUMBLE . on ( ) ) {
if ( resumeFight ) {
FightCountDown . resume ( ) ; // Resume the paused fight countdown.
resumeFight = false ;
}
else {
FightCountDown . start ( countdownTIME ) ; // Fresh start.
fightStarted = true ;
}
}
else if ( switchRUMBLE . on ( ) ) {
// For the rumble branch, you may decide if a resume is relevant.
// Here we simply start the rumble timer as before.
rumbleTIME . start ( ) ;
resumeFight = false ;
}
}
}
// If either team has tapped out, skip updating the display .
if ( buttonREDTEAMtapout | | buttonBLUETEAMtapout ) {
return ;
}
// Display the Ready CountDown timer if it is running.
// The display update logic now considers all conditions in order .
void updateDisplay ( ) {
if ( ReadyCountDown . isRunning ( ) ) {
// Display Ready Countdown in Yellow
sendTimeDisplay ( ( ReadyCountDown . remaining ( ) / 60 % 60 ) , ( ReadyCountDown . remaining ( ) % 60 ) , 255 , 165 , 0 , CLOCK_LED_BRIGHTNESS ) ;
return ;
}
// When the RUMBLE switch is active, always show the rumble timer.
if ( switchRUMBLE . on ( ) ) {
} else if ( switchRUMBLE . on ( ) ) {
// Display the Rumble Timer
sendTimeDisplay ( ( rumbleTIME . elapsed ( ) / 60 % 60 ) , ( rumbleTIME . elapsed ( ) % 60 ) , 0 , 255 , 255 , CLOCK_LED_BRIGHTNESS ) ;
return ;
}
// If the fight countdown isn't running and isn't paused, determine which countdown color to use.
if ( ! FightCountDown . isRunning ( ) & & ! countdownPAUSED ) {
// Choose green if both buttons are active; otherwise, choose magenta.
} else if ( ! FightCountDown . isRunning ( ) & & ! countdownPAUSED ) {
// Choose green if both team buttons are active; otherwise, choose magenta.
if ( buttonREDTEAMvar & & buttonBLUETEAMvar ) {
sendTimeDisplay ( ( countdownTIME / 60 % 60 ) , ( countdownTIME % 60 ) , 0 , 255 , 0 , CLOCK_LED_BRIGHTNESS ) ;
} else {
sendTimeDisplay ( ( countdownTIME / 60 % 60 ) , ( countdownTIME % 60 ) , 255 , 0 , 255 , CLOCK_LED_BRIGHTNESS ) ;
}
} else {
// Otherwise, show the fight countdown timer.
// choose yellow if countdown is paused and both teams aren't ready
if ( countdownPAUSED & & ( ! buttonREDTEAMvar | | ! buttonBLUETEAMvar ) ) {
sendTimeDisplay ( ( FightCountDown . remaining ( ) / 60 % 60 ) , ( FightCountDown . remaining ( ) % 60 ) , 255 , 165 , 0 , CLOCK_LED_BRIGHTNESS ) ;
} else {
// Display the countdown in green
sendTimeDisplay ( ( FightCountDown . remaining ( ) / 60 % 60 ) , ( FightCountDown . remaining ( ) % 60 ) , 0 , 255 , 0 , CLOCK_LED_BRIGHTNESS ) ;
}
}
}
// endless cycling for Red Team tap-out.
void handleRedTapOut ( ) {
// When the red tap-out is first triggered.
if ( ! redTapOutActive & & buttonREDTEAMtapout & & ! switchRUMBLE . on ( ) & & ! blueTapOutActive ) {
buttonREDTEAMtapout = false ;
countdownPAUSED = true ;
FightCountDown . stop ( ) ;
REDTEAM_LED ( true ) ;
redTapOutActive = true ;
redTapOutStartTime = millis ( ) ;
}
// If tap-out is active, continuously update the display in a cycle.
if ( redTapOutActive ) {
// Define a full cycle period of 8'000 ms.
const unsigned long cycleDuration = 8000 ;
// Compute how far into the current cycle we are.
unsigned long cycleTime = ( millis ( ) - redTapOutStartTime ) % cycleDuration ;
if ( cycleTime < 1500 ) {
// Stage 1 (0 - 1500ms): Display first tap-out message.
sendTimeDisplay ( 99 , 0 , 255 , 0 , 0 , CLOCK_LED_BRIGHTNESS ) ;
}
else if ( cycleTime < 3000 ) {
// Stage 2 (1500 - 3000ms): Display second tap-out message.
sendTimeDisplay ( 0 , 99 , 255 , 0 , 0 , CLOCK_LED_BRIGHTNESS ) ;
}
else {
// Stage 3 (3000 - 10'000ms): Show the remaining fight countdown.
sendTimeDisplay ( ( FightCountDown . remaining ( ) / 60 % 60 ) , ( FightCountDown . remaining ( ) % 60 ) , 255 , 0 , 0 , CLOCK_LED_BRIGHTNESS ) ;
}
}
}
// endless cycling for Blue Team tap-out.
void handleBlueTapOut ( ) {
// When the blue tap-out is first triggered.
if ( ! blueTapOutActive & & buttonBLUETEAMtapout & & ! switchRUMBLE . on ( ) & & ! redTapOutActive ) {
buttonBLUETEAMtapout = false ;
countdownPAUSED = true ;
FightCountDown . stop ( ) ;
BLUETEAM_LED ( true ) ;
blueTapOutActive = true ;
blueTapOutStartTime = millis ( ) ;
}
// If tap-out is active, continuously update the display in a cycle.
if ( blueTapOutActive ) {
// Define a full cycle period of 8'000 ms.
const unsigned long cycleDuration = 8000 ;
// Compute the cycle progress.
unsigned long cycleTime = ( millis ( ) - blueTapOutStartTime ) % cycleDuration ;
if ( cycleTime < 1500 ) {
// Stage 1 (0 - 1500ms): Display first tap-out message.
sendTimeDisplay ( 99 , 0 , 0 , 0 , 255 , CLOCK_LED_BRIGHTNESS ) ;
}
else if ( cycleTime < 3000 ) {
// Stage 2 (1500 - 3000ms): Display second tap-out message.
sendTimeDisplay ( 0 , 99 , 0 , 0 , 255 , CLOCK_LED_BRIGHTNESS ) ;
}
else {
// Stage 3 (3000 - 10'000ms): Show the remaining fight countdown.
sendTimeDisplay ( ( FightCountDown . remaining ( ) / 60 % 60 ) , ( FightCountDown . remaining ( ) % 60 ) , 0 , 0 , 255 , CLOCK_LED_BRIGHTNESS ) ;
}
}
}
void handleFightEnd ( ) {
// When the countdown hits 0 and the fight had started, mark it as ended.
if ( ( FightCountDown . remaining ( ) = = 0 ) & & fightStarted ) {
fightEnded = true ;
}
// Only do blinking if the fight has ended.
if ( ! fightEnded ) {
return ; // Skip the rest until fightEnded becomes true.
}
// Each full blink cycle includes an "on" and "off" state.
// Therefore, we count transitions: total transitions = END_BLINK_COUNT * 2.
const int totalTransitions = END_BLINK_COUNT * 2 + 1 ;
// If we haven't completed our full blink sequence, manage timing:
if ( endBlinkTransitions < totalTransitions ) {
unsigned long currentMillis = millis ( ) ;
if ( currentMillis - lastEndBlinkTime > = END_BLINK_INTERVAL ) {
endBlinkState = ! endBlinkState ; // Toggle between on and off states.
endBlinkTransitions + + ; // Count this toggle.
lastEndBlinkTime = currentMillis ; // Reset the timer.
}
// Depending on the blink state, update the display.
// When endBlinkState is true, use the normal brightness.
// When false, replace CLOCK_LED_BRIGHTNESS with 0 to blank the display.
if ( endBlinkState ) {
sendTimeDisplay ( 0 , 0 , 255 , 165 , 0 , CLOCK_LED_BRIGHTNESS ) ;
} else {
sendTimeDisplay ( 0 , 0 , 255 , 165 , 0 , 0 ) ;
}
} else {
// After completing the blink sequence, ensure that the display remains on.
sendTimeDisplay ( 0 , 0 , 255 , 165 , 0 , CLOCK_LED_BRIGHTNESS ) ;
}
}
//----------------------------------------------------------------------------------------
void loop ( ) {
// Poll and update low-level I/O and LEDs.
pollInput ( ) ;
checkPIT ( ) ;
arenaLIGHT ( ) ;
statusLEDs ( ) ;
updateTEAMLEDs ( ) ;
// Process button events.
handleStartButton ( ) ;
handleForcedStartButton ( ) ;
handlePauseButton ( ) ;
handlePitButton ( ) ;
handleAutoPitRelease ( ) ;
// Process tap-out sequences.
if ( ! switchRUMBLE . on ( ) ) {
handleRedTapOut ( ) ;
handleBlueTapOut ( ) ;
}
// Handle reset
if ( buttonRESETvar ) {
buttonRESETvar = false ;
PITreleased = false ;
ESP . restart ( ) ;
}
// Transition from ReadyCountDown to the appropriate fight countdown.
handleCountdownTransition ( ) ;
// End display
handleFightEnd ( ) ;
// When no team is in a tap‐ out sequence and fight hasn't ended, update the display.
if ( ! redTapOutActive & & ! blueTapOutActive & & ! fightEnded ) {
updateDisplay ( ) ;
}
}