Untitled

 avatar
unknown
c_cpp
2 months ago
23 kB
5
Indexable
#include <Arduino.h>
#include <SPI.h>
#include <RadioLib.h>
#include <stdlib.h>
#include <string.h>
#include <stdio.h>

// ============================================================
// DEBUG CONTROL
// ============================================================
static const bool DEBUG_ENABLE = true;  // set to false to disable debug prints

// These macros check DEBUG_ENABLE before printing, to avoid the overhead of string formatting when debug is off
#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 / RADIO SETTINGS
// ============================================================
#define LORA_SCK   14   // Serial Clock
#define LORA_MISO  35   // Master In Slave Out
#define LORA_MOSI  13   // Master Out Slave In
#define LORA_CS     5   // Chip Select
#define LORA_DIO1  25   // DIO1 for SX1262 (used for RX done interrupt)
#define LORA_RST   27   // Reset pin
#define LORA_BUSY  26   // Busy pin

#define PIR_PIN    21   // PIR sensor input pin

// UART pins from Unit B -> XIAO server
#define FREENOVE_TX   17    // FREENOVE board TX pin (to XIAO RX)
#define FREENOVE_RX   16    // FREENOVE board RX pin (to XIAO TX)

static const float LORA_FREQ_MHZ = 915.0f;  // LoRa frequency in MHz (adjust as needed for your region)
static const int   LORA_SF = 9;             // LoRa Spreading Factor (7-12, higher is slower but longer range)
static const float LORA_BW_KHZ = 62.5f;     // LoRa Bandwidth in kHz (125, 250, 500 are common values)
static const int   LORA_CR = 5;             // LoRa Coding Rate (5-8, where 5 means 4/5, 6 means 4/6, etc.)
static const int   LORA_TX_DBM = 2;         // LoRa transmit power in dBm (adjust as needed, max is usually around 14-20dBm depending on the module)

// ============================================================
// PIR FILTER SETTINGS
// ============================================================
static const uint32_t PIR_STABLE_MS = 250;  // Time that PIR must be continuously high to consider it a valid trigger
static const uint32_t PIR_REARM_MS = 1500;  // Minimum time between valid PIR triggers (to prevent multiple triggers from one event)

static const uint32_t ARMED_TIMEOUT_MS = 600000;        // Time to wait in ARMED state before giving up and returning to IDLE (e.g. 10 minutes)
static const uint32_t WAIT_RESULT_TIMEOUT_MS = 5000;    // Time to wait for RESULT packet after sending CMD STOP before giving up and returning to IDLE (e.g. 5 seconds)

#define NAME_BUF_SZ   32    // Buffer size for racer names
#define PACKET_BUF_SZ 160   // Buffer size for building/parsing radio packets (should be large enough to hold the longest expected packet)
#define TIME_BUF_SZ   24    // Buffer size for formatted time strings

// ============================================================
// OBJECTS
// ============================================================
SX1262 radio = new Module(LORA_CS, LORA_DIO1, LORA_RST, LORA_BUSY); // LoRa radio object
HardwareSerial ServerUART(1);   // Dedicated UART for communication with XIAO server (using Serial1 on ESP32)

// ============================================================
// STATE
// ============================================================
enum B_MODE {
    B_IDLE,
    B_ARMED,
    B_WAIT_RESULT
};

static B_MODE bMode = B_IDLE;

static uint32_t stateEnterMs = 0;   // Timestamp of when we entered the current state, used for timeouts
static uint32_t activeRunId = 0;    // The run ID of the currently active run (set when we receive CMD START, cleared when we go idle)

static bool pirWasHigh = false;
static uint32_t pirHighStartMs = 0; // Timestamp of when the PIR sensor was first detected as high, used for stable trigger detection
static uint32_t lastTriggerMs = 0;  // Timestamp of the last valid PIR trigger, used for re-arming logic

static char activeRacerName[NAME_BUF_SZ] = { 0 };   // The name of the racer for the active run, if provided by Unit A

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

static void buildAckStart(uint32_t runId, char* out, size_t outSize);
static void buildCmdStop(uint32_t runId, char* out, size_t outSize);
static void buildAckPing(uint32_t runId, char* out, size_t outSize);

