// ROFLS+ Arena Controller #include #include // Required for wifi_tx_info_t #include // automatically installed for ESP32 boards, I think? #include // automatically installed for ESP32 boards #include // https://github.com/avdwebLibraries/avdweb_Switch #include // https://github.com/RobTillaart/CountDown #include // https://github.com/RobTillaart/StopWatch_RT // Hardware connections // Buttons: #define START_BTN_PIN 1 #define PAUSE_BTN_PIN 3 #define PIT_BTN_PIN 5 #define RESET_BTN_PIN 7 // Switches: #define RUMBLE_SWITCH_PIN 9 #define PIT_ENABLE_SWITCH_PIN 11 #define LIGHT_SWITCH_PIN 12 #define TESTmode_SWITCH_PIN 4 // LEDs: #define LIGHT_STATUS_LED 10 #define AUTOPIT_STATUS_LED 13 #define MODE_STATUS_LED 14 // Relays: #define PIT_RELEASE_PIN 37 #define LIGHT_PIN 39 #define UNUSED_RELAY3_PIN 35 #define UNUSED_RELAY4_PIN 33 const byte relayOnState = LOW; const byte relayOffState = HIGH; // define buttons and switches // // constructor Switch // Parameters: byte _pin, byte PinMode = 5, bool polarity = 0, unsigned long debouncePeriod = 50, unsigned long longPressPeriod = 300, unsigned long doubleClickPeriod = 250, unsigned long deglitchPeriod = 10 Switch buttonSTART = Switch(START_BTN_PIN, INPUT_PULLUP, LOW, 50, 1000); Switch buttonPAUSE = Switch(PAUSE_BTN_PIN); Switch buttonPIT = Switch(PIT_BTN_PIN, INPUT_PULLUP, LOW, 50, 1000); Switch buttonRESET = Switch(RESET_BTN_PIN, INPUT_PULLUP, LOW, 50, 1000); Switch switchRUMBLE = Switch(RUMBLE_SWITCH_PIN); Switch switchPIT = Switch(PIT_ENABLE_SWITCH_PIN); Switch switchLIGHT = Switch(LIGHT_SWITCH_PIN); Switch switchTESTmode = Switch(TESTmode_SWITCH_PIN); bool buttonSTARTvar = false; bool buttonSTARTforced = false; bool buttonPAUSEvar = false; bool buttonPITvar = false; bool buttonPIThold = false; bool buttonRESETvar = false; bool buttonREDTEAMvar = false; bool buttonREDTEAMtapout = false; bool buttonBLUETEAMvar = false; bool buttonBLUETEAMtapout = false; unsigned long PITopenTimestamp = 0; bool PITreleased = false; 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); // Rumble stopwatch StopWatch rumbleTIME(StopWatch::SECONDS); int CLOCK_LED_BRIGHTNESS = 64; // 64 is okay int BLINK_COUNTER_REDTEAM = 0; int BLINK_COUNTER_BLUETEAM = 0; 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: // A Arena //uint8_t broadcastAddressClock1[] = {0x48, 0x27, 0xE2, 0x5D, 0xB6, 0x84}; //uint8_t broadcastAddressClock2[] = {0xD8, 0x3B, 0xDA, 0xC8, 0x95, 0x42}; // B Arena uint8_t broadcastAddressClock1[] = {0xD8, 0x3B, 0xDA, 0xC9, 0x49, 0xC6}; uint8_t broadcastAddressClock2[] = {0xD8, 0x3B, 0xDA, 0xC8, 0xFF, 0xFA}; // struct for clock data typedef struct struct_message_Clock { int sendMinutes; int sendSeconds; int sendREDchannel; int sendGREENchannel; int sendBLUEchannel; int sendBrightness; } struct_message_Clock; struct_message_Clock sendClockDATA; // send config, pilot buttons: // A Arena //uint8_t broadcastAddressREDTEAMbutton[] = {0x84, 0xFC, 0xE6, 0xC7, 0x23, 0x14}; //uint8_t broadcastAddressBLUETEAMbutton[] = {0x84, 0xFC, 0xE6, 0xC7, 0x1A, 0x02}; // B Arena uint8_t broadcastAddressREDTEAMbutton[] = {0xD8, 0x3B, 0xDA, 0xC8, 0x95, 0x58}; uint8_t broadcastAddressBLUETEAMbutton[] = {0xD8, 0x3B, 0xDA, 0xC8, 0x95, 0x1C}; // Structure for sending data typedef struct struct_message_send { bool TEAMLED; // LED state } struct_message_send; // Create struct_message instances for sending to both receivers struct_message_send sendToREDTEAMbutton; struct_message_send sendToBLUETEAMbutton; // ESP-Now stuff for the Pit controller // A Arena //uint8_t broadcastAddressPitController[] = {0x84, 0xFC, 0xE6, 0xC7, 0x19, 0xDE}; // B Arena uint8_t broadcastAddressPitController[] = {0xDE, 0xAD, 0xBE, 0xEF, 0xB0, 0x0B}; // Structure for sending data typedef struct struct_message_pit { bool PIT; // LED state } struct_message_pit; struct_message_pit sendToPitController; esp_now_peer_info_t peerInfo; // callback when data is sent void OnDataSent(const wifi_tx_info_t *tx_info, esp_now_send_status_t status) { Serial.print("\r\nLast Packet Send Status:\t"); Serial.println(status == ESP_NOW_SEND_SUCCESS ? "Delivery Success" : "Delivery Fail"); } //------------------------------------------------------------------------------------ // receive config // Structure example to send data // Must match the receiver structure typedef struct struct_message_receive { int boardID; bool buttonSTART; bool buttonSTARTforced; bool buttonPAUSE; bool buttonPIT; bool buttonPIThold; bool buttonRESET; bool buttonREDTEAM; bool buttonREDTEAMtapout; bool buttonBLUETEAM; bool buttonBLUETEAMtapout; } struct_message_receive; // Create a struct_message called receiveDATA struct_message_receive receiveDATA; // callback function that will be executed when data is received void OnDataRecv(const uint8_t * mac, const uint8_t *incomingData, int len) { memcpy(&receiveDATA, incomingData, sizeof(receiveDATA)); // only fill the data to the right vars switch (receiveDATA.boardID) { case 0: // referee remote buttonSTARTvar = receiveDATA.buttonSTART; buttonSTARTforced = receiveDATA.buttonSTARTforced; buttonPAUSEvar = receiveDATA.buttonPAUSE; buttonPITvar = receiveDATA.buttonPIT; buttonPIThold = receiveDATA.buttonPIThold; buttonRESETvar = receiveDATA.buttonRESET; 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; } } // send data to clock: void sendTimeDisplay(int MINUTES, int SECONDS, int RED, int GREEN, int BLUE, int BRIGHTNESS) { // only send data if there was a change if ((sendClockDATA.sendMinutes != MINUTES) || (sendClockDATA.sendSeconds != SECONDS) || (sendClockDATA.sendREDchannel != RED) || (sendClockDATA.sendGREENchannel != GREEN) || (sendClockDATA.sendBLUEchannel != BLUE) || (sendClockDATA.sendBrightness != BRIGHTNESS)) { sendClockDATA.sendMinutes = MINUTES; sendClockDATA.sendSeconds = SECONDS; sendClockDATA.sendREDchannel = RED; sendClockDATA.sendGREENchannel = GREEN; sendClockDATA.sendBLUEchannel = BLUE; sendClockDATA.sendBrightness = BRIGHTNESS; // 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; //------------------------------------------------------------------------------------ void setup() { Serial.begin(115200); // set relay outputs: pinMode(PIT_RELEASE_PIN, OUTPUT); 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(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 pinMode(AUTOPIT_STATUS_LED, OUTPUT); digitalWrite(AUTOPIT_STATUS_LED, LOW); pinMode(MODE_STATUS_LED, OUTPUT); digitalWrite(MODE_STATUS_LED, LOW); // Set device as a Wi-Fi Station WiFi.mode(WIFI_STA); // Init ESP-NOW if (esp_now_init() != ESP_OK) { Serial.println("Error initializing ESP-NOW"); return; } //------------------------------------------------------------------------------------ // ESP Now send part: // Once ESPNow is successfully Init, we will register for Send CB to // get the status of Transmitted packet esp_now_register_send_cb(OnDataSent); // 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 Clock1 peer"); return; } // 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); peerInfo.channel = 0; peerInfo.encrypt = false; if (esp_now_add_peer(&peerInfo) != ESP_OK) { Serial.println("Failed to add receiver 1"); return; } // Register Blue Team Button peer memcpy(peerInfo.peer_addr, broadcastAddressBLUETEAMbutton, 6); peerInfo.channel = 0; peerInfo.encrypt = false; if (esp_now_add_peer(&peerInfo) != ESP_OK) { Serial.println("Failed to add receiver 2"); return; } // Register Pit controller peer memcpy(peerInfo.peer_addr, broadcastAddressPitController, 6); peerInfo.channel = 0; peerInfo.encrypt = false; if (esp_now_add_peer(&peerInfo) != ESP_OK) { Serial.println("Failed to add receiver 3"); return; } // Initialize both Team button LED states and pit controller state sendToREDTEAMbutton.TEAMLED = false; sendToBLUETEAMbutton.TEAMLED = false; sendToPitController.PIT = false; // reset remote button LEDs and pit esp_now_send(broadcastAddressREDTEAMbutton, (uint8_t *)&sendToREDTEAMbutton, sizeof(sendToREDTEAMbutton)); esp_now_send(broadcastAddressBLUETEAMbutton, (uint8_t *)&sendToBLUETEAMbutton, sizeof(sendToBLUETEAMbutton)); esp_now_send(broadcastAddressPitController, (uint8_t *)&sendToPitController, sizeof(sendToPitController)); //------------------------------------------------------------------------------------ // ESP Now receive part: // Once ESPNow is successfully Init, we will register for recv CB to // get recv packer info esp_now_register_recv_cb(esp_now_recv_cb_t(OnDataRecv)); } //---------------------------------------------------------------------------------------- // Handler for the START button logic. void handleStartButton() { if (buttonSTARTvar) { buttonSTARTvar = false; // 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; ARENA_READY = true; REDTEAM_LED(true); BLUETEAM_LED(true); ReadyCountDown.start(countdownToFightTIME); } else if (buttonREDTEAMvar && buttonBLUETEAMvar && !switchRUMBLE.on()) { buttonREDTEAMvar = false; buttonBLUETEAMvar = false; ARENA_READY = true; REDTEAM_LED(true); BLUETEAM_LED(true); ReadyCountDown.start(countdownToFightTIME); } } else { // Resume (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; ARENA_READY = true; REDTEAM_LED(true); BLUETEAM_LED(true); ReadyCountDown.start(countdownToFightTIME); } else if (switchRUMBLE.on()) { buttonREDTEAMvar = false; buttonBLUETEAMvar = false; resumeFight = true; countdownPAUSED = false; ARENA_READY = true; REDTEAM_LED(true); BLUETEAM_LED(true); ReadyCountDown.start(countdownToFightTIME); } } } } // Handler for the forced start button logic. void handleForcedStartButton() { if (buttonSTARTforced) { buttonSTARTforced = false; if (!FightCountDown.isRunning() && !rumbleTIME.isRunning() && countdownPAUSED == false) { buttonREDTEAMvar = false; buttonBLUETEAMvar = false; ARENA_READY = true; REDTEAM_LED(true); BLUETEAM_LED(true); ReadyCountDown.start(countdownToFightTIME); } else { buttonREDTEAMvar = false; buttonBLUETEAMvar = false; resumeFight = true; countdownPAUSED = false; ARENA_READY = true; REDTEAM_LED(true); BLUETEAM_LED(true); ReadyCountDown.start(countdownToFightTIME); } } } // Handler for the pause button logic. void handlePauseButton() { if (buttonPAUSEvar) { buttonPAUSEvar = false; if (FightCountDown.isRunning()) { countdownPAUSED = true; FightCountDown.stop(); } if (rumbleTIME.isRunning()) { countdownPAUSED = true; rumbleTIME.stop(); } } } // Handler for the pit button logic. void handlePitButton() { if (buttonPITvar) { buttonPITvar = false; buttonPIThold = false; openPITmanually(); } } // 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(); } else if (rumbleTIME.elapsed() >= PITreleaseTime && switchRUMBLE.on() && switchPIT.on() && buttonPIThold == false) { openPIT(); } } } // 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()) { 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; } } } // 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); } else if (switchRUMBLE.on()) { // Display the Rumble Timer sendTimeDisplay((rumbleTIME.elapsed()/60 % 60), (rumbleTIME.elapsed()%60), 0, 255, 255, CLOCK_LED_BRIGHTNESS); } 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 { // 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(); } }