Untitled

 avatar
unknown
c_cpp
2 months ago
30 kB
6
Indexable
#include <SPI.h>
#include <MFRC522.h>
#include <Wire.h>
#include <RadioLib.h>
#include <LiquidCrystal_I2C.h>
#include <stdlib.h>
#include <string.h>
#include <strings.h>
#include <stdio.h>

// ============================================================
// DEBUG CONTROL
// ============================================================
static const bool DEBUG_ENABLE = true;

#define DBG_PRINT(...)   do { if (DEBUG_ENABLE) Serial.print(__VA_ARGS__); } while (0)
#define DBG_PRINTLN(...) do { if (DEBUG_ENABLE) Serial.println(__VA_ARGS__); } while (0)
#define DBG_PRINTF(...)  do { if (DEBUG_ENABLE) Serial.printf(__VA_ARGS__); } while (0)

// ============================================================
// HARDWARE PINS
// ============================================================
#define LED_YELLOW_P  2     // Yellow LED on pin 2 
#define LED_GREEN_P   17    // Green LED on pin 17
#define LED_RED_P     15    // Red LED on pin 15

#define RC522_SS_P    4     // RC522 SS pin
#define RC522_RST_P   16    // RC522 RST pin
#define SCK_P         14    // SPI SCK pin
#define MISO_P        35    // SPI MISO pin
#define MOSI_P        13    // SPI MOSI pin

#define SENSOR_P      21    // PIR sensor pin

#define LCD_SDA_P     32    // LCD SDA pin
#define LCD_SCL_P     33    // LCD SCL pin

#define LORA_CS_P     5     // LoRa CS pin
#define LORA_DIO1_P   25    // LoRa DIO1 pin
#define LORA_RST_P    27    // LoRa RST pin
#define LORA_BUSY_P   26    // LoRa BUSY pin

// ============================================================
// TIMING / RADIO SETTINGS
// ============================================================
static const float LORA_FREQ_MHZ = 915.0f;  // CHANGE: set to 915 MHz for US; change to 433.0f for EU
static const int   LORA_SF = 9;             // CHANGE: can be 7-12; higher is more robust but slower
static const float LORA_BW_KHZ = 62.5f;     // CHANGE: set to 62.5 kHz for US; change to 125.0 kHz for EU
static const int   LORA_CR = 5;             // CHANGE: can be 5-8; higher is more robust but slower
static const int   LORA_TX_DBM = 2;         // CHANGE: can be 2-17; higher is stronger but draws more power

static const uint32_t PIR_STABLE_MS = 200;  // Time that PIR must be continuously HIGH to trigger start
static const uint32_t PIR_REARM_MS = 1200;  // Minimum time after a PIR trigger before another can be registered (prevents multiple triggers from one crossing)
static const uint32_t START_LOW_VERIFY_MS = 800;    // Time that PIR must be continuously LOW to arm the start (prevents false arming from brief LOWs)
static const uint32_t START_WAIT_TIMEOUT_MS = 15000;    // Time to wait for the racer to trigger the start PIR before cancelling and returning to RFID_WAIT
static const uint32_t RUN_TIMEOUT_MS = 600000;      // Maximum run time (10 minutes) before automatically cancelling and returning to RFID_WAIT
static const uint32_t START_ACK_RESEND_MS = 800;    // Time after sending CMD START to resend if no ACK START received
static const uint32_t START_ACK_TIMEOUT_MS = 3500;  // Time to wait for ACK START before cancelling run (should be longer than START_ACK_RESEND_MS to allow for at least one resend)
static const uint32_t READY_FLASH_MS = 250; // Time interval for flashing LEDs when waiting for start arm
static const uint32_t RUN_PING_MS = 10000;  // Time interval to send PING packets during an active run to verify the link is still alive
static const uint32_t RUN_LINK_WARN_MS = 30000; // Time with no ACK PING received before showing a warning about possible link issues (but without cancelling the run, since it may just be a one-way issue or a delayed packet)