static bool parseCmdStart(const char* rx, uint32_t& runIdOut);
static bool parseCmdCancel(const char* rx, uint32_t& runIdOut);
static bool parseCmdReset(const char* rx);
static bool parseTimePacket(const char* rx, uint32_t& runIdOut, uint32_t& elapsedOut);
static bool parseRacerPacket(const char* rx, uint32_t& runIdOut, char* nameOut, size_t nameOutSize);
static bool parsePingPacket(const char* rx, uint32_t& runIdOut);
static bool parseResultPacket(const char* rx,
    char* nameOut, size_t nameOutSize,
    char* uidOut, size_t uidOutSize,
    uint32_t& runIdOut,
    uint32_t& startMsOut,
    uint32_t& elapsedMsOut,
    char* formattedTimeOut, size_t formattedTimeOutSize);

static void sendPacket(const char* s);
static void sendStartAckBurst(uint32_t runId);
static void resetPirState();
static void goIdle();
static bool pirTriggered();

static void processIncomingRadio();
static void handleIdle();
static void handleArmed();
static void handleWaitResult();

static void printLoggerRecord(const char* rawLine,
    const char* name,
    const char* uid,
    uint32_t runId,
    uint32_t startMs,
    uint32_t elapsedMs,
    const char* formattedTime);

// ============================================================
// SETUP
// ============================================================
void setup() {
    // USB serial for debugging
    Serial.begin(115200);

    // Dedicated UART to XIAO server
    ServerUART.begin(115200, SERIAL_8N1, FREENOVE_RX, FREENOVE_TX);

    delay(200);

	setupPins();    // Set pin modes
	setupRadio();   // Initialize LoRa radio
	goIdle();       // Initialize state

    DBG_PRINTF("Unit B UART -> XIAO ready @115200 (RX=%d TX=%d)\n", FREENOVE_RX, FREENOVE_TX);
    DBG_PRINTLN("Unit B ready. Waiting for CMD START...");
}

// ============================================================
// LOOP
// ============================================================
void loop() {
	processIncomingRadio(); // Check for and process any incoming radio packets

	// State machine handling
    switch (bMode) {
    case B_IDLE:
		handleIdle();   // In idle state, we just wait for incoming commands. No timeouts or PIR checks needed.
        break;

    case B_ARMED:
		handleArmed();  // In armed state, we check for PIR triggers and also handle timeout to return to idle if no trigger occurs within the timeout period.
        break;

    case B_WAIT_RESULT:
		handleWaitResult(); // In wait result state, we just wait for the RESULT packet from Unit A. 
                            // We also handle a timeout to return to idle if the RESULT doesn't arrive within the expected time.
        break;
    }

	delay(5);   // Small delay to avoid tight loop and give time for other tasks (like radio interrupts) to run
}

// ============================================================
// SETUP HELPERS
// ============================================================

// Set pin modes for PIR sensor and any other necessary pins
static void setupPins() {
    pinMode(PIR_PIN, INPUT);
}

// Initialize the LoRa radio module with the specified settings and start it in receive mode
static void setupRadio() {
    SPI.begin(LORA_SCK, LORA_MISO, LORA_MOSI, LORA_CS);

    int state = radio.begin(LORA_FREQ_MHZ);
    if (state != RADIOLIB_ERR_NONE) {
        DBG_PRINT("BOOT FAIL: ");
        DBG_PRINTLN(state);
        while (true) {
            delay(1000);
        }
    }

    radio.setSpreadingFactor(LORA_SF);  // Set the spreading factor for the LoRa radio
    radio.setBandwidth(LORA_BW_KHZ);    // Set the bandwidth for the LoRa radio
    radio.setCodingRate(LORA_CR);       // Set the coding rate for the LoRa radio
    radio.setOutputPower(LORA_TX_DBM);  // Set the output power for the LoRa radio
    radio.startReceive();               // Start the radio in receive mode
}

// ============================================================
// PACKET BUILDERS
// ============================================================

// These functions build the various packets that Unit B needs to send back to Unit A based on the protocol defined in the prompt.
// They take the necessary parameters (like runId) and format them into the expected string format for transmission over LoRa.
static void buildAckStart(uint32_t runId, char* out, size_t outSize) {
    snprintf(out, outSize, "ACK START %lu", (unsigned long)runId);
}

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

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

// ============================================================
// PARSERS
// ============================================================

// These functions parse incoming strings from the LoRa radio and extract the relevant 
// information based on the expected packet formats defined in the prompt.
// They return true if the parsing was successful and the packet matches the expected format, and false otherwise.
static bool parseCmdStart(const char* rx, uint32_t& runIdOut) {
    if (strncmp(rx, "CMD START ", 10) != 0) return false;
    runIdOut = strtoul(rx + 10, nullptr, 10);
    return true;
}

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

