Untitled
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