// ============================================================
// BUFFER SIZES
// ============================================================
#define UID_BUF_SZ         24   // Enough for 10 bytes in hex plus colons (e.g. "FF:FF:FF:FF:FF:FF:FF:FF:FF:FF") and null terminator
#define NAME_BUF_SZ        32   // Enough for the longest expected racer name plus null terminator
#define TIME_BUF_SZ        24   // Enough for formatted time strings like "HH:MM:SS:MMM" plus null terminator
#define PACKET_BUF_SZ      160  // Enough for the longest expected packet (e.g. RESULT with all fields) plus null terminator
#define LCD_LINE_BUF_SZ    17   // 16 characters for LCD line plus null terminator

// ============================================================
// OBJECTS
// ============================================================
MFRC522 rfid(RC522_SS_P, RC522_RST_P);  // Create MFRC522 instance for RFID reader
LiquidCrystal_I2C lcd(0x27, 16, 2);     // Create LCD instance (I2C address 0x27, 16 columns, 2 rows)
SX1262 radio = new Module(LORA_CS_P, LORA_DIO1_P, LORA_RST_P, LORA_BUSY_P); // Create SX1262 instance for LoRa radio

// ============================================================
// RACER DATABASE
// ============================================================

// Racer Data
struct Racer {
    const char* uid;
    const char* name;
};

// RFID saved UIDs and corresponding racer names; CHANGE as needed for your racers
static const Racer library[] = {
  {"A3:36:3A:21", "NJ Bascug"},
  {"D3:62:E2:20", "WenXing Tan"},
  {"2D:25:BC:01", "Jacob Matthews"},
  {"A7:88:4A:01", "Ian Booty"},
  {"3D:02:3E:03", "Rob Baillie"},
  {"76:EA:1E:2F", "Lucas Fermer"},
  {"9C:0D:76:33", "Kip"}
};

// Number of racers in the library; calculated from the size of the array
static const int RACER_COUNT = sizeof(library) / sizeof(library[0]);   

// ============================================================
// STATE
// ============================================================
enum MODE {
    RFID_WAIT,
    START_ARM_WAIT,
    RUN_ACTIVE
};

static MODE mode = RFID_WAIT;

static bool timerRunning = false;
static uint32_t currentRunId = 0;
static uint32_t nextRunId = 1;
static uint32_t tStartMs = 0;
static uint32_t startStampMs = 0;

static uint32_t stateEnterMs = 0;
static uint32_t startCmdFirstSentMs = 0;
static uint32_t startCmdLastSentMs = 0;
static uint32_t lastPingSentMs = 0;
static uint32_t lastPingAckMs = 0;

static bool waitingForStartAck = false;
static bool startAckReceived = false;
static bool runLinkWarned = false;

static bool startPirArmed = false;
static uint32_t startPirLowSinceMs = 0;
static bool pirWasHigh = false;
static uint32_t pirHighStartMs = 0;
static uint32_t lastPirTriggerMs = 0;

static bool readyFlashState = false;
static uint32_t readyFlashLastToggleMs = 0;

static char activeRacerUid[UID_BUF_SZ] = { 0 };
static char activeRacerName[NAME_BUF_SZ] = { 0 };

static char lcdLine1Cache[LCD_LINE_BUF_SZ] = { 0 };
static char lcdLine2Cache[LCD_LINE_BUF_SZ] = { 0 };

// ============================================================
// FORWARD DECLARATIONS
// ============================================================
static void setupRadio();
static void setupDisplay();
static void setupPins();

static void handleRfidWait();
static void handleStartArmWait();
static void handleRunActive();

static void resetStartAckState();
static void resetStartPirState();
static void resetReadyFlashState();
static void resetRunLinkState();
static void resetActiveRacer();
static void resetForIdle();

static bool parseAckStart(const char* rx, uint32_t& runIdOut);
static bool parseAckPing(const char* rx, uint32_t& runIdOut);
static bool parseCmdStop(const char* rx, uint32_t& runIdOut);

static bool rfidScanDetected();
static void finishRfidTransaction();
static void getScannedUidString(char* out, size_t outSize);
static bool lookupRacerName(const char* uid, char* outName, size_t outNameSize);