static bool parseCmdReset(const char* rx) {
    return strcmp(rx, "CMD RESET") == 0;
}

// Expected format: "TIME <runId> <elapsedMs>"
static bool parseTimePacket(const char* rx, uint32_t& runIdOut, uint32_t& elapsedOut) {
	// Check if the packet starts with "TIME "
    if (strncmp(rx, "TIME ", 5) != 0) return false;

	const char* p = rx + 5; // Move past "TIME "
	char* endPtr = nullptr; // strtoul will set endPtr to point to the character after the number it parsed

	// Parse runId
    runIdOut = strtoul(p, &endPtr, 10);
    if (!endPtr || *endPtr != ' ') return false;

	// Parse elapsed time
    elapsedOut = strtoul(endPtr + 1, nullptr, 10);
    return true;
}

// Expected format: "RACER <runId> <name>"
static bool parseRacerPacket(const char* rx, uint32_t& runIdOut, char* nameOut, size_t nameOutSize) {
    if (strncmp(rx, "RACER ", 6) != 0) return false;

    const char* p = rx + 6;
    char* endPtr = nullptr;

    runIdOut = strtoul(p, &endPtr, 10);
    if (!endPtr || *endPtr != ' ') return false;

    strncpy(nameOut, endPtr + 1, nameOutSize - 1);
    nameOut[nameOutSize - 1] = '\0';
    return true;
}

// Expected format: "PING <runId>"
static bool parsePingPacket(const char* rx, uint32_t& runIdOut) {
    if (strncmp(rx, "PING ", 5) != 0) return false;
    runIdOut = strtoul(rx + 5, nullptr, 10);
    return true;
}

// Expected format: "RESULT|<name>|<uid>|<runId>|<startMs>|<elapsedMs>|<formattedTime>"
static bool parseResultPacket(const char* rx,
    char* nameOut, size_t nameOutSize,
    char* uidOut, size_t uidOutSize,
    uint32_t& runIdOut,
    uint32_t& startMsOut,
    uint32_t& elapsedMsOut,
    char* formattedTimeOut, size_t formattedTimeOutSize) {
    if (strncmp(rx, "RESULT|", 7) != 0) return false;

    char temp[PACKET_BUF_SZ];
    strncpy(temp, rx, sizeof(temp) - 1);
    temp[sizeof(temp) - 1] = '\0';

    char* savePtr = nullptr;
    char* token = strtok_r(temp, "|", &savePtr);
    if (!token || strcmp(token, "RESULT") != 0) return false;

    token = strtok_r(nullptr, "|", &savePtr);
    if (!token) return false;
    strncpy(nameOut, token, nameOutSize - 1);
    nameOut[nameOutSize - 1] = '\0';

    token = strtok_r(nullptr, "|", &savePtr);
    if (!token) return false;
    strncpy(uidOut, token, uidOutSize - 1);
    uidOut[uidOutSize - 1] = '\0';

    token = strtok_r(nullptr, "|", &savePtr);
    if (!token) return false;
    runIdOut = strtoul(token, nullptr, 10);

    token = strtok_r(nullptr, "|", &savePtr);
    if (!token) return false;
    startMsOut = strtoul(token, nullptr, 10);

    token = strtok_r(nullptr, "|", &savePtr);
    if (!token) return false;
    elapsedMsOut = strtoul(token, nullptr, 10);

    token = strtok_r(nullptr, "|", &savePtr);
    if (!token) return false;
    strncpy(formattedTimeOut, token, formattedTimeOutSize - 1);
    formattedTimeOut[formattedTimeOutSize - 1] = '\0';

    return true;
}

// ============================================================
// HELPERS
// ============================================================

// This function sends a string packet over the LoRa radio and prints debug information about the transmission result.
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();
}

// To improve reliability of the ACK START response, we send it in a burst of 3 packets with short delays in between.
// This increases the chances that Unit A receives at least one of them, especially if there is interference or 
// if Unit A starts listening slightly after we send.
static void sendStartAckBurst(uint32_t runId) {
    char pkt[PACKET_BUF_SZ];
    buildAckStart(runId, pkt, sizeof(pkt));
    sendPacket(pkt);
    delay(120);
    sendPacket(pkt);
    delay(120);
    sendPacket(pkt);
}

