15 Commits

Author SHA1 Message Date
Bunkerotter 48384f87c2 fixed some unicode characters 2026-05-06 20:50:07 +02:00
Bunkerotter bba3a5d50b added functionality for a camera controller 2026-05-06 20:47:46 +02:00
Bunkerotter 3d9a1a778c fixed wrong MAC addresses for clocks 2025-09-26 04:00:48 +02:00
Bunkerotter 4d7a43c9d1 added preprocessor statements to switch out MAC Addresses at compile time 2025-09-23 07:10:36 +02:00
Bunkerotter c450cfb7c9 Changed Clock Brightness to 128, added relay 3 to be controlled by the lightswitch 2025-09-22 16:12:14 +02:00
Bunkerotter 7c3dbfab63 added additional MAC addresses for Arena B 2025-09-14 19:46:50 +02:00
Bunkerotter fc472600c4 updated Arena Controller to support two clocks at the same time 2025-09-13 00:27:22 +02:00
Bunkerotter 649cbe63b1 updated libraries, fixed ESP-NOW send data function to work with the new library 2025-09-09 01:09:15 +02:00
Bunkerotter 79c734bd34 fixed a copy and paste error in the manual 2025-05-04 19:33:19 +02:00
Bunkerotter 3e29ab76e2 added Reference Manual, increased Clock Brightness 2025-05-04 19:26:57 +02:00
Bunkerotter d81ab7da89 prevent Tapout while not in fight 2025-04-20 03:16:40 +02:00
Bunkerotter 5e5ff83506 prevent Team buttons being pushed in a running fight 2025-04-20 03:09:39 +02:00
Bunkerotter 606219b8bd implemented 'end screen' to be displayed when the fight timer ends. Cleaned up some comments 2025-04-19 08:48:59 +02:00
Bunkerotter d93b283d2c shifted 'out' from the tapout screen one digit to the right 2025-04-19 06:39:17 +02:00
Bunkerotter c9c4b1c1c5 refactored main loop, added ready countdown to unpause 2025-04-18 10:15:02 +02:00
8 changed files with 589 additions and 138 deletions
@@ -0,0 +1,116 @@
#include <WiFi.h>
#include <esp_wifi.h>
#include <esp_now.h>
const int camPin = 16; // Any GPIO pin
const int pressTime = 250; // 0.25s HIGH pulse
const int gapTime = 250; // Gap between pulses for double press
void doSinglePress() {
digitalWrite(camPin, HIGH);
delay(pressTime);
digitalWrite(camPin, LOW);
}
void doDoublePress() {
doSinglePress();
delay(gapTime);
doSinglePress();
}
// Must match the sender struct exactly
typedef struct struct_message_send {
int boardID;
bool buttonSTART;
bool buttonSTARTforced;
bool buttonPAUSE;
bool buttonPIT;
bool buttonPIThold;
bool buttonRESET;
bool buttonREDTEAM;
bool buttonREDTEAMtapout;
bool buttonBLUETEAM;
bool buttonBLUETEAMtapout;
} struct_message_send;
struct_message_send incoming;
// Recording state variable
bool RECORDING = false;
bool lastRECORDING = false; // for change detection
// LED blink timing
unsigned long lastBlink = 0;
bool ledState = false;
// ESP-NOW receive callback
void onDataRecv(const esp_now_recv_info *info, const uint8_t *data, int len) {
if (len != sizeof(incoming)) return;
memcpy(&incoming, data, sizeof(incoming));
// Only accept packets from boardID 0 (referee remote)
if (incoming.boardID != 0) return;
// START or STARTforced → RECORDING = true
if (incoming.buttonSTART || incoming.buttonSTARTforced) {
RECORDING = true;
}
// RESET → RECORDING = false
if (incoming.buttonRESET) {
RECORDING = false;
}
}
void setup() {
pinMode(camPin, OUTPUT);
digitalWrite(camPin, LOW); // Idle state
Serial.begin(115200);
delay(200);
Serial.println("Extra Receiver Ready");
pinMode(LED_BUILTIN, OUTPUT);
digitalWrite(LED_BUILTIN, LOW);
WiFi.mode(WIFI_STA);
WiFi.setSleep(false);
if (esp_now_init() != ESP_OK) {
Serial.println("ESP-NOW init failed!");
return;
}
esp_now_register_recv_cb(onDataRecv);
}
void loop() {
// Detect changes in RECORDING state
if (RECORDING != lastRECORDING) {
// Debug print
Serial.print("RECORDING state changed: ");
Serial.println(RECORDING ? "TRUE" : "FALSE");
// Trigger camera once on state change
doSinglePress();
lastRECORDING = RECORDING;
}
// Non-blocking LED blink when RECORDING = true
if (RECORDING) {
unsigned long now = millis();
if (now - lastBlink >= 1000) { // blink every 1s
lastBlink = now;
ledState = !ledState;
digitalWrite(LED_BUILTIN, ledState);
}
} else {
// Ensure LED is off when not recording
digitalWrite(LED_BUILTIN, LOW);
ledState = false;
}
}
@@ -25,9 +25,11 @@ void openPITmanually() {
void arenaLIGHT() { void arenaLIGHT() {
if (switchLIGHT.on()) { if (switchLIGHT.on()) {
digitalWrite(LIGHT_PIN, LOW); digitalWrite(LIGHT_PIN, LOW);
digitalWrite(LIGHT_PIN2, LOW);
} }
else { else {
digitalWrite(LIGHT_PIN, HIGH); digitalWrite(LIGHT_PIN, HIGH);
digitalWrite(LIGHT_PIN2, HIGH);
} }
} }
+347 -125
View File
@@ -1,13 +1,18 @@
// ROFLS+ Arena Controller // ROFLS+ Arena Controller
#include <WiFi.h> #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 <esp_now.h> // automatically installed for ESP32 boards, I think?
#include <Preferences.h> // automatically installed for ESP32 boards #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 <avdweb_Switch.h> // https://github.com/avdwebLibraries/avdweb_Switch
#include <CountDown.h> // https://github.com/RobTillaart/CountDown #include <CountDown.h> // https://github.com/RobTillaart/CountDown
#include <StopWatch.h> // https://github.com/RobTillaart/StopWatch_RT #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 // Hardware connections
// Buttons: // Buttons:
#define START_BTN_PIN 1 #define START_BTN_PIN 1
@@ -26,7 +31,7 @@
// Relays: // Relays:
#define PIT_RELEASE_PIN 37 #define PIT_RELEASE_PIN 37
#define LIGHT_PIN 39 #define LIGHT_PIN 39
#define UNUSED_RELAY3_PIN 35 #define LIGHT_PIN2 35
#define UNUSED_RELAY4_PIN 33 #define UNUSED_RELAY4_PIN 33
const byte relayOnState = LOW; const byte relayOnState = LOW;
@@ -58,10 +63,10 @@ bool buttonBLUETEAMtapout = false;
unsigned long PITopenTimestamp = 0; unsigned long PITopenTimestamp = 0;
bool PITreleased = false; bool PITreleased = false;
const long PITopenTime = 500; // activate solenoid for 500ms const long PITopenTime = 500; // default: 500 activate solenoid for 500ms
const int countdownTIME = 180; // countdown timer length in seconds, actual countdown for the fight const int countdownTIME = 180; // default: 180 countdown timer length in seconds, actual countdown for the fight
const int countdownToFightTIME = 3; // countdown timer length in seconds, "ready" countdown const int countdownToFightTIME = 3; // default: 3 countdown timer length in seconds, "ready" countdown
const int PITreleaseTime = 90; // automatic pit release time in seconds until end of countdown const int PITreleaseTime = 90; // default: 90 automatic pit release time in seconds until end of countdown
bool countdownPAUSED = false; bool countdownPAUSED = false;
CountDown FightCountDown(CountDown::SECONDS); CountDown FightCountDown(CountDown::SECONDS);
CountDown ReadyCountDown(CountDown::SECONDS); CountDown ReadyCountDown(CountDown::SECONDS);
@@ -69,7 +74,7 @@ CountDown ReadyCountDown(CountDown::SECONDS);
// Rumble stopwatch // Rumble stopwatch
StopWatch rumbleTIME(StopWatch::SECONDS); 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_REDTEAM = 0;
int BLINK_COUNTER_BLUETEAM = 0; int BLINK_COUNTER_BLUETEAM = 0;
@@ -79,11 +84,21 @@ int BLINK_INTERVAL = 200;
bool ARENA_READY = false; bool ARENA_READY = false;
bool REDTEAM_READY = false; bool REDTEAM_READY = false;
bool BLUETEAM_READY = false; bool BLUETEAM_READY = false;
bool resumeFight = false;
//------------------------------------------------------------------------------------ //------------------------------------------------------------------------------------
// ESP-NOW config // ESP-NOW config
// send config, Clock: // 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, 0xC8, 0xFF, 0xFA};
uint8_t broadcastAddressClock2[] = {0xD8, 0x3B, 0xDA, 0xC8, 0x95, 0x42};
#endif
// struct for clock data // struct for clock data
typedef struct struct_message_Clock { typedef struct struct_message_Clock {
int sendMinutes; int sendMinutes;
@@ -98,8 +113,15 @@ typedef struct struct_message_Clock {
struct_message_Clock sendClockDATA; struct_message_Clock sendClockDATA;
// send config, pilot buttons: // send config, pilot buttons:
uint8_t broadcastAddressREDTEAMbutton[] = {0x84, 0xFC, 0xE6, 0xC7, 0x23, 0x14}; #if ARENA == 1
uint8_t broadcastAddressBLUETEAMbutton[] = {0x84, 0xFC, 0xE6, 0xC7, 0x1A, 0x02}; // 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 // Structure for sending data
typedef struct struct_message_send { typedef struct struct_message_send {
@@ -111,7 +133,16 @@ struct_message_send sendToREDTEAMbutton;
struct_message_send sendToBLUETEAMbutton; struct_message_send sendToBLUETEAMbutton;
// ESP-Now stuff for the Pit controller // ESP-Now stuff for the Pit controller
uint8_t broadcastAddressPitController[] = {0x84, 0xFC, 0xE6, 0xC7, 0x19, 0xDE}; #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 // Structure for sending data
typedef struct struct_message_pit { typedef struct struct_message_pit {
bool PIT; // LED state bool PIT; // LED state
@@ -122,7 +153,7 @@ esp_now_peer_info_t peerInfo;
// callback when data is sent // 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\nLast Packet Send Status:\t"); Serial.print("\r\nLast Packet Send Status:\t");
Serial.println(status == ESP_NOW_SEND_SUCCESS ? "Delivery Success" : "Delivery Fail"); 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; break;
case 1: case 1:
// RED team button // RED team button
buttonREDTEAMvar = receiveDATA.buttonREDTEAM; // ignore button input if in rumble mode
buttonREDTEAMtapout = receiveDATA.buttonREDTEAMtapout; 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; break;
case 2: case 2:
// BLUE team button // BLUE team button
buttonBLUETEAMvar = receiveDATA.buttonBLUETEAM; // ignore button input if in rumble mode
buttonBLUETEAMtapout = receiveDATA.buttonBLUETEAMtapout; 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; break;
} }
} }
@@ -184,11 +233,30 @@ void sendTimeDisplay(int MINUTES, int SECONDS, int RED, int GREEN, int BLUE, int
sendClockDATA.sendGREENchannel = GREEN; sendClockDATA.sendGREENchannel = GREEN;
sendClockDATA.sendBLUEchannel = BLUE; sendClockDATA.sendBLUEchannel = BLUE;
sendClockDATA.sendBrightness = BRIGHTNESS; sendClockDATA.sendBrightness = BRIGHTNESS;
// actually send it // actually send it to first clock
esp_err_t result = esp_now_send(broadcastAddressClock, (uint8_t *) &sendClockDATA, sizeof(sendClockDATA)); 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); digitalWrite(PIT_RELEASE_PIN, relayOffState);
pinMode(LIGHT_PIN, OUTPUT); pinMode(LIGHT_PIN, OUTPUT);
digitalWrite(LIGHT_PIN, LOW); // have it by default on, to prevent flickering, needs a better fix tho digitalWrite(LIGHT_PIN, LOW); // have it by default on, to prevent flickering, needs a better fix tho
pinMode(UNUSED_RELAY3_PIN, OUTPUT); pinMode(LIGHT_PIN2, OUTPUT);
digitalWrite(UNUSED_RELAY3_PIN, relayOffState); digitalWrite(LIGHT_PIN2, LOW); // have it by default on, to prevent flickering, needs a better fix tho
pinMode(UNUSED_RELAY4_PIN, OUTPUT); pinMode(UNUSED_RELAY4_PIN, OUTPUT);
digitalWrite(UNUSED_RELAY4_PIN, relayOffState); digitalWrite(UNUSED_RELAY4_PIN, relayOffState);
// set status LED outputs: // set status LED outputs:
pinMode(LIGHT_STATUS_LED, OUTPUT); 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); pinMode(AUTOPIT_STATUS_LED, OUTPUT);
digitalWrite(AUTOPIT_STATUS_LED, LOW); digitalWrite(AUTOPIT_STATUS_LED, LOW);
pinMode(MODE_STATUS_LED, OUTPUT); pinMode(MODE_STATUS_LED, OUTPUT);
@@ -225,17 +293,27 @@ void setup() {
// get the status of Transmitted packet // get the status of Transmitted packet
esp_now_register_send_cb(OnDataSent); esp_now_register_send_cb(OnDataSent);
// Register clock peer // Register first clock peer
memcpy(peerInfo.peer_addr, broadcastAddressClock, 6); memcpy(peerInfo.peer_addr, broadcastAddressClock1, 6);
peerInfo.channel = 0; peerInfo.channel = 0;
peerInfo.encrypt = false; peerInfo.encrypt = false;
// Add peer // Add peer
if (esp_now_add_peer(&peerInfo) != ESP_OK){ if (esp_now_add_peer(&peerInfo) != ESP_OK){
Serial.println("Failed to add peer"); Serial.println("Failed to add Clock1 peer");
return; 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 // Register Red Team Button peer
memcpy(peerInfo.peer_addr, broadcastAddressREDTEAMbutton, 6); memcpy(peerInfo.peer_addr, broadcastAddressREDTEAMbutton, 6);
@@ -280,81 +358,85 @@ void setup() {
esp_now_register_recv_cb(esp_now_recv_cb_t(OnDataRecv)); 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) { if (buttonSTARTvar) {
buttonSTARTvar = false; 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 (!FightCountDown.isRunning() && !rumbleTIME.isRunning() && countdownPAUSED == false) {
if (switchRUMBLE.on()) { if (switchRUMBLE.on()) {
buttonREDTEAMvar = false; buttonREDTEAMvar = false;
buttonBLUETEAMvar = false; buttonBLUETEAMvar = false;
//rumbleTIME.start();
ARENA_READY = true; ARENA_READY = true;
REDTEAM_LED(true);
BLUETEAM_LED(true);
ReadyCountDown.start(countdownToFightTIME); ReadyCountDown.start(countdownToFightTIME);
} else if (buttonREDTEAMvar && buttonBLUETEAMvar && !switchRUMBLE.on()) { }
else if (buttonREDTEAMvar && buttonBLUETEAMvar && !switchRUMBLE.on()) {
buttonREDTEAMvar = false; buttonREDTEAMvar = false;
buttonBLUETEAMvar = false; buttonBLUETEAMvar = false;
//FightCountDown.start(countdownTIME);
ARENA_READY = true; ARENA_READY = true;
REDTEAM_LED(true);
BLUETEAM_LED(true);
ReadyCountDown.start(countdownToFightTIME); ReadyCountDown.start(countdownToFightTIME);
} }
} }
else { else {
// resume logic // Resume (unpause) branch: instead of calling FightCountDown.resume() immediately,
// we want to display ReadyCountDown first.
if (buttonREDTEAMvar && buttonBLUETEAMvar && !switchRUMBLE.on()) { if (buttonREDTEAMvar && buttonBLUETEAMvar && !switchRUMBLE.on()) {
buttonREDTEAMvar = false; buttonREDTEAMvar = false;
buttonBLUETEAMvar = false; buttonBLUETEAMvar = false;
resumeFight = true; // Mark that we want to resume later.
countdownPAUSED = false; countdownPAUSED = false;
FightCountDown.resume(); ARENA_READY = true;
} else if (switchRUMBLE.on()) { REDTEAM_LED(true);
BLUETEAM_LED(true);
ReadyCountDown.start(countdownToFightTIME);
}
else if (switchRUMBLE.on()) {
buttonREDTEAMvar = false; buttonREDTEAMvar = false;
buttonBLUETEAMvar = false; buttonBLUETEAMvar = false;
resumeFight = true;
countdownPAUSED = false; 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) { if (buttonSTARTforced) {
buttonSTARTforced = false; buttonSTARTforced = false;
if (!FightCountDown.isRunning() && !rumbleTIME.isRunning() && countdownPAUSED == false) { if (!FightCountDown.isRunning() && !rumbleTIME.isRunning() && countdownPAUSED == false) {
buttonREDTEAMvar = false; buttonREDTEAMvar = false;
buttonBLUETEAMvar = false; buttonBLUETEAMvar = false;
if (!switchRUMBLE.on()) { ARENA_READY = true;
//FightCountDown.start(countdownTIME); REDTEAM_LED(true);
ARENA_READY = true; BLUETEAM_LED(true);
ReadyCountDown.start(countdownToFightTIME); ReadyCountDown.start(countdownToFightTIME);
} else if (switchRUMBLE.on()) {
//rumbleTIME.start();
ARENA_READY = true;
ReadyCountDown.start(countdownToFightTIME);
}
} }
else { else {
// forced resume logic
buttonREDTEAMvar = false; buttonREDTEAMvar = false;
buttonBLUETEAMvar = false; buttonBLUETEAMvar = false;
resumeFight = true;
countdownPAUSED = false; countdownPAUSED = false;
if (!switchRUMBLE.on()) { ARENA_READY = true;
FightCountDown.resume(); REDTEAM_LED(true);
} else if (switchRUMBLE.on()) { BLUETEAM_LED(true);
rumbleTIME.start(); ReadyCountDown.start(countdownToFightTIME);
}
} }
} }
// pause button logic }
// Handler for the pause button logic.
void handlePauseButton() {
if (buttonPAUSEvar) { if (buttonPAUSEvar) {
buttonPAUSEvar = false; buttonPAUSEvar = false;
if (FightCountDown.isRunning()) { if (FightCountDown.isRunning()) {
@@ -366,90 +448,230 @@ void loop() {
rumbleTIME.stop(); rumbleTIME.stop();
} }
} }
// pit button logic }
// Handler for the pit button logic.
void handlePitButton() {
if (buttonPITvar) { if (buttonPITvar) {
buttonPITvar = false; buttonPITvar = false;
buttonPIThold = false; buttonPIThold = false;
openPITmanually(); openPITmanually();
} }
// automatic pit release }
if (FightCountDown.remaining() <= PITreleaseTime && FightCountDown.remaining() != 0 && !switchRUMBLE.on() && switchPIT.on() && buttonPIThold == false) {
openPIT();
}
if (rumbleTIME.elapsed() >= PITreleaseTime && switchRUMBLE.on() && switchPIT.on() && buttonPIThold == false) {
openPIT();
}
// tap out logic // check for automatic pit release.
if (buttonREDTEAMtapout && !switchRUMBLE.on()) { 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; countdownPAUSED = true;
FightCountDown.stop(); FightCountDown.stop();
REDTEAM_LED(true); REDTEAM_LED(true);
sendTimeDisplay(99, 0, 255, 0, 0, CLOCK_LED_BRIGHTNESS); redTapOutActive = true;
delay(1500); redTapOutStartTime = millis();
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()) {
// 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; countdownPAUSED = true;
FightCountDown.stop(); FightCountDown.stop();
BLUETEAM_LED(true); BLUETEAM_LED(true);
sendTimeDisplay(99, 0, 0, 0, 255, CLOCK_LED_BRIGHTNESS); blueTapOutActive = true;
delay(1500); blueTapOutStartTime = millis();
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 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) { if (buttonRESETvar) {
buttonRESETvar = false; buttonRESETvar = false;
PITreleased = false; PITreleased = false;
ESP.restart(); ESP.restart();
} }
// Ready Countdown to Fight Countdown transition // Transition from ReadyCountDown to the appropriate fight countdown.
if (ReadyCountDown.remaining() == 0 && ARENA_READY) { handleCountdownTransition();
ARENA_READY = false;
REDTEAM_LED(false);
BLUETEAM_LED(false);
if (!switchRUMBLE.on()) {
FightCountDown.start(countdownTIME);
} else if (switchRUMBLE.on()) {
rumbleTIME.start();
}
}
// If either team has tapped out, skip updating the display. // End display
if (buttonREDTEAMtapout || buttonBLUETEAMtapout) { handleFightEnd();
return;
}
// Display the Ready CountDown timer if it is running. // When no team is in a tapout sequence and fight hasn't ended, update the display.
if (ReadyCountDown.isRunning()) { if (!redTapOutActive && !blueTapOutActive && !fightEnded) {
sendTimeDisplay((ReadyCountDown.remaining()/60%60), (ReadyCountDown.remaining()%60), 255, 165, 0, CLOCK_LED_BRIGHTNESS); updateDisplay();
return;
}
// When the RUMBLE switch is active, always show the rumble timer.
if (switchRUMBLE.on()) {
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.
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.
sendTimeDisplay((FightCountDown.remaining()/60%60), (FightCountDown.remaining()%60), 0, 255, 0, CLOCK_LED_BRIGHTNESS);
} }
} }
@@ -25,7 +25,9 @@ const int COLONArray [] = {224,225,226,227,228,229,230,231};
// tap out // tap out
const int LitArrayTap [] = {0,1,2,3,4,5,6,7,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,56,57,58,59,60,61,62,63,64,65,66,67,68,69,70,71,72,73,74,75,76,77,78,79,80,81,82,83,84,85,86,87,88,89,90,91,92,93,94,95,104,105,106,107,108,109,110,111,112,113,114,115,116,117,118,119,120,121,122,123,124,125,126,127,128,129,130,131,132,133,134,135,136,137,138,139,140,141,142,143,144,145,146,147,148,149,150,151}; const int LitArrayTap [] = {0,1,2,3,4,5,6,7,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,56,57,58,59,60,61,62,63,64,65,66,67,68,69,70,71,72,73,74,75,76,77,78,79,80,81,82,83,84,85,86,87,88,89,90,91,92,93,94,95,104,105,106,107,108,109,110,111,112,113,114,115,116,117,118,119,120,121,122,123,124,125,126,127,128,129,130,131,132,133,134,135,136,137,138,139,140,141,142,143,144,145,146,147,148,149,150,151};
const int LitArrayOut [] = {0,1,2,3,4,5,6,7,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48,49,50,51,52,53,54,55,88,89,90,91,92,93,94,95,96,97,98,99,100,101,102,103,104,105,106,107,108,109,110,111,112,113,114,115,116,117,118,119,136,137,138,139,140,141,142,143,144,145,146,147,148,149,150,151,152,153,154,155,156,157,158,159}; //const int LitArrayOut [] = {0,1,2,3,4,5,6,7,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48,49,50,51,52,53,54,55,88,89,90,91,92,93,94,95,96,97,98,99,100,101,102,103,104,105,106,107,108,109,110,111,112,113,114,115,116,117,118,119,136,137,138,139,140,141,142,143,144,145,146,147,148,149,150,151,152,153,154,155,156,157,158,159};
const int LitArrayOut [] = {56,57,58,59,60,61,62,63,88,89,90,91,92,93,94,95,96,97,98,99,100,101,102,103,104,105,106,107,108,109,110,111,144,145,146,147,148,149,150,151,152,153,154,155,156,157,158,159,160,161,162,163,164,165,166,167,168,169,170,171,172,173,174,175,192,193,194,195,196,197,198,199,200,201,202,203,204,205,206,207,208,209,210,211,212,213,214,215};
// set digits of the timer/clock, use: setDIGIT(<digit ID>, <Number>, <red channel intensity 0-255>, <green channel intensity 0-255>, <blue channel intensity 0-255>) // set digits of the timer/clock, use: setDIGIT(<digit ID>, <Number>, <red channel intensity 0-255>, <green channel intensity 0-255>, <blue channel intensity 0-255>)
@@ -4,6 +4,7 @@
// 48:27:E2:50:86:84 // 48:27:E2:50:86:84
#include <WiFi.h> #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 <esp_now.h> // automatically installed for ESP32 boards, I think?
#include <Preferences.h> // automatically installed for ESP32 boards #include <Preferences.h> // automatically installed for ESP32 boards
#include <FastLED.h> // https://fastled.io/ #include <FastLED.h> // https://fastled.io/
@@ -58,7 +59,7 @@ void setup() {
//FastLED.addLeds<WS2811Controller800Khz, LED_DATA_PIN_TIMER, GRB>(leds_TIMER, NUM_LEDS_TIMER); // GRB ordering is typical //FastLED.addLeds<WS2811Controller800Khz, LED_DATA_PIN_TIMER, GRB>(leds_TIMER, NUM_LEDS_TIMER); // GRB ordering is typical
FastLED.addLeds<WS2812B, LED_DATA_PIN_TIMER, GRB>(leds_TIMER, NUM_LEDS_TIMER); // GRB ordering is typical FastLED.addLeds<WS2812B, LED_DATA_PIN_TIMER, GRB>(leds_TIMER, NUM_LEDS_TIMER); // GRB ordering is typical
//FastLED.setMaxRefreshRate(10, true); //FastLED.setMaxRefreshRate(10, true);
FastLED.setMaxPowerInVoltsAndMilliamps(5,2000); // Limit to 10W of output power FastLED.setMaxPowerInVoltsAndMilliamps(5,4000); // Limit to 20W of output power
// set default values // set default values
// default screen, just to show it's up and running but hasn't received any data, should display 00:00 in blue // default screen, just to show it's up and running but hasn't received any data, should display 00:00 in blue
@@ -13,7 +13,7 @@
#define ACTUATED_PULSE 1000 // Actuated position #define ACTUATED_PULSE 1000 // Actuated position
// Define the reset time in milliseconds // Define the reset time in milliseconds
#define RESET_TIME_MS 500 #define RESET_TIME_MS 1000
// Create two Servo objects for the two servos // Create two Servo objects for the two servos
Servo servo1; Servo servo1;
+38 -10
View File
@@ -1,6 +1,7 @@
// ROFLS+ Referee Remote // ROFLS+ Referee Remote
#include <WiFi.h> #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 <esp_now.h> // automatically installed for ESP32 boards, I think?
#include <Preferences.h> // automatically installed for ESP32 boards #include <Preferences.h> // automatically installed for ESP32 boards
#include <avdweb_Switch.h> // https://github.com/avdwebLibraries/avdweb_Switch #include <avdweb_Switch.h> // https://github.com/avdwebLibraries/avdweb_Switch
@@ -14,6 +15,11 @@
const bool writeBoardID = false; const bool writeBoardID = false;
int boardID = 0; int boardID = 0;
// set Arena, switches the MAC addresses out
// 1 = Arena A (Ant)
// 2 = Arena B (Beetle)
#define ARENA 1
// Hardware connections // Hardware connections
#define START_BTN_PIN 10 #define START_BTN_PIN 10
#define PAUSE_BTN_PIN 8 #define PAUSE_BTN_PIN 8
@@ -45,7 +51,17 @@ bool sendDATAvar = false;
// ESP-NOW config // ESP-NOW config
// REPLACE WITH YOUR RECEIVER MAC Address // REPLACE WITH YOUR RECEIVER MAC Address
uint8_t broadcastAddress[] = {0x84, 0xFC, 0xE6, 0xC7, 0x1A, 0x8C}; #if ARENA == 1
// A Arena Controller MAC Address
uint8_t broadcastAddress[] = {0x84, 0xFC, 0xE6, 0xC7, 0x1A, 0x8C};
// Additional receiver MAC address
uint8_t extraReceiverMAC[] = { 0x78, 0x42, 0x1C, 0x6B, 0x2A, 0x4C }; // replace with real MAC
#elif ARENA == 2
// B Arena Controller MAC Address
uint8_t broadcastAddress[] = {0xD8, 0x3B, 0xDA, 0xC9, 0x0C, 0xEE};
// Additional receiver MAC address
uint8_t extraReceiverMAC[] = { 0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF }; // replace with real MAC
#endif
// Structure example to send data // Structure example to send data
// Must match the receiver structure // Must match the receiver structure
@@ -74,12 +90,10 @@ typedef struct struct_message_receive {
// Create instance for receiving ESP-NOW data // Create instance for receiving ESP-NOW data
struct_message_receive receiveDATA; struct_message_receive receiveDATA;
esp_now_peer_info_t peerInfo;
Preferences preferences; Preferences preferences;
// callback when data is sent // 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\nLast Packet Send Status:\t"); Serial.print("\r\nLast Packet Send Status:\t");
Serial.println(status == ESP_NOW_SEND_SUCCESS ? "Delivery Success" : "Delivery Fail"); Serial.println(status == ESP_NOW_SEND_SUCCESS ? "Delivery Success" : "Delivery Fail");
} }
@@ -126,13 +140,25 @@ void setup() {
// get the status of Transmitted packet // get the status of Transmitted packet
esp_now_register_send_cb(OnDataSent); esp_now_register_send_cb(OnDataSent);
// Register peer // Register peer 1 (arena controller)
memcpy(peerInfo.peer_addr, broadcastAddress, 6); esp_now_peer_info_t peerInfo1 = {};
peerInfo.channel = 0; memcpy(peerInfo1.peer_addr, broadcastAddress, 6);
peerInfo.encrypt = false; peerInfo1.channel = 0;
peerInfo1.encrypt = false;
// Add peer // Add peer
if (esp_now_add_peer(&peerInfo) != ESP_OK){ if (esp_now_add_peer(&peerInfo1) != ESP_OK){
Serial.println("Failed to add peer"); Serial.println("Failed to add peer1");
return;
}
// Register peer 2 (extra receiver)
esp_now_peer_info_t peerInfo2 = {};
memcpy(peerInfo2.peer_addr, extraReceiverMAC, 6);
peerInfo2.channel = 0;
peerInfo2.encrypt = false;
// Add peer
if (esp_now_add_peer(&peerInfo2) != ESP_OK){
Serial.println("Failed to add peer2");
return; return;
} }
@@ -171,6 +197,8 @@ void sendDATA_to_controller() {
sendDATA.buttonBLUETEAMtapout = buttonBLUETEAMtapout; sendDATA.buttonBLUETEAMtapout = buttonBLUETEAMtapout;
// send data // send data
esp_err_t result = esp_now_send(broadcastAddress, (uint8_t *) &sendDATA, sizeof(sendDATA)); esp_err_t result = esp_now_send(broadcastAddress, (uint8_t *) &sendDATA, sizeof(sendDATA));
esp_now_send(extraReceiverMAC, (uint8_t *) &sendDATA, sizeof(sendDATA));
// check if transmission was successfull // check if transmission was successfull
// if (result == ESP_OK) { // if (result == ESP_OK) {
// Serial.println("Sent with success"); // Serial.println("Sent with success");
+80
View File
@@ -0,0 +1,80 @@
# ROFLS Arena Reference Manual
### Before a fight/tournament (omit step 1-6 after initial setup):
1. Check all connections
2. Power on the PSU
3. Turn on the Teambuttons and the Referee Remote
4. Wait for the 7 Segment Clock to turn on (~2s)
5. Turn on the lights (if off and needed)
6. Open the Controller Box and set the brightness with the potentiometer on the top right
7. Select the mode (explanations for the modes, see below):
8. Press and Hold Reset for >1s, can be on the Controller itself or the Referee Remote
9. Arena is ready
### Start a normal fight:
1. for good measure: Press and Hold Reset for >1s, can be on the Controller itself or the Referee Remote.
2. Team Red and Team Blue need to (short) press their respective Teambuttons, the Clock turns green to signal that both are ready.
3. Press Start, if the Teambuttons are not working, the Start button can be long pressed (>1s), to force a start.
4. To prevent the automatic pit opening, press and hold the Pit button, as soon as the pit button gets released, the pit will open.
### Start a rumble fight:
1. for good measure: Press and Hold Reset for >1s, can be on the Controller itself or the Referee Remote.
2. Press Start
3. To prevent the automatic pit opening, press and hold the Pit button, as soon as the pit button gets released, the pit will open.
### Pausing a running fight:
1. Press the Pause button
2. To resume:
- normal fight:
Both Teams need to press their respective Teambuttons to signal that they are both ready.
- Press start, after 3s, the timer resumes.
- rumble fight:
- Press start, after 3s, the stop watch resumes.
## Modes
* Normal Mode:
* Normal fight mode, 3 Minute Timer, Tapout enabled, after 90s (halftime) automatic pit opening (if enabled)
* Teambuttons are active and need to be pressed once to sign that they are ready. They light up if ready.
* Teambuttons LEDs turn of when the fight starts
* During a running fight, if one of the Teambuttons is pressed >1s, it counts as a tapout. The Timer stops and displays "TAPOUT", cycles between "TAPOUT" and the end time.
* Can be paused, resuming works the same as starting, Teams must be ready
* Rumble Mode:
* Rumble mode, stop watch counting up, Tapout disabled, after 90s automatic pit opening (if enabled)
* Match starts 3s after pressing start
* Teambuttons are inactive
* Teambutton LEDs Light up 3s before the match begins, and turn off when it starts.
* No Tapout available
* Can be paused, resuming works the same as starting, Teams must be ready
## Buttons explained
> The 4 buttons on the referee remote and the controller box have the same function and can both be used.
* Start
* Starts a fight, can be long pressed (>1s) to force start a fight
* Pause
* Pauses a fight
* Pit
* Short press (<1s) immediately opens the pit, Long press (>1s) prevents the pit from automatically open, but opens immediately after released
* Reset
* Resets the Arena Controller, needs to be long pressed (>1s)
* Lights Switch
* Turns on/off the Arena Lights, lights up if lights are on.
* Auto Pit Switch
* Turn on/off the automatic pit opening, lights up if enabled
* Mode Switch
* R = Rumble mode
* N = Normal mode
* T = Self Test (not implemented yet)
## Display Colours & Status explained
| Colour | Meaning |
|--------|---------|
| Blue with one purple segment, displaying 00:00 | not initialized (hit reset to fix) |
| Purple, displaying 03:00 | Normal Mode, Idle/Arena Ready |
| Green | Normal Mode, Both Teams Ready/Running Fight |
| Yellow | 3s Ready Countdown or Paused |
| Yellow, initally blinking 3 times | Timer run out / finish |
| Red, cycling between tap, out and time | Red Team has tapped out |
| Blue, cycling between tap, out and time | Blue Team has tapped out |
| Teal, displaying 00:00 | Rumble mode, Idle/Arena Ready |
| Teal, displaying timer | Rumble mode, active fight |