static void sendPacket(const char* s);
static void sendCancelBurst(uint32_t runId);
static void sendRacerBurst(uint32_t runId, const char* racerName);
static void sendResultBurst(const char* name, const char* uid, uint32_t runId, uint32_t startMs, uint32_t elapsedMs);
static void sendResetBurst();

static void ledsWaitForRfid();
static void ledsReady();
static void ledsTiming();
static void ledsWaitForStartArm();

static void setLcdLines(const char* line1, const char* line2);
static void showScanPrompt();
static void showRunnerWaiting();
static void showStartArmed();
static void showRunning();
static void showCancelled();
static void showTimeout(const char* top, const char* bottom);
static void showElapsedTime(uint32_t elapsedMs);

static void formatElapsedTime(uint32_t elapsedMs, char* out, size_t outSize);

static bool detectStartMovement();
static void cancelCurrentRun(const char* reason, bool sendRadioCancel);
static void completeRunFromStop(uint32_t stopRunId);

static void printUID();
static void buildCmdStart(uint32_t runId, char* out, size_t outSize);
static void buildCmdCancel(uint32_t runId, char* out, size_t outSize);
static void buildRacerPacket(uint32_t runId, const char* racerName, char* out, size_t outSize);
static void buildTimePacket(uint32_t runId, uint32_t elapsedMs, char* out, size_t outSize);
static void buildPingPacket(uint32_t runId, char* out, size_t outSize);
static void buildResultPacket(const char* name, const char* uid, uint32_t runId, uint32_t startMs, uint32_t elapsedMs, char* out, size_t outSize);

// ============================================================
// SETUP
// ============================================================
void setup() {
    Serial.begin(115200);
    delay(200);

    setupPins();
    setupDisplay();
    setupRadio();

    rfid.PCD_Init();
    delay(50);
    rfid.PCD_DumpVersionToSerial();

    // CHANGE: force Unit B back to idle whenever Unit A boots
    sendResetBurst();

    resetForIdle();
    showScanPrompt();
}

// ============================================================
// LOOP
// ============================================================
void loop() {
    switch (mode) {
    case RFID_WAIT:
        handleRfidWait();
        break;
    case START_ARM_WAIT:
        handleStartArmWait();
        break;
    case RUN_ACTIVE:
        handleRunActive();
        break;
    }
}

// ============================================================
// SETUP HELPERS
// ============================================================
// setupPins
// Sets up the pin modes for the PIR sensor and the three LEDs.
// Inputs: none
// Outputs: none
static void setupPins() {
    pinMode(SENSOR_P, INPUT);
    pinMode(LED_YELLOW_P, OUTPUT);
    pinMode(LED_GREEN_P, OUTPUT);
    pinMode(LED_RED_P, OUTPUT);
}

// setupDisplay
// Initializes the LCD display so it can show messages.
// Inputs: none
// Outputs: none
static void setupDisplay() {
    Wire.begin(LCD_SDA_P, LCD_SCL_P);
    lcd.init();
    lcd.backlight();
    lcd.clear();
    lcd.setCursor(0, 0);
    lcd.print("Booting...");
}

// setupRadio
// Sets up the LoRa radio for wireless communication. If it fails, shows an error on the LCD and stops the program.
// Inputs: none
// Outputs: none
static void setupRadio() {
    SPI.begin(SCK_P, MISO_P, MOSI_P);

    int state = radio.begin(LORA_FREQ_MHZ);
    if (state != RADIOLIB_ERR_NONE) {
        lcd.clear();
        lcd.setCursor(0, 0);
        lcd.print("BOOT FAIL:");
        lcd.setCursor(0, 1);
        lcd.print(state);

        DBG_PRINT("LoRa boot failed: ");
        DBG_PRINTLN(state);

        while (true) {
            delay(1000);
        }
    }

    radio.setSpreadingFactor(LORA_SF);
    radio.setBandwidth(LORA_BW_KHZ);
    radio.setCodingRate(LORA_CR);
    radio.setOutputPower(LORA_TX_DBM);
    radio.startReceive();
}