// This function resets the PIR state variables to their initial values. It is called when we enter
// the ARMED state and also when we return to IDLE, to ensure that we start fresh for each run and 
// avoid any leftover state from previous runs.
static void resetPirState() {
    pirWasHigh = false;
    pirHighStartMs = 0;
    lastTriggerMs = 0;
}

// This function checks the PIR sensor and implements the logic to determine if a valid trigger has occurred.
static void goIdle() {
    bMode = B_IDLE;
    activeRunId = 0;
    activeRacerName[0] = '\0';
    stateEnterMs = millis();
    resetPirState();
}

// This function implements a simple state machine to filter the raw PIR sensor input and determine when a valid trigger has occurred.
static bool pirTriggered() {
    uint32_t now = millis();
    bool pir = digitalRead(PIR_PIN);

    if ((now - lastTriggerMs) < 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) {
        lastTriggerMs = now;
        pirWasHigh = false;
        return true;
    }

    return false;
}

// ============================================================
// RADIO PROCESSING
// ============================================================

// This function checks for incoming radio packets, parses them, and takes the appropriate actions based on the packet type and content.
static void processIncomingRadio() {
    String rxStr;
    int r = radio.readData(rxStr);
    if (r != RADIOLIB_ERR_NONE || rxStr.length() == 0) return;

    const char* rx = rxStr.c_str();

    uint32_t rxRunId = 0;
    uint32_t rxElapsed = 0;
    char rxName[NAME_BUF_SZ];

    char resultName[NAME_BUF_SZ];
    char resultUid[24];
    uint32_t resultRunId = 0;
    uint32_t resultStartMs = 0;
    uint32_t resultElapsedMs = 0;
    char resultFormattedTime[TIME_BUF_SZ];

    // Reset can interrupt any state
    if (parseCmdReset(rx)) {
        DBG_PRINTLN("RESET received -> returning to idle");
        goIdle();
        return;
    }

	// CMD START can be received in any state, but we only act on it if it's for a new run. 
    // If it's for the same run that's already active, we ignore it (but still send ACK burst)
    // to avoid disrupting an ongoing run.
    if (parseCmdStart(rx, rxRunId)) {
        activeRunId = rxRunId;
        bMode = B_ARMED;
        stateEnterMs = millis();
        resetPirState();

        DBG_PRINT("ARMED (B): received START for run ");
        DBG_PRINT(activeRunId);
        DBG_PRINTLN(". Waiting for PIR...");

        sendStartAckBurst(activeRunId);
        return;
    }

	// RACER packet can be received in any state, but we only store the racer name if it's for the currently active run.
    if (parseRacerPacket(rx, rxRunId, rxName, sizeof(rxName))) {
        if (rxRunId == activeRunId) {
            strncpy(activeRacerName, rxName, sizeof(activeRacerName) - 1);
            activeRacerName[sizeof(activeRacerName) - 1] = '\0';

            DBG_PRINT("Stored racer for run ");
            DBG_PRINT(rxRunId);
            DBG_PRINT(": ");
            DBG_PRINTLN(activeRacerName);
        }
        return;
    }

	// PING can be received in any state, but we only respond to it if it's for the currently active run and we're in ARMED state.
    if (parsePingPacket(rx, rxRunId)) {
        if (rxRunId == activeRunId && bMode == B_ARMED) {
            DBG_PRINT("PING received for run ");
            DBG_PRINTLN(rxRunId);

            char pkt[PACKET_BUF_SZ];
            buildAckPing(rxRunId, pkt, sizeof(pkt));
            sendPacket(pkt);

            stateEnterMs = millis();
        }
        return;
    }

	// CMD CANCEL can be received in any state, but we only act on it if it's for the currently active run. 
    // If we receive a cancel for a different run, we ignore it.
    if (parseCmdCancel(rx, rxRunId)) {
        if (rxRunId == activeRunId) {
            DBG_PRINT("CANCEL received for run ");
            DBG_PRINTLN(rxRunId);
            goIdle();
        }
        return;
    }

	// TIME packet can be received in any state, but we only act on it if it's for the currently active run.
    if (parseTimePacket(rx, rxRunId, rxElapsed)) {
        if (rxRunId == activeRunId) {
            DBG_PRINT("FINAL TIME for run ");
            DBG_PRINT(rxRunId);
            DBG_PRINT(" | Racer ");
            DBG_PRINT(activeRacerName[0] ? activeRacerName : "UNKNOWN");
            DBG_PRINT(" | Elapsed ");
            DBG_PRINT(rxElapsed);
            DBG_PRINTLN(" ms");

            goIdle();
        }
        return;
    }

	// RESULT can be received in any state, but we only act on it if it's for the currently active run and we're in either ARMED or WAIT_RESULT state.
    if (parseResultPacket(rx,
        resultName, sizeof(resultName),
        resultUid, sizeof(resultUid),
        resultRunId,
        resultStartMs,
        resultElapsedMs,
        resultFormattedTime, sizeof(resultFormattedTime))) {

        if (resultRunId == activeRunId && (bMode == B_WAIT_RESULT || bMode == B_ARMED)) {
            DBG_PRINT("RESULT received for active run ");
            DBG_PRINTLN(resultRunId);

            printLoggerRecord(rx,
                resultName,
                resultUid,
                resultRunId,
                resultStartMs,
                resultElapsedMs,
                resultFormattedTime);

            goIdle();
        }
        else {
            DBG_PRINT("Ignored RESULT for run ");
            DBG_PRINT(resultRunId);
            DBG_PRINT(" while active run is ");
            DBG_PRINTLN(activeRunId);
        }
        return;
    }

    DBG_PRINT("RX: ");
    DBG_PRINTLN(rx);
}

