/*
MASTER ESP32-C6 — Light Chase Game
MAC: 58:E6:C5:1A:7F:28
- D6 = GPIO16 = TOP LED
- D7 = GPIO17 = BOTTOM LED
- GPIO 21 = Piezo speaker (MASTER ONLY)
- GPIO 0 = Hall sensor (LOW when magnet present)
TIMEOUT BEHAVIOUR:
Every 10s with no hall sensor triggered anywhere:
→ twitter on master speaker → move light
Repeats max 2 times. After 2nd move (30s total ignored):
→ all LEDs off, descending tone, program goes idle.
Any hall trigger resets the counter. Send "RESTART" over
serial to re-activate.
*/
#include <esp_now.h>
#include <WiFi.h>
#define LED_TOP 16
#define LED_BOTTOM 17
#define SPEAKER_PIN 21
#define HALL_PIN 1
// Node 1: B4:3A:45:89:E9:F4
uint8_t node1Mac[] = {0xB4, 0x3A, 0x45, 0x89, 0xE9, 0xF4};
// Node 2: B4:3A:45:89:F2:B4
uint8_t node2Mac[] = {0xB4, 0x3A, 0x45, 0x89, 0xF2, 0xB4};
typedef struct {
int nodeID;
char type[8];
int value;
} IncomingMsg;
typedef struct {
char cmd[16];
} OutgoingCmd;
int currentNode = -1;
int currentPos = -1;
int moveCount = 0;
bool gameActive = true;
unsigned long lastActivityTime = 0;
const unsigned long TIMEOUT_MS = 10000;
bool masterHallTriggered = false;
bool node1HallActive = false;
bool node2HallActive = false;
void onReceive(const esp_now_recv_info_t *info, const uint8_t *data, int len) {
IncomingMsg msg;
memcpy(&msg, data, sizeof(msg));
Serial.print("NODE:");
Serial.print(msg.nodeID);
Serial.print(",");
Serial.print(msg.type);
Serial.print(":");
Serial.println(msg.value);
if (strcmp(msg.type, "HALL") == 0) {
if (msg.nodeID == 1) node1HallActive = (msg.value == 1);
if (msg.nodeID == 2) node2HallActive = (msg.value == 1);
if (msg.value == 1 && gameActive) {
lastActivityTime = millis();
moveCount = 0;
}
}
}
void sendToNode(int nodeID, const char* cmd) {
OutgoingCmd msg;
strncpy(msg.cmd, cmd, sizeof(msg.cmd));
if (nodeID == 1) esp_now_send(node1Mac, (uint8_t*)&msg, sizeof(msg));
else if (nodeID == 2) esp_now_send(node2Mac, (uint8_t*)&msg, sizeof(msg));
}
void masterLedsOff() {
digitalWrite(LED_TOP, LOW);
digitalWrite(LED_BOTTOM, LOW);
}
void masterSetLed(int pos, bool on) {
if (pos == 0) digitalWrite(LED_TOP, on ? HIGH : LOW);
else digitalWrite(LED_BOTTOM, on ? HIGH : LOW);
}
void allLedsOff() {
masterLedsOff();
sendToNode(1, "LED_TOP_OFF");
sendToNode(1, "LED_BOT_OFF");
sendToNode(2, "LED_TOP_OFF");
sendToNode(2, "LED_BOT_OFF");
}
void twitter() {
for (int chirp = 0; chirp < 5; chirp++) {
for (int i = 0; i < 60; i++) {
digitalWrite(SPEAKER_PIN, HIGH);
delayMicroseconds(250);
digitalWrite(SPEAKER_PIN, LOW);
delayMicroseconds(250);
}
delay(40);
}
}
void beepMid() {
for (int i = 0; i < 240; i++) {
digitalWrite(SPEAKER_PIN, HIGH);
delayMicroseconds(625);
digitalWrite(SPEAKER_PIN, LOW);
delayMicroseconds(625);
}
}
void beepShutdown() {
int delays[] = {300, 400, 500, 700};
for (int d = 0; d < 4; d++) {
for (int i = 0; i < 100; i++) {
digitalWrite(SPEAKER_PIN, HIGH);
delayMicroseconds(delays[d]);
digitalWrite(SPEAKER_PIN, LOW);
delayMicroseconds(delays[d]);
}
delay(30);
}
}
void moveLightTo(int node, int pos) {
if (currentNode == 0) masterLedsOff();
else if (currentNode == 1) { sendToNode(1, "LED_TOP_OFF"); sendToNode(1, "LED_BOT_OFF"); }
else if (currentNode == 2) { sendToNode(2, "LED_TOP_OFF"); sendToNode(2, "LED_BOT_OFF"); }
currentNode = node;
currentPos = pos;
if (node == 0) masterSetLed(pos, true);
else if (node == 1) sendToNode(1, pos == 0 ? "LED_TOP_ON" : "LED_BOT_ON");
else if (node == 2) sendToNode(2, pos == 0 ? "LED_TOP_ON" : "LED_BOT_ON");
beepMid();
Serial.print("LIGHT:");
Serial.print(node);
Serial.print(",");
Serial.println(pos);
}
void handleTimeout() {
if (moveCount >= 2) {
shutDown();
return;
}
twitter();
delay(100);
int newNode, newPos;
do {
newNode = random(0, 3);
newPos = random(0, 2);
} while (newNode == currentNode && newPos == currentPos);
moveCount++;
moveLightTo(newNode, newPos);
lastActivityTime = millis();
Serial.print("EVENT:TIMEOUT_MOVE:");
Serial.println(moveCount);
}
void shutDown() {
gameActive = false;
allLedsOff();
beepShutdown();
Serial.println("EVENT:SHUTDOWN");
}
void setup() {
Serial.begin(115200);
pinMode(LED_TOP, OUTPUT);
pinMode(LED_BOTTOM, OUTPUT);
pinMode(SPEAKER_PIN, OUTPUT);
pinMode(HALL_PIN, INPUT_PULLUP);
masterLedsOff();
WiFi.mode(WIFI_STA);
esp_now_init();
esp_now_register_recv_cb(onReceive);
esp_now_peer_info_t peer = {};
peer.channel = 0;
peer.encrypt = false;
memcpy(peer.peer_addr, node1Mac, 6);
esp_now_add_peer(&peer);
memcpy(peer.peer_addr, node2Mac, 6);
esp_now_add_peer(&peer);
randomSeed(esp_random());
int startNode = random(0, 3);
int startPos = random(0, 2);
currentNode = startNode;
currentPos = startPos;
if (startNode == 0) masterSetLed(startPos, true);
else sendToNode(startNode, startPos == 0 ? "LED_TOP_ON" : "LED_BOT_ON");
Serial.print("LIGHT:");
Serial.print(startNode);
Serial.print(",");
Serial.println(startPos);
lastActivityTime = millis();
moveCount = 0;
Serial.println("STATUS:MASTER_READY");
}
void loop() {
if (!gameActive) {
if (Serial.available()) {
String cmd = Serial.readStringUntil('\n');
cmd.trim();
if (cmd == "RESTART") {
gameActive = true;
moveCount = 0;
randomSeed(esp_random());
int n = random(0, 3);
int p = random(0, 2);
currentNode = -1;
moveLightTo(n, p);
lastActivityTime = millis();
Serial.println("STATUS:RESTARTED");
}
}
delay(50);
return;
}
bool hallNow = (digitalRead(HALL_PIN) == LOW);
if (hallNow && !masterHallTriggered) {
masterHallTriggered = true;
lastActivityTime = millis();
moveCount = 0;
Serial.println("NODE:0,HALL:1");
} else if (!hallNow && masterHallTriggered) {
masterHallTriggered = false;
Serial.println("NODE:0,HALL:0");
}
if ((millis() - lastActivityTime) >= TIMEOUT_MS) {
handleTimeout();
}
if (Serial.available()) {
String cmd = Serial.readStringUntil('\n');
cmd.trim();
if (cmd.startsWith("MOVE:")) {
String params = cmd.substring(5);
int comma = params.indexOf(',');
if (comma > 0) {
int n = params.substring(0, comma).toInt();
int p = params.substring(comma + 1).toInt();
moveLightTo(n, p);
lastActivityTime = millis();
moveCount = 0;
}
}
// Processing reconnected — resend current state
else if (cmd == "STATUS") {
Serial.println("STATUS:MASTER_READY");
if (currentNode >= 0) {
Serial.print("LIGHT:");
Serial.print(currentNode);
Serial.print(",");
Serial.println(currentPos);
}
}
}
delay(20);
}