// ============================================================
// STATE HANDLERS
// ============================================================
// handleRfidWait
// This function runs when the system is waiting for someone to scan their RFID card.
// It checks for a scan, looks up the racer's name, and prepares for the next step.
// Inputs: none
// Outputs: none
static void handleRfidWait() {
    ledsWaitForRfid();

    if (!rfidScanDetected()) return;

    char scannedUid[UID_BUF_SZ];
    char matchedName[NAME_BUF_SZ];

    getScannedUidString(scannedUid, sizeof(scannedUid));
    bool found = lookupRacerName(scannedUid, matchedName, sizeof(matchedName));
    finishRfidTransaction();

    if (!found) {
        DBG_PRINT("RFID not found in database: ");
        DBG_PRINTLN(scannedUid);

        setLcdLines("Card Not Found", "Scan Again");
        delay(1500);
        showScanPrompt();
        return;
    }

    // CHANGE: new scan means old state anywhere should be cleared on Unit B
    sendResetBurst();

    resetForIdle();

    strncpy(activeRacerUid, scannedUid, sizeof(activeRacerUid) - 1);
    activeRacerUid[sizeof(activeRacerUid) - 1] = '\0';

    strncpy(activeRacerName, matchedName, sizeof(activeRacerName) - 1);
    activeRacerName[sizeof(activeRacerName) - 1] = '\0';

    currentRunId = nextRunId++;

    DBG_PRINT("RFID UID: ");
    DBG_PRINTLN(activeRacerUid);
    DBG_PRINT("Matched Racer: ");
    DBG_PRINTLN(activeRacerName);
    DBG_PRINT("Prepared run ID ");
    DBG_PRINTLN(currentRunId);

    mode = START_ARM_WAIT;
    stateEnterMs = millis();
    showRunnerWaiting();
}

static void handleStartArmWait() {
    if (rfidScanDetected()) {
        finishRfidTransaction();
        cancelCurrentRun("RFID re-scan before start", true);
        return;
    }

    if (!startPirArmed) {
        ledsWaitForStartArm();
    }
    else {
        ledsReady();
    }

    if ((millis() - stateEnterMs) >= START_WAIT_TIMEOUT_MS) {
        DBG_PRINTLN("START TIMEOUT -> returning to RFID_WAIT");
        sendResetBurst();
        resetForIdle();
        showTimeout("Start Timeout", "Scan RFID Again");
        delay(1000);
        showScanPrompt();
        return;
    }

    if (!detectStartMovement()) return;

    uint32_t now = millis();
    timerRunning = true;
    tStartMs = now;
    startStampMs = now;

    waitingForStartAck = true;
    startAckReceived = false;
    startCmdFirstSentMs = now;
    startCmdLastSentMs = now;

    char pkt[PACKET_BUF_SZ];
    buildCmdStart(currentRunId, pkt, sizeof(pkt));

    DBG_PRINT("Start PIR triggered -> sending ");
    DBG_PRINTLN(pkt);
    sendPacket(pkt);

    ledsTiming();
    setLcdLines("Timer Started", "Wait ACK...");

    mode = RUN_ACTIVE;
    stateEnterMs = millis();
}