// ============================================================
// STATE HANDLERS
// ============================================================

// These functions handle the logic for each state of Unit B. They are called from the main loop based on the current state.
static void handleIdle() {
    // idle state
}

// In the ARMED state, we check for PIR triggers and also handle a timeout to return to idle if no trigger occurs within the timeout period.
static void handleArmed() {
    if ((millis() - stateEnterMs) >= ARMED_TIMEOUT_MS) {
        DBG_PRINTLN("ARM TIMEOUT (B) -> returning to idle");
        goIdle();
        return;
    }

    if (!pirTriggered()) return;

    DBG_PRINT("PIR triggered (B) -> sending ONE STOP for run ");
    DBG_PRINT(activeRunId);
    if (activeRacerName[0]) {
        DBG_PRINT(" | Racer ");
        DBG_PRINT(activeRacerName);
    }
    DBG_PRINTLN("");

    char pkt[PACKET_BUF_SZ];
    buildCmdStop(activeRunId, pkt, sizeof(pkt));
    sendPacket(pkt);

    // stay quiet and wait for RESULT from Unit A
    bMode = B_WAIT_RESULT;
    stateEnterMs = millis();
}

// In the WAIT_RESULT state, we just wait for the RESULT packet from Unit A. We also handle a timeout to return to 
// idle if the RESULT doesn't arrive within the expected time.
static void handleWaitResult() {
    if ((millis() - stateEnterMs) >= WAIT_RESULT_TIMEOUT_MS) {
        DBG_PRINTLN("WAIT_RESULT TIMEOUT (B) -> returning to idle");
        goIdle();
    }
}

// ============================================================
// LOGGER / SERVER FORWARDING
// ============================================================

// This function prints the details of a RESULT record to the debug console and also forwards the raw RESULT line to the XIAO server over UART.
static void printLoggerRecord(const char* rawLine,
    const char* name,
    const char* uid,
    uint32_t runId,
    uint32_t startMs,
    uint32_t elapsedMs,
    const char* formattedTime) {

    // Forward clean RESULT line to XIAO server
    ServerUART.println(rawLine);
    ServerUART.flush();

    DBG_PRINTLN("========== LOGGER RECORD ==========");
    DBG_PRINT("Name: ");
    DBG_PRINTLN(name);
    DBG_PRINT("UID: ");
    DBG_PRINTLN(uid);
    DBG_PRINT("Run ID: ");
    DBG_PRINTLN(runId);
    DBG_PRINT("StartMs: ");
    DBG_PRINTLN(startMs);
    DBG_PRINT("ElapsedMs: ");
    DBG_PRINTLN(elapsedMs);
    DBG_PRINT("Formatted: ");
    DBG_PRINTLN(formattedTime);
    DBG_PRINT("UART LINE: ");
    DBG_PRINTLN(rawLine);
    DBG_PRINT("CSV: ");
    DBG_PRINT(name);
    DBG_PRINT(",");
    DBG_PRINT(uid);
    DBG_PRINT(",");
    DBG_PRINT(runId);
    DBG_PRINT(",");
    DBG_PRINT(startMs);
    DBG_PRINT(",");
    DBG_PRINT(elapsedMs);
    DBG_PRINT(",");
    DBG_PRINTLN(formattedTime);
    DBG_PRINTLN("===================================");
}
Editor is loading...
Leave a Comment