static void handleRunActive() {
    if (rfidScanDetected()) {
        finishRfidTransaction();
        cancelCurrentRun("RFID re-scan during active run", true);
        return;
    }

    String rxStr;
    int r = radio.readData(rxStr);

    if (r == RADIOLIB_ERR_NONE && rxStr.length() > 0) {
        const char* rx = rxStr.c_str();
        uint32_t rxRunId = 0;

        if (parseAckStart(rx, rxRunId)) {
            if (rxRunId == currentRunId) {
                waitingForStartAck = false;
                startAckReceived = true;
                lastPingSentMs = millis();
                lastPingAckMs = millis();
                runLinkWarned = false;

                DBG_PRINT("ACK START received for run ");
                DBG_PRINTLN(rxRunId);

                DBG_PRINT("Sending racer name to Unit B: ");
                DBG_PRINTLN(activeRacerName);
                sendRacerBurst(currentRunId, activeRacerName);

                showRunning();
            }
        }
        else if (parseAckPing(rx, rxRunId)) {
            if (rxRunId == currentRunId) {
                lastPingAckMs = millis();

                if (runLinkWarned) {
                    DBG_PRINTLN("RUN LINK RESTORED");
                    runLinkWarned = false;
                }

                DBG_PRINT("ACK PING received for run ");
                DBG_PRINTLN(rxRunId);
            }
        }
        else if (parseCmdStop(rx, rxRunId)) {
            if (timerRunning) {
                completeRunFromStop(rxRunId);
                return;
            }
            else {
                DBG_PRINTLN("Got STOP but timer not running.");
            }
        }
        else {
            DBG_PRINT("RX: ");
            DBG_PRINTLN(rx);
        }
    }

    if (waitingForStartAck) {
        uint32_t now = millis();

        if ((now - startCmdLastSentMs) >= START_ACK_RESEND_MS) {
            char pkt[PACKET_BUF_SZ];
            buildCmdStart(currentRunId, pkt, sizeof(pkt));
            DBG_PRINT("No ACK yet -> re-sending ");
            DBG_PRINTLN(pkt);
            sendPacket(pkt);
            startCmdLastSentMs = now;
        }

        if ((now - startCmdFirstSentMs) >= START_ACK_TIMEOUT_MS) {
            DBG_PRINTLN("START ACK TIMEOUT -> cancelling run");
            cancelCurrentRun("ACK timeout", true);
            return;
        }
    }

    if (timerRunning && !waitingForStartAck) {
        uint32_t now = millis();

        if ((now - lastPingSentMs) >= RUN_PING_MS) {
            char pkt[PACKET_BUF_SZ];
            buildPingPacket(currentRunId, pkt, sizeof(pkt));
            DBG_PRINT("Sending ");
            DBG_PRINTLN(pkt);
            sendPacket(pkt);
            lastPingSentMs = now;
        }

        if ((now - lastPingAckMs) >= RUN_LINK_WARN_MS && !runLinkWarned) {
            DBG_PRINTLN("WARNING: no ACK PING recently, but race continues");
            runLinkWarned = true;
        }
    }

    if ((millis() - stateEnterMs) >= RUN_TIMEOUT_MS) {
        DBG_PRINTLN("RUN TIMEOUT -> returning to RFID_WAIT");
        sendResetBurst();
        resetForIdle();
        showTimeout("Run Timeout", "Resetting...");
        delay(1000);
        showScanPrompt();
    }
}

// ============================================================
// RESET HELPERS
// ============================================================
static void resetStartAckState() {
    waitingForStartAck = false;
    startAckReceived = false;
    startCmdFirstSentMs = 0;
    startCmdLastSentMs = 0;
}

static void resetStartPirState() {
    startPirArmed = false;
    startPirLowSinceMs = 0;
    pirWasHigh = false;
    pirHighStartMs = 0;
    lastPirTriggerMs = 0;
}

static void resetReadyFlashState() {
    readyFlashState = false;
    readyFlashLastToggleMs = millis();
}

static void resetRunLinkState() {
    lastPingSentMs = 0;
    lastPingAckMs = 0;
    runLinkWarned = false;
}

static void resetActiveRacer() {
    activeRacerUid[0] = '\0';
    activeRacerName[0] = '\0';
}

static void resetForIdle() {
    timerRunning = false;
    tStartMs = 0;
    startStampMs = 0;
    currentRunId = 0;
    mode = RFID_WAIT;
    stateEnterMs = millis();

    resetStartAckState();
    resetStartPirState();
    resetReadyFlashState();
    resetRunLinkState();
    resetActiveRacer();
}

// ============================================================
// PACKET HELPERS
// ============================================================
static void buildCmdStart(uint32_t runId, char* out, size_t outSize) {
    snprintf(out, outSize, "CMD START %lu", (unsigned long)runId);
}

static void buildCmdCancel(uint32_t runId, char* out, size_t outSize) {
    snprintf(out, outSize, "CMD CANCEL %lu", (unsigned long)runId);
}

static void buildRacerPacket(uint32_t runId, const char* racerName, char* out, size_t outSize) {
    snprintf(out, outSize, "RACER %lu %s", (unsigned long)runId, racerName);
}

static void buildTimePacket(uint32_t runId, uint32_t elapsedMs, char* out, size_t outSize) {
    snprintf(out, outSize, "TIME %lu %lu", (unsigned long)runId, (unsigned long)elapsedMs);
}

static void buildPingPacket(uint32_t runId, char* out, size_t outSize) {
    snprintf(out, outSize, "PING %lu", (unsigned long)runId);
}

static void buildResultPacket(const char* name, const char* uid, uint32_t runId, uint32_t startMs, uint32_t elapsedMs, char* out, size_t outSize) {
    char timeBuf[TIME_BUF_SZ];
    formatElapsedTime(elapsedMs, timeBuf, sizeof(timeBuf));

    snprintf(out, outSize,
        "RESULT|%s|%s|%lu|%lu|%lu|%s",
        name,
        uid,
        (unsigned long)runId,
        (unsigned long)startMs,
        (unsigned long)elapsedMs,
        timeBuf);
}

// ============================================================
// PARSER HELPERS
// ============================================================
static bool parseAckStart(const char* rx, uint32_t& runIdOut) {
    if (strncmp(rx, "ACK START ", 10) != 0) return false;
    runIdOut = strtoul(rx + 10, nullptr, 10);
    return true;
}

static bool parseAckPing(const char* rx, uint32_t& runIdOut) {
    if (strncmp(rx, "ACK PING ", 9) != 0) return false;
    runIdOut = strtoul(rx + 9, nullptr, 10);
    return true;
}

static bool parseCmdStop(const char* rx, uint32_t& runIdOut) {
    if (strncmp(rx, "CMD STOP ", 9) != 0) return false;
    runIdOut = strtoul(rx + 9, nullptr, 10);
    return true;
}

// ============================================================
// RFID HELPERS
// ============================================================
static bool rfidScanDetected() {
    if (rfid.PICC_IsNewCardPresent() && rfid.PICC_ReadCardSerial()) {
        printUID();
        return true;
    }
    return false;
}

static void finishRfidTransaction() {
    rfid.PICC_HaltA();
    rfid.PCD_StopCrypto1();
}

static void getScannedUidString(char* out, size_t outSize) {
    out[0] = '\0';

    size_t pos = 0;
    for (byte i = 0; i < rfid.uid.size && pos + 3 < outSize; i++) {
        int written = snprintf(out + pos, outSize - pos,
            (i < rfid.uid.size - 1) ? "%02X:" : "%02X",
            rfid.uid.uidByte[i]);
        if (written < 0) break;
        pos += (size_t)written;
    }
}

static bool lookupRacerName(const char* uid, char* outName, size_t outNameSize) {
    for (int i = 0; i < RACER_COUNT; i++) {
        if (strcasecmp(library[i].uid, uid) == 0) {
            strncpy(outName, library[i].name, outNameSize - 1);
            outName[outNameSize - 1] = '\0';
            return true;
        }
    }
    outName[0] = '\0';
    return false;
}

// ============================================================
// RADIO HELPERS
// ============================================================
static void sendPacket(const char* s) {
    int txState = radio.transmit(s);

    if (txState != RADIOLIB_ERR_NONE) {
        DBG_PRINT("TX FAIL: ");
        DBG_PRINTLN(txState);
    }
    else {
        DBG_PRINT("TX: ");
        DBG_PRINTLN(s);
    }

    delay(30);
    radio.startReceive();
}

static void sendCancelBurst(uint32_t runId) {
    if (runId == 0) return;
    char pkt[PACKET_BUF_SZ];
    buildCmdCancel(runId, pkt, sizeof(pkt));
    sendPacket(pkt);
    delay(120);
    sendPacket(pkt);
}

static void sendRacerBurst(uint32_t runId, const char* racerName) {
    if (runId == 0 || racerName[0] == '\0') return;
    char pkt[PACKET_BUF_SZ];
    buildRacerPacket(runId, racerName, pkt, sizeof(pkt));
    sendPacket(pkt);
    delay(120);
    sendPacket(pkt);
}

static void sendResultBurst(const char* name, const char* uid, uint32_t runId, uint32_t startMs, uint32_t elapsedMs) {
    char pkt[PACKET_BUF_SZ];
    buildResultPacket(name, uid, runId, startMs, elapsedMs, pkt, sizeof(pkt));
    sendPacket(pkt);
    delay(120);
    sendPacket(pkt);
}

// CHANGE: reset/sync packet for Unit B
static void sendResetBurst() {
    sendPacket("CMD RESET");
    delay(120);
    sendPacket("CMD RESET");
}

// ============================================================
// UI HELPERS
// ============================================================
static void ledsWaitForRfid() {
    digitalWrite(LED_YELLOW_P, HIGH);
    digitalWrite(LED_GREEN_P, LOW);
    digitalWrite(LED_RED_P, LOW);
}

static void ledsReady() {
    digitalWrite(LED_YELLOW_P, LOW);
    digitalWrite(LED_GREEN_P, HIGH);
    digitalWrite(LED_RED_P, LOW);
}

static void ledsTiming() {
    digitalWrite(LED_YELLOW_P, LOW);
    digitalWrite(LED_GREEN_P, LOW);
    digitalWrite(LED_RED_P, HIGH);
}

static void ledsWaitForStartArm() {
    uint32_t now = millis();

    if ((now - readyFlashLastToggleMs) >= READY_FLASH_MS) {
        readyFlashLastToggleMs = now;
        readyFlashState = !readyFlashState;
    }

    digitalWrite(LED_YELLOW_P, readyFlashState ? HIGH : LOW);
    digitalWrite(LED_GREEN_P, readyFlashState ? HIGH : LOW);
    digitalWrite(LED_RED_P, LOW);
}

static void setLcdLines(const char* line1, const char* line2) {
    char l1[LCD_LINE_BUF_SZ];
    char l2[LCD_LINE_BUF_SZ];

    snprintf(l1, sizeof(l1), "%-16.16s", line1 ? line1 : "");
    snprintf(l2, sizeof(l2), "%-16.16s", line2 ? line2 : "");

    if (strncmp(l1, lcdLine1Cache, 16) == 0 && strncmp(l2, lcdLine2Cache, 16) == 0) {
        return;
    }

    strncpy(lcdLine1Cache, l1, sizeof(lcdLine1Cache));
    lcdLine1Cache[sizeof(lcdLine1Cache) - 1] = '\0';

    strncpy(lcdLine2Cache, l2, sizeof(lcdLine2Cache));
    lcdLine2Cache[sizeof(lcdLine2Cache) - 1] = '\0';

    lcd.clear();
    lcd.setCursor(0, 0);
    lcd.print(l1);
    lcd.setCursor(0, 1);
    lcd.print(l2);
}

static void showScanPrompt() {
    setLcdLines("Start Unit Ready", "Scan RFID");
}

static void showRunnerWaiting() {
    setLcdLines(activeRacerName, "Wait PIR LOW...");
}

static void showStartArmed() {
    setLcdLines(activeRacerName, "Cross to begin");
}

static void showRunning() {
    setLcdLines(activeRacerName, "Running...");
}

static void showCancelled() {
    setLcdLines("Run Cancelled", "Scan RFID");
}

static void showTimeout(const char* top, const char* bottom) {
    setLcdLines(top, bottom);
}

static void showElapsedTime(uint32_t elapsedMs) {
    char timeBuf[TIME_BUF_SZ];
    formatElapsedTime(elapsedMs, timeBuf, sizeof(timeBuf));
    setLcdLines(activeRacerName, timeBuf);
}

static void formatElapsedTime(uint32_t elapsedMs, char* out, size_t outSize) {
    uint32_t ms = elapsedMs % 1000;
    uint32_t totalSeconds = elapsedMs / 1000;
    uint32_t seconds = totalSeconds % 60;
    uint32_t totalMinutes = totalSeconds / 60;
    uint32_t minutes = totalMinutes % 60;
    uint32_t hours = totalMinutes / 60;

    if (hours > 0) {
        snprintf(out, outSize, "%02lu:%02lu:%02lu:%03lu",
            (unsigned long)hours,
            (unsigned long)minutes,
            (unsigned long)seconds,
            (unsigned long)ms);
    }
    else {
        snprintf(out, outSize, "%02lu:%02lu:%03lu",
            (unsigned long)minutes,
            (unsigned long)seconds,
            (unsigned long)ms);
    }
}

// ============================================================
// RUN HELPERS
// ============================================================
static bool detectStartMovement() {
    uint32_t now = millis();
    bool pir = digitalRead(SENSOR_P);

    if (!startPirArmed) {
        if (!pir) {
            if (startPirLowSinceMs == 0) startPirLowSinceMs = now;

            if ((now - startPirLowSinceMs) >= START_LOW_VERIFY_MS) {
                startPirArmed = true;
                pirWasHigh = false;
                pirHighStartMs = 0;
                lastPirTriggerMs = 0;

                DBG_PRINTLN("Start PIR verified LOW -> sensor armed");
                showStartArmed();
            }
        }
        else {
            startPirLowSinceMs = 0;
        }
        return false;
    }

    if ((now - lastPirTriggerMs) < PIR_REARM_MS) return false;

    if (pir && !pirWasHigh) {
        pirWasHigh = true;
        pirHighStartMs = now;
    }

    if (!pir) {
        pirWasHigh = false;
        return false;
    }

    if (pirWasHigh && (now - pirHighStartMs) >= PIR_STABLE_MS) {
        lastPirTriggerMs = now;
        pirWasHigh = false;
        return true;
    }

    return false;
}

static void cancelCurrentRun(const char* reason, bool sendRadioCancel) {
    DBG_PRINT("CANCEL: ");
    DBG_PRINTLN(reason);

    if (sendRadioCancel && currentRunId != 0) {
        char pkt[PACKET_BUF_SZ];
        buildCmdCancel(currentRunId, pkt, sizeof(pkt));
        DBG_PRINT("Sending ");
        DBG_PRINTLN(pkt);
        sendCancelBurst(currentRunId);
    }

    // CHANGE: always hard-reset Unit B too
    sendResetBurst();

    resetForIdle();
    ledsWaitForRfid();
    showCancelled();
    delay(1000);
    showScanPrompt();
}

static void completeRunFromStop(uint32_t stopRunId) {
    uint32_t tStopMs = millis();
    uint32_t elapsed = tStopMs - tStartMs;
    timerRunning = false;

    if (stopRunId != currentRunId) {
        DBG_PRINT("WARNING: STOP run ID ");
        DBG_PRINT(stopRunId);
        DBG_PRINT(" did not match current run ID ");
        DBG_PRINT(currentRunId);
        DBG_PRINTLN(" -> accepting STOP anyway");
    }

    char formattedTime[TIME_BUF_SZ];
    formatElapsedTime(elapsed, formattedTime, sizeof(formattedTime));

    DBG_PRINT("TIMER STOP (A) Run ");
    DBG_PRINT(currentRunId);
    DBG_PRINT(" Racer ");
    DBG_PRINT(activeRacerName);
    DBG_PRINT(" StartMs ");
    DBG_PRINT(startStampMs);
    DBG_PRINT(" Elapsed ");
    DBG_PRINTLN(formattedTime);

    char resultLine[PACKET_BUF_SZ];
    buildResultPacket(activeRacerName, activeRacerUid, currentRunId, startStampMs, elapsed, resultLine, sizeof(resultLine));
    DBG_PRINTLN(resultLine);

    sendResultBurst(activeRacerName, activeRacerUid, currentRunId, startStampMs, elapsed);
    showElapsedTime(elapsed);

    char timePkt[PACKET_BUF_SZ];
    buildTimePacket(currentRunId, elapsed, timePkt, sizeof(timePkt));
    sendPacket(timePkt);

    resetForIdle();
}

// ============================================================
// DEBUG
// ============================================================
static void printUID() {
    DBG_PRINT("UID: ");
    for (byte i = 0; i < rfid.uid.size; i++) {
        if (rfid.uid.uidByte[i] < 0x10) {
            DBG_PRINT('0');
        }
        DBG_PRINT(rfid.uid.uidByte[i], HEX);
        if (i < rfid.uid.size - 1) {
            DBG_PRINT(':');
        }
    }
    DBG_PRINTLN("");
}
Editor is loading...
Leave a Comment