diff --git a/exercises/01_lora_ascii_pingpong/README.md b/exercises/01_lora_ascii_pingpong/README.md deleted file mode 100644 index b769466..0000000 --- a/exercises/01_lora_ascii_pingpong/README.md +++ /dev/null @@ -1,249 +0,0 @@ -# Exercise: LoRa Transmission Validation (SX1262) - -## Overview - -This exercise validates raw LoRa packet transmission from the **LILYGO T-Beam SUPREME V3.0** using the onboard **SX1262** radio. - -The objective is to: - -1. Transmit deterministic LoRa packets at known parameters. -2. Confirm successful reception using: - - * A second T-Beam - * A Waveshare SX1303 concentrator sniffer - * Or any SDR/LoRa receiver configured with identical PHY settings -3. Verify correct alignment of frequency, spreading factor, bandwidth, and coding rate. - -This is a **PHY-layer validation exercise**, not LoRaWAN. - ---- - -## Hardware - -### Transmitter - -* Board: **LILYGO T-Beam SUPREME V3.0** -* MCU: ESP32-S3 -* Radio: SX1262 -* Antenna: 915 MHz tuned antenna -* Power: USB-C or 18650 battery - -### Receiver / Sniffer - -Any device capable of raw LoRa reception with manual PHY configuration: - -* Second T-Beam (SX1262) -* Waveshare SX1303 + `lora_pkt_fwd` -* SDR with LoRa demodulator - ---- - -## LoRa Radio Specifications - -The sniffer **must** match these parameters exactly. - -| Parameter | Value | -| ---------------- | ------------------ | -| Radio Chip | SX1262 | -| Frequency | **915.000 MHz** | -| Modulation | LoRa | -| Bandwidth | **125 kHz** | -| Spreading Factor | **SF8** | -| Coding Rate | **4/5** | -| Preamble Length | 8 symbols | -| Sync Word | 0x12 (Public LoRa) | -| CRC | Enabled | -| IQ Inversion | Disabled | -| Output Power | 14 dBm (default) | - ---- - -## Important Notes for Sniffer Operators - -### 1. Frequency - -Ensure your sniffer JSON or configuration file contains: - -``` -"freq": 915000000 -``` - -If using SX130x HAL: - -``` -915000000 -``` - -No offset. No channel hopping. - ---- - -### 2. Spreading Factor - -Must be: - -``` -SF8 -``` - -If the sniffer is set to multi-SF mode, confirm that SF8 is enabled. - ---- - -### 3. Bandwidth - -``` -125000 Hz -``` - -Not 250 kHz. Not 500 kHz. - ---- - -### 4. Coding Rate - -``` -4/5 -``` - -Some interfaces represent this as: - -``` -CR = 1 -``` - ---- - -### 5. Sync Word - -If your sniffer filters on sync word: - -``` -0x12 -``` - -This is the public LoRa sync word (not LoRaWAN private). - ---- - -## Expected Packet Behavior - -The transmitter: - -* Sends a short ASCII payload -* Repeats at a fixed interval -* Does not use LoRaWAN -* Does not use encryption -* Does not use MAC layer framing - -Sniffer output should display: - -* RSSI -* SNR -* SF8 -* BW125 -* Payload length matching transmitter - ---- - -## Confirming Correct Alignment - -A properly aligned sniffer will show: - -* Stable RSSI -* Correct SF detection (SF8) -* Clean CRC pass -* No excessive packet loss at short range - -If you see: - -* No packets → Check frequency mismatch first. -* Packets but CRC fail → Check bandwidth mismatch. -* Packets only intermittently → Check spreading factor. - ---- - -## SX1262 SPI Mapping (T-Beam SUPREME) - -For reference, the radio is connected as follows: - -| Signal | ESP32-S3 Pin | -| ------ | ------------ | -| SCK | 12 | -| MISO | 13 | -| MOSI | 11 | -| CS | 10 | -| RESET | 5 | -| BUSY | 4 | -| DIO1 | 1 | - -These match the board’s hardware routing. - ---- - -## Build & Flash - -### PlatformIO - -1. Open project folder -2. Select correct environment -3. Compile -4. Upload via USB-C -5. Monitor serial output - -### Arduino IDE - -* Board: ESP32S3 Dev Module -* Flash: 8MB -* PSRAM: QSPI -* Upload speed: 921600 -* USB Mode: CDC and JTAG - ---- - -## Purpose of This Exercise - -This exercise verifies: - -* SPI communication with SX1262 -* Radio configuration correctness -* Antenna functionality -* Sniffer alignment -* Baseline RF performance - -It is intended as the foundational RF validation step before: - -* Reticulum interface integration -* microReticulum radio abstraction -* LoRa time-synchronized experiments -* Multi-node field testing - ---- - -## If You Cannot See Packets - -Work through this checklist: - -1. Confirm antenna attached. -2. Confirm sniffer at 915 MHz. -3. Confirm SF8. -4. Confirm BW125. -5. Reduce distance to < 2 meters. -6. Increase TX power to 17–20 dBm for testing. -7. Confirm no regional regulatory lock mismatch. - ---- - -## Relationship to `main.cpp` - -This README corresponds to the current exercise implementation in: - -``` -main.cpp -``` - -See source for definitive parameter values - -If you modify radio parameters in code, update this README accordingly. - - diff --git a/exercises/01_lora_ascii_pingpong/platformio.ini b/exercises/01_lora_ascii_pingpong/platformio.ini index 999de85..5ea81a2 100644 --- a/exercises/01_lora_ascii_pingpong/platformio.ini +++ b/exercises/01_lora_ascii_pingpong/platformio.ini @@ -3,7 +3,7 @@ ; $HeadURL$ [platformio] -default_envs = amy +default_envs = node_a [env] platform = espressif32 @@ -26,32 +26,12 @@ build_flags = -D ARDUINO_USB_MODE=1 -D ARDUINO_USB_CDC_ON_BOOT=1 -[env:amy] -extends = env +[env:node_a] build_flags = ${env.build_flags} - -D NODE_LABEL=\"Amy\" + -D NODE_LABEL=\"A\" -[env:bob] -extends = env +[env:node_b] build_flags = ${env.build_flags} - -D NODE_LABEL=\"Bob\" - -[env:cy] -extends = env -build_flags = - ${env.build_flags} - -D NODE_LABEL=\"Cy\" - -[env:dan] -extends = env -build_flags = - ${env.build_flags} - -D NODE_LABEL=\"Dan\" - -[env:ed] -extends = env -build_flags = - ${env.build_flags} - -D NODE_LABEL=\"Ed\" \ No newline at end of file + -D NODE_LABEL=\"B\" diff --git a/exercises/01_lora_ascii_pingpong/src/main.cpp b/exercises/01_lora_ascii_pingpong/src/main.cpp index b13fd1e..2b27af1 100644 --- a/exercises/01_lora_ascii_pingpong/src/main.cpp +++ b/exercises/01_lora_ascii_pingpong/src/main.cpp @@ -10,9 +10,6 @@ #ifndef NODE_LABEL #define NODE_LABEL "?" #endif -#ifndef UNIT_NAME - #define UNIT_NAME "UNNAMED" -#endif // --- Pins injected via platformio.ini build_flags --- #ifndef LORA_CS @@ -102,7 +99,7 @@ void loop() { next_tx_ms = now + 2000; // 2 seconds for this smoke test // String msg = String("I am ") + NODE_LABEL + " iter=" + String(iter++); - String msg = String("") + NODE_LABEL + " says hi. iter=" + String(iter++); + String msg = String(" ") + NODE_LABEL + " sends greetings. iter=" + String(iter++); Serial.printf("TX: %s\r\n", msg.c_str()); //int tx = radio.transmit(msg); diff --git a/exercises/12_FiveTalk/lib/startup_sd/StartupSdManager.cpp b/exercises/12_FiveTalk/lib/startup_sd/StartupSdManager.cpp deleted file mode 100644 index 1e8791c..0000000 --- a/exercises/12_FiveTalk/lib/startup_sd/StartupSdManager.cpp +++ /dev/null @@ -1,360 +0,0 @@ -#include "StartupSdManager.h" - -#include -#include "driver/gpio.h" - -StartupSdManager::StartupSdManager(Print& serial) : serial_(serial) {} - -bool StartupSdManager::begin(const SdWatcherConfig& cfg, SdStatusCallback callback) { - cfg_ = cfg; - callback_ = callback; - - forceSpiDeselected(); - dumpSdPins("very-early"); - - if (!initPmuForSdPower()) { - return false; - } - - cycleSdRail(); - delay(cfg_.startupWarmupMs); - - bool warmMounted = false; - for (uint8_t i = 0; i < 3; ++i) { - if (mountPreferred(false)) { - warmMounted = true; - break; - } - delay(200); - } - - // Some cards need a longer power/settle window after cold boot. - // Before declaring ABSENT, retry with extended settle and a full scan. - if (!warmMounted) { - logf("Watcher: startup preferred mount failed, retrying with extended settle"); - cycleSdRail(400, 1200); - delay(cfg_.startupWarmupMs + 1500); - warmMounted = mountCardFullScan(); - } - - if (warmMounted) { - setStateMounted(); - } else { - setStateAbsent(); - } - return true; -} - -void StartupSdManager::update() { - const uint32_t now = millis(); - const uint32_t pollInterval = - (watchState_ == SdWatchState::MOUNTED) ? cfg_.pollIntervalMountedMs : cfg_.pollIntervalAbsentMs; - - if ((uint32_t)(now - lastPollMs_) < pollInterval) { - return; - } - lastPollMs_ = now; - - if (watchState_ == SdWatchState::MOUNTED) { - if (verifyMountedCard()) { - presentVotes_ = 0; - absentVotes_ = 0; - return; - } - - if (mountPreferred(false) && verifyMountedCard()) { - presentVotes_ = 0; - absentVotes_ = 0; - return; - } - - absentVotes_++; - presentVotes_ = 0; - if (absentVotes_ >= cfg_.votesToAbsent) { - setStateAbsent(); - absentVotes_ = 0; - } - return; - } - - bool mounted = mountPreferred(false); - if (!mounted && (uint32_t)(now - lastFullScanMs_) >= cfg_.fullScanIntervalMs) { - lastFullScanMs_ = now; - if (cfg_.recoveryRailCycleOnFullScan) { - logf("Watcher: recovery rail cycle before full scan"); - cycleSdRail(cfg_.recoveryRailOffMs, cfg_.recoveryRailOnSettleMs); - delay(150); - } - logf("Watcher: preferred probe failed, running full scan"); - mounted = mountCardFullScan(); - } - - if (mounted) { - presentVotes_++; - absentVotes_ = 0; - if (presentVotes_ >= cfg_.votesToPresent) { - setStateMounted(); - presentVotes_ = 0; - } - } else { - absentVotes_++; - presentVotes_ = 0; - if (absentVotes_ >= cfg_.votesToAbsent) { - setStateAbsent(); - absentVotes_ = 0; - } - } -} - -bool StartupSdManager::consumeMountedEvent() { - bool out = mountedEventPending_; - mountedEventPending_ = false; - return out; -} - -bool StartupSdManager::consumeRemovedEvent() { - bool out = removedEventPending_; - removedEventPending_ = false; - return out; -} - -void StartupSdManager::logf(const char* fmt, ...) { - char msg[196]; - va_list args; - va_start(args, fmt); - vsnprintf(msg, sizeof(msg), fmt, args); - va_end(args); - serial_.printf("[%10lu][%06lu] %s\r\n", - (unsigned long)millis(), - (unsigned long)logSeq_++, - msg); -} - -void StartupSdManager::notify(SdEvent event, const char* message) { - if (callback_ != nullptr) { - callback_(event, message); - } -} - -void StartupSdManager::forceSpiDeselected() { - pinMode(tbeam_supreme::sdCs(), OUTPUT); - digitalWrite(tbeam_supreme::sdCs(), HIGH); - pinMode(tbeam_supreme::imuCs(), OUTPUT); - digitalWrite(tbeam_supreme::imuCs(), HIGH); -} - -void StartupSdManager::dumpSdPins(const char* tag) { - if (!cfg_.enablePinDumps) { - (void)tag; - return; - } - - const gpio_num_t cs = (gpio_num_t)tbeam_supreme::sdCs(); - const gpio_num_t sck = (gpio_num_t)tbeam_supreme::sdSck(); - const gpio_num_t miso = (gpio_num_t)tbeam_supreme::sdMiso(); - const gpio_num_t mosi = (gpio_num_t)tbeam_supreme::sdMosi(); - logf("PINS(%s): CS=%d SCK=%d MISO=%d MOSI=%d", - tag, gpio_get_level(cs), gpio_get_level(sck), gpio_get_level(miso), gpio_get_level(mosi)); -} - -bool StartupSdManager::initPmuForSdPower() { - if (!tbeam_supreme::initPmuForPeripherals(pmu_, &serial_)) { - logf("ERROR: PMU init failed"); - return false; - } - return true; -} - -void StartupSdManager::cycleSdRail(uint32_t offMs, uint32_t onSettleMs) { - if (!cfg_.enableSdRailCycle) { - return; - } - if (!pmu_) { - logf("SD rail cycle skipped: pmu=null"); - return; - } - - forceSpiDeselected(); - pmu_->disablePowerOutput(XPOWERS_BLDO1); - delay(offMs); - pmu_->setPowerChannelVoltage(XPOWERS_BLDO1, 3300); - pmu_->enablePowerOutput(XPOWERS_BLDO1); - delay(onSettleMs); -} - -bool StartupSdManager::tryMountWithBus(SPIClass& bus, const char* busName, uint32_t hz, bool verbose) { - SD.end(); - bus.end(); - delay(10); - forceSpiDeselected(); - - bus.begin(tbeam_supreme::sdSck(), tbeam_supreme::sdMiso(), tbeam_supreme::sdMosi(), tbeam_supreme::sdCs()); - digitalWrite(tbeam_supreme::sdCs(), HIGH); - delay(2); - for (int i = 0; i < 10; i++) { - bus.transfer(0xFF); - } - delay(2); - - if (verbose) { - logf("SD: trying bus=%s freq=%lu Hz", busName, (unsigned long)hz); - } - - if (!SD.begin(tbeam_supreme::sdCs(), bus, hz)) { - if (verbose) { - logf("SD: mount failed (possible non-FAT format, power, or bus issue)"); - } - return false; - } - - if (SD.cardType() == CARD_NONE) { - SD.end(); - return false; - } - - sdSpi_ = &bus; - sdBusName_ = busName; - sdFreq_ = hz; - return true; -} - -bool StartupSdManager::mountPreferred(bool verbose) { - return tryMountWithBus(sdSpiH_, "HSPI", 400000, verbose); -} - -bool StartupSdManager::mountCardFullScan() { - const uint32_t freqs[] = {400000, 1000000, 4000000, 10000000}; - - for (uint8_t i = 0; i < (sizeof(freqs) / sizeof(freqs[0])); ++i) { - if (tryMountWithBus(sdSpiH_, "HSPI", freqs[i], true)) { - logf("SD: card detected and mounted"); - return true; - } - } - for (uint8_t i = 0; i < (sizeof(freqs) / sizeof(freqs[0])); ++i) { - if (tryMountWithBus(sdSpiF_, "FSPI", freqs[i], true)) { - logf("SD: card detected and mounted"); - return true; - } - } - - logf("SD: begin() failed on all bus/frequency attempts"); - return false; -} - -bool StartupSdManager::verifyMountedCard() { - File root = SD.open("/", FILE_READ); - if (!root) { - return false; - } - root.close(); - return true; -} - -const char* StartupSdManager::cardTypeToString(uint8_t type) { - switch (type) { - case CARD_MMC: - return "MMC"; - case CARD_SD: - return "SDSC"; - case CARD_SDHC: - return "SDHC/SDXC"; - default: - return "UNKNOWN"; - } -} - -void StartupSdManager::printCardInfo() { - uint8_t cardType = SD.cardType(); - uint64_t cardSizeMB = SD.cardSize() / (1024ULL * 1024ULL); - uint64_t totalMB = SD.totalBytes() / (1024ULL * 1024ULL); - uint64_t usedMB = SD.usedBytes() / (1024ULL * 1024ULL); - - logf("SD type: %s", cardTypeToString(cardType)); - logf("SD size: %llu MB", cardSizeMB); - logf("FS total: %llu MB", totalMB); - logf("FS used : %llu MB", usedMB); - logf("SPI bus: %s @ %lu Hz", sdBusName_, (unsigned long)sdFreq_); -} - -bool StartupSdManager::ensureDirRecursive(const char* path) { - String full(path); - if (!full.startsWith("/")) { - full = "/" + full; - } - - int start = 1; - while (start > 0 && start < (int)full.length()) { - int slash = full.indexOf('/', start); - String partial = (slash < 0) ? full : full.substring(0, slash); - if (!SD.exists(partial.c_str()) && !SD.mkdir(partial.c_str())) { - logf("ERROR: mkdir failed for %s", partial.c_str()); - return false; - } - if (slash < 0) { - break; - } - start = slash + 1; - } - - return true; -} - -bool StartupSdManager::rewriteFile(const char* path, const char* payload) { - if (SD.exists(path) && !SD.remove(path)) { - logf("ERROR: failed to erase %s", path); - return false; - } - - File f = SD.open(path, FILE_WRITE); - if (!f) { - logf("ERROR: failed to create %s", path); - return false; - } - - size_t wrote = f.println(payload); - f.close(); - if (wrote == 0) { - logf("ERROR: write failed for %s", path); - return false; - } - return true; -} - -void StartupSdManager::permissionsDemo(const char* path) { - logf("Permissions demo: FAT has no Unix chmod/chown, use open mode only."); - File r = SD.open(path, FILE_READ); - if (!r) { - logf("Could not open %s as FILE_READ", path); - return; - } - size_t writeInReadMode = r.print("attempt write while opened read-only"); - if (writeInReadMode == 0) { - logf("As expected, FILE_READ write was blocked."); - } else { - logf("NOTE: FILE_READ write returned %u (unexpected)", (unsigned)writeInReadMode); - } - r.close(); -} - -void StartupSdManager::setStateMounted() { - if (watchState_ != SdWatchState::MOUNTED) { - logf("EVENT: card inserted/mounted"); - mountedEventPending_ = true; - notify(SdEvent::CARD_MOUNTED, "SD card mounted"); - } - watchState_ = SdWatchState::MOUNTED; -} - -void StartupSdManager::setStateAbsent() { - if (watchState_ == SdWatchState::MOUNTED) { - logf("EVENT: card removed/unavailable"); - removedEventPending_ = true; - notify(SdEvent::CARD_REMOVED, "SD card removed"); - } else if (watchState_ != SdWatchState::ABSENT) { - logf("EVENT: no card detected"); - notify(SdEvent::NO_CARD, "Missing SD card or invalid FAT16/FAT32 format"); - } - SD.end(); - watchState_ = SdWatchState::ABSENT; -} diff --git a/exercises/12_FiveTalk/lib/startup_sd/StartupSdManager.h b/exercises/12_FiveTalk/lib/startup_sd/StartupSdManager.h deleted file mode 100644 index be9ef27..0000000 --- a/exercises/12_FiveTalk/lib/startup_sd/StartupSdManager.h +++ /dev/null @@ -1,90 +0,0 @@ -#pragma once - -#include -#include -#include -#include -#include "tbeam_supreme_adapter.h" - -enum class SdWatchState : uint8_t { - UNKNOWN = 0, - ABSENT, - MOUNTED -}; - -enum class SdEvent : uint8_t { - NO_CARD, - CARD_MOUNTED, - CARD_REMOVED -}; - -using SdStatusCallback = void (*)(SdEvent event, const char* message); - -struct SdWatcherConfig { - bool enableSdRailCycle = true; - bool enablePinDumps = true; - bool recoveryRailCycleOnFullScan = true; - uint32_t recoveryRailOffMs = 250; - uint32_t recoveryRailOnSettleMs = 700; - uint32_t startupWarmupMs = 1500; - uint32_t pollIntervalAbsentMs = 1000; - uint32_t pollIntervalMountedMs = 2000; - uint32_t fullScanIntervalMs = 10000; - uint8_t votesToPresent = 2; - uint8_t votesToAbsent = 5; -}; - -class StartupSdManager { - public: - explicit StartupSdManager(Print& serial = Serial); - - bool begin(const SdWatcherConfig& cfg, SdStatusCallback callback = nullptr); - void update(); - - bool isMounted() const { return watchState_ == SdWatchState::MOUNTED; } - SdWatchState state() const { return watchState_; } - - bool consumeMountedEvent(); - bool consumeRemovedEvent(); - - void printCardInfo(); - bool ensureDirRecursive(const char* path); - bool rewriteFile(const char* path, const char* payload); - void permissionsDemo(const char* path); - - private: - void logf(const char* fmt, ...); - void notify(SdEvent event, const char* message); - void forceSpiDeselected(); - void dumpSdPins(const char* tag); - bool initPmuForSdPower(); - void cycleSdRail(uint32_t offMs = 250, uint32_t onSettleMs = 600); - bool tryMountWithBus(SPIClass& bus, const char* busName, uint32_t hz, bool verbose); - bool mountPreferred(bool verbose); - bool mountCardFullScan(); - bool verifyMountedCard(); - const char* cardTypeToString(uint8_t type); - void setStateMounted(); - void setStateAbsent(); - - Print& serial_; - SdWatcherConfig cfg_{}; - SdStatusCallback callback_ = nullptr; - - SPIClass sdSpiH_{HSPI}; - SPIClass sdSpiF_{FSPI}; - SPIClass* sdSpi_ = nullptr; - const char* sdBusName_ = "none"; - uint32_t sdFreq_ = 0; - XPowersLibInterface* pmu_ = nullptr; - - SdWatchState watchState_ = SdWatchState::UNKNOWN; - uint8_t presentVotes_ = 0; - uint8_t absentVotes_ = 0; - uint32_t lastPollMs_ = 0; - uint32_t lastFullScanMs_ = 0; - uint32_t logSeq_ = 0; - - bool mountedEventPending_ = false; - bool removedEventPending_ = false; -}; diff --git a/exercises/12_FiveTalk/lib/startup_sd/library.json b/exercises/12_FiveTalk/lib/startup_sd/library.json deleted file mode 100644 index 4978fdd..0000000 --- a/exercises/12_FiveTalk/lib/startup_sd/library.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "name": "startup_sd", - "version": "0.1.0", - "dependencies": [ - { - "name": "XPowersLib" - }, - { - "name": "Wire" - } - ] -} diff --git a/exercises/12_FiveTalk/platformio.ini b/exercises/12_FiveTalk/platformio.ini deleted file mode 100644 index 8bb9199..0000000 --- a/exercises/12_FiveTalk/platformio.ini +++ /dev/null @@ -1,79 +0,0 @@ -; 20260219 ChatGPT -; Exercise 12_FiveTalk - -[platformio] -default_envs = amy - -[env] -platform = espressif32 -framework = arduino -board = esp32-s3-devkitc-1 -monitor_speed = 115200 -extra_scripts = pre:scripts/set_build_epoch.py -lib_deps = - jgromes/RadioLib@^6.6.0 - lewisxhe/XPowersLib@0.3.3 - Wire - olikraus/U8g2@^2.36.4 - -build_flags = - -I ../../shared/boards - -I ../../external/microReticulum_Firmware - -D BOARD_MODEL=BOARD_TBEAM_S_V1 - -D OLED_SDA=17 - -D OLED_SCL=18 - -D OLED_ADDR=0x3C - -D GPS_RX_PIN=9 - -D GPS_TX_PIN=8 - -D GPS_WAKEUP_PIN=7 - -D GPS_1PPS_PIN=6 - -D LORA_CS=10 - -D LORA_MOSI=11 - -D LORA_SCK=12 - -D LORA_MISO=13 - -D LORA_RESET=5 - -D LORA_DIO1=1 - -D LORA_BUSY=4 - -D LORA_TCXO_VOLTAGE=1.8 - -D ARDUINO_USB_MODE=1 - -D ARDUINO_USB_CDC_ON_BOOT=1 - -[env:amy] -extends = env -build_flags = - ${env.build_flags} - -D NODE_LABEL=\"Amy\" - -D NODE_SHORT=\"A\" - -D NODE_SLOT_INDEX=0 - -[env:bob] -extends = env -build_flags = - ${env.build_flags} - -D NODE_LABEL=\"Bob\" - -D NODE_SHORT=\"B\" - -D NODE_SLOT_INDEX=1 - -[env:cy] -extends = env -build_flags = - ${env.build_flags} - -D NODE_LABEL=\"Cy\" - -D NODE_SHORT=\"C\" - -D NODE_SLOT_INDEX=2 - -[env:dan] -extends = env -build_flags = - ${env.build_flags} - -D NODE_LABEL=\"Dan\" - -D NODE_SHORT=\"D\" - -D NODE_SLOT_INDEX=3 - -[env:ed] -extends = env -build_flags = - ${env.build_flags} - -D NODE_LABEL=\"Ed\" - -D NODE_SHORT=\"E\" - -D NODE_SLOT_INDEX=4 diff --git a/exercises/12_FiveTalk/scripts/set_build_epoch.py b/exercises/12_FiveTalk/scripts/set_build_epoch.py deleted file mode 100644 index 3011129..0000000 --- a/exercises/12_FiveTalk/scripts/set_build_epoch.py +++ /dev/null @@ -1,12 +0,0 @@ -import time -Import("env") - -epoch = int(time.time()) -utc_tag = time.strftime("%Y%m%d_%H%M%S_z", time.gmtime(epoch)) - -env.Append( - CPPDEFINES=[ - ("FW_BUILD_EPOCH", str(epoch)), - ("FW_BUILD_UTC", '\\"%s\\"' % utc_tag), - ] -) diff --git a/exercises/12_FiveTalk/src/main.cpp b/exercises/12_FiveTalk/src/main.cpp deleted file mode 100644 index 0451083..0000000 --- a/exercises/12_FiveTalk/src/main.cpp +++ /dev/null @@ -1,920 +0,0 @@ -// 20260219 ChatGPT -// Exercise 12_FiveTalk - -#include -#include -#include -#include -#include -#include - -#include -#include -#include - -#include "StartupSdManager.h" -#include "tbeam_supreme_adapter.h" - -#ifdef SX1262 -#undef SX1262 -#endif - -#ifndef NODE_LABEL -#define NODE_LABEL "UNNAMED" -#endif - -#ifndef NODE_SHORT -#define NODE_SHORT "?" -#endif - -#ifndef NODE_SLOT_INDEX -#define NODE_SLOT_INDEX 0 -#endif - -#ifndef OLED_SDA -#define OLED_SDA 17 -#endif - -#ifndef OLED_SCL -#define OLED_SCL 18 -#endif - -#ifndef OLED_ADDR -#define OLED_ADDR 0x3C -#endif - -#ifndef RTC_I2C_ADDR -#define RTC_I2C_ADDR 0x51 -#endif - -#ifndef GPS_BAUD -#define GPS_BAUD 9600 -#endif - -#ifndef FILE_APPEND -#define FILE_APPEND FILE_WRITE -#endif - -#ifndef FW_BUILD_EPOCH -#define FW_BUILD_EPOCH 0 -#endif - -#ifndef FW_BUILD_UTC -#define FW_BUILD_UTC "unknown" -#endif - -#if (NODE_SLOT_INDEX < 0) || (NODE_SLOT_INDEX > 4) -#error "NODE_SLOT_INDEX must be 0..4" -#endif - -static const uint32_t kSerialDelayMs = 1000; -static const uint32_t kDisciplineMaxAgeSec = 24UL * 60UL * 60UL; -static const uint32_t kDisciplineRetryMs = 5000; -static const uint32_t kPpsWaitTimeoutMs = 1500; -static const uint32_t kSdMessagePeriodMs = 1200; -static const uint32_t kNoGpsMessagePeriodMs = 1500; -static const uint32_t kHealthCheckPeriodMs = 60000; -static const uint32_t kSlotSeconds = 2; - -static XPowersLibInterface* g_pmu = nullptr; -static StartupSdManager g_sd(Serial); -static U8G2_SH1106_128X64_NONAME_F_HW_I2C g_oled(U8G2_R0, U8X8_PIN_NONE); -static HardwareSerial g_gpsSerial(1); -static SX1262 g_radio = new Module(LORA_CS, LORA_DIO1, LORA_RESET, LORA_BUSY); - -static volatile bool g_rxFlag = false; -static volatile uint32_t g_ppsEdgeCount = 0; - -static uint32_t g_logSeq = 0; -static uint32_t g_lastWarnMs = 0; -static uint32_t g_lastDisciplineTryMs = 0; -static uint32_t g_lastHealthCheckMs = 0; - -static int64_t g_lastDisciplineEpoch = -1; -static int64_t g_lastTxEpochSecond = -1; -static uint32_t g_txCount = 0; - -static bool g_radioReady = false; -static bool g_sessionReady = false; -static bool g_gpsPathReady = false; - -static char g_sessionStamp[20] = {0}; -static char g_sentPath[64] = {0}; -static char g_recvPath[64] = {0}; - -static File g_sentFile; -static File g_recvFile; - -static char g_gpsLine[128]; -static size_t g_gpsLineLen = 0; - -struct DateTime { - uint16_t year; - uint8_t month; - uint8_t day; - uint8_t hour; - uint8_t minute; - uint8_t second; -}; - -struct GpsState { - bool sawAnySentence = false; - bool hasValidUtc = false; - uint8_t satsUsed = 0; - uint8_t satsInView = 0; - uint32_t lastUtcMs = 0; - DateTime utc{}; -}; - -static GpsState g_gps; - -enum class AppPhase : uint8_t { - WAIT_SD = 0, - WAIT_DISCIPLINE, - RUN -}; - -static AppPhase g_phase = AppPhase::WAIT_SD; - -static uint8_t bestSatelliteCount() { - return (g_gps.satsUsed > g_gps.satsInView) ? g_gps.satsUsed : g_gps.satsInView; -} - -static void logf(const char* fmt, ...) { - char msg[256]; - va_list args; - va_start(args, fmt); - vsnprintf(msg, sizeof(msg), fmt, args); - va_end(args); - Serial.printf("[%10lu][%06lu] %s\r\n", (unsigned long)millis(), (unsigned long)g_logSeq++, msg); -} - -static void oledShowLines(const char* l1, - const char* l2 = nullptr, - const char* l3 = nullptr, - const char* l4 = nullptr, - const char* l5 = nullptr) { - g_oled.clearBuffer(); - g_oled.setFont(u8g2_font_5x8_tf); - if (l1) g_oled.drawUTF8(0, 12, l1); - if (l2) g_oled.drawUTF8(0, 24, l2); - if (l3) g_oled.drawUTF8(0, 36, l3); - if (l4) g_oled.drawUTF8(0, 48, l4); - if (l5) g_oled.drawUTF8(0, 60, l5); - g_oled.sendBuffer(); -} - -static uint8_t toBcd(uint8_t v) { - return (uint8_t)(((v / 10U) << 4U) | (v % 10U)); -} - -static uint8_t fromBcd(uint8_t b) { - return (uint8_t)(((b >> 4U) * 10U) + (b & 0x0FU)); -} - -static bool isLeapYear(uint16_t y) { - return ((y % 4U) == 0U && (y % 100U) != 0U) || ((y % 400U) == 0U); -} - -static uint8_t daysInMonth(uint16_t year, uint8_t month) { - static const uint8_t kDays[12] = {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31}; - if (month == 2) return (uint8_t)(isLeapYear(year) ? 29 : 28); - if (month >= 1 && month <= 12) return kDays[month - 1]; - return 30; -} - -static bool isValidDateTime(const DateTime& dt) { - if (dt.year < 2000U || dt.year > 2099U) return false; - if (dt.month < 1 || dt.month > 12) return false; - if (dt.day < 1 || dt.day > daysInMonth(dt.year, dt.month)) return false; - if (dt.hour > 23 || dt.minute > 59 || dt.second > 59) return false; - return true; -} - -static int64_t daysFromCivil(int y, unsigned m, unsigned d) { - y -= (m <= 2); - const int era = (y >= 0 ? y : y - 399) / 400; - const unsigned yoe = (unsigned)(y - era * 400); - const unsigned doy = (153 * (m + (m > 2 ? (unsigned)-3 : 9)) + 2) / 5 + d - 1; - const unsigned doe = yoe * 365 + yoe / 4 - yoe / 100 + doy; - return era * 146097 + (int)doe - 719468; -} - -static int64_t toEpochSeconds(const DateTime& dt) { - int64_t days = daysFromCivil((int)dt.year, dt.month, dt.day); - return days * 86400LL + (int64_t)dt.hour * 3600LL + (int64_t)dt.minute * 60LL + (int64_t)dt.second; -} - -static bool fromEpochSeconds(int64_t sec, DateTime& out) { - if (sec < 0) return false; - - int64_t days = sec / 86400LL; - int64_t rem = sec % 86400LL; - if (rem < 0) { - rem += 86400LL; - days -= 1; - } - - out.hour = (uint8_t)(rem / 3600LL); - rem %= 3600LL; - out.minute = (uint8_t)(rem / 60LL); - out.second = (uint8_t)(rem % 60LL); - - days += 719468; - const int era = (days >= 0 ? days : days - 146096) / 146097; - const unsigned doe = (unsigned)(days - era * 146097); - const unsigned yoe = (doe - doe / 1460 + doe / 36524 - doe / 146096) / 365; - int y = (int)yoe + era * 400; - const unsigned doy = doe - (365 * yoe + yoe / 4 - yoe / 100); - const unsigned mp = (5 * doy + 2) / 153; - const unsigned d = doy - (153 * mp + 2) / 5 + 1; - const unsigned m = mp + (mp < 10 ? 3 : (unsigned)-9); - y += (m <= 2); - - out.year = (uint16_t)y; - out.month = (uint8_t)m; - out.day = (uint8_t)d; - return isValidDateTime(out); -} - -static bool rtcRead(DateTime& out, bool& lowVoltageFlag) { - Wire1.beginTransmission(RTC_I2C_ADDR); - Wire1.write(0x02); - if (Wire1.endTransmission(false) != 0) return false; - - const uint8_t need = 7; - uint8_t got = Wire1.requestFrom((int)RTC_I2C_ADDR, (int)need); - if (got != need) return false; - - uint8_t sec = Wire1.read(); - uint8_t min = Wire1.read(); - uint8_t hour = Wire1.read(); - uint8_t day = Wire1.read(); - (void)Wire1.read(); - uint8_t month = Wire1.read(); - uint8_t year = Wire1.read(); - - lowVoltageFlag = (sec & 0x80U) != 0; - out.second = fromBcd(sec & 0x7FU); - out.minute = fromBcd(min & 0x7FU); - out.hour = fromBcd(hour & 0x3FU); - out.day = fromBcd(day & 0x3FU); - out.month = fromBcd(month & 0x1FU); - - uint8_t yy = fromBcd(year); - bool century = (month & 0x80U) != 0; - out.year = century ? (1900U + yy) : (2000U + yy); - return true; -} - -static bool rtcWrite(const DateTime& dt) { - Wire1.beginTransmission(RTC_I2C_ADDR); - Wire1.write(0x02); - Wire1.write(toBcd(dt.second & 0x7FU)); - Wire1.write(toBcd(dt.minute)); - Wire1.write(toBcd(dt.hour)); - Wire1.write(toBcd(dt.day)); - Wire1.write(0x00); - - uint8_t monthReg = toBcd(dt.month); - if (dt.year < 2000U) monthReg |= 0x80U; - Wire1.write(monthReg); - Wire1.write(toBcd((uint8_t)(dt.year % 100U))); - - return Wire1.endTransmission() == 0; -} - -static bool getCurrentUtc(DateTime& dt, int64_t& epoch) { - bool lowV = false; - if (!rtcRead(dt, lowV)) return false; - if (lowV || !isValidDateTime(dt)) return false; - epoch = toEpochSeconds(dt); - return true; -} - -static void formatUtcHuman(const DateTime& dt, char* out, size_t outLen) { - snprintf(out, outLen, "%04u-%02u-%02u %02u:%02u:%02u UTC", - (unsigned)dt.year, - (unsigned)dt.month, - (unsigned)dt.day, - (unsigned)dt.hour, - (unsigned)dt.minute, - (unsigned)dt.second); -} - -static void formatUtcCompact(const DateTime& dt, char* out, size_t outLen) { - snprintf(out, - outLen, - "%04u%02u%02u_%02u%02u%02u", - (unsigned)dt.year, - (unsigned)dt.month, - (unsigned)dt.day, - (unsigned)dt.hour, - (unsigned)dt.minute, - (unsigned)dt.second); -} - -static bool parseUInt2(const char* s, uint8_t& out) { - if (!s || !isdigit((unsigned char)s[0]) || !isdigit((unsigned char)s[1])) return false; - out = (uint8_t)((s[0] - '0') * 10 + (s[1] - '0')); - return true; -} - -static void parseRmc(char* fields[], int count) { - if (count <= 9) return; - - const char* utc = fields[1]; - const char* status = fields[2]; - const char* date = fields[9]; - - if (!status || status[0] != 'A') return; - if (!utc || !date || strlen(utc) < 6 || strlen(date) < 6) return; - - uint8_t hh = 0, mm = 0, ss = 0; - uint8_t dd = 0, mo = 0, yy = 0; - if (!parseUInt2(utc + 0, hh) || !parseUInt2(utc + 2, mm) || !parseUInt2(utc + 4, ss)) return; - if (!parseUInt2(date + 0, dd) || !parseUInt2(date + 2, mo) || !parseUInt2(date + 4, yy)) return; - - g_gps.utc.hour = hh; - g_gps.utc.minute = mm; - g_gps.utc.second = ss; - g_gps.utc.day = dd; - g_gps.utc.month = mo; - g_gps.utc.year = (uint16_t)(2000U + yy); - g_gps.hasValidUtc = true; - g_gps.lastUtcMs = millis(); -} - -static void parseGga(char* fields[], int count) { - if (count <= 7) return; - int sats = atoi(fields[7]); - if (sats >= 0 && sats <= 255) g_gps.satsUsed = (uint8_t)sats; -} - -static void parseGsv(char* fields[], int count) { - if (count <= 3) return; - int sats = atoi(fields[3]); - if (sats >= 0 && sats <= 255) g_gps.satsInView = (uint8_t)sats; -} - -static void processNmeaLine(char* line) { - if (!line || line[0] != '$') return; - - g_gps.sawAnySentence = true; - char* star = strchr(line, '*'); - if (star) *star = '\0'; - - char* fields[24] = {0}; - int count = 0; - char* saveptr = nullptr; - char* tok = strtok_r(line, ",", &saveptr); - while (tok && count < 24) { - fields[count++] = tok; - tok = strtok_r(nullptr, ",", &saveptr); - } - if (count == 0 || !fields[0]) return; - - const char* header = fields[0]; - size_t n = strlen(header); - if (n < 6) return; - - const char* type = header + (n - 3); - if (strcmp(type, "RMC") == 0) parseRmc(fields, count); - else if (strcmp(type, "GGA") == 0) parseGga(fields, count); - else if (strcmp(type, "GSV") == 0) parseGsv(fields, count); -} - -static void pollGpsSerial() { - while (g_gpsSerial.available() > 0) { - char c = (char)g_gpsSerial.read(); - if (c == '\r') continue; - if (c == '\n') { - if (g_gpsLineLen > 0) { - g_gpsLine[g_gpsLineLen] = '\0'; - processNmeaLine(g_gpsLine); - g_gpsLineLen = 0; - } - continue; - } - - if (g_gpsLineLen + 1 < sizeof(g_gpsLine)) g_gpsLine[g_gpsLineLen++] = c; - else g_gpsLineLen = 0; - } -} - -static bool gpsUtcIsFresh() { - return g_gps.hasValidUtc && ((uint32_t)(millis() - g_gps.lastUtcMs) <= 2000U); -} - -static IRAM_ATTR void onPpsEdge() { - g_ppsEdgeCount++; -} - -static bool waitForNextPps(uint32_t timeoutMs) { - uint32_t startEdges = g_ppsEdgeCount; - uint32_t startMs = millis(); - while ((uint32_t)(millis() - startMs) < timeoutMs) { - pollGpsSerial(); - g_sd.update(); - if (g_ppsEdgeCount != startEdges) return true; - delay(2); - } - return false; -} - -static bool ensureGpsLogPathReady() { - if (!g_sd.isMounted()) { - g_gpsPathReady = false; - return false; - } - if (g_gpsPathReady) return true; - - if (!g_sd.ensureDirRecursive("/gps")) return false; - File f = SD.open("/gps/discipline_rtc.log", FILE_APPEND); - if (!f) return false; - f.close(); - g_gpsPathReady = true; - return true; -} - -static bool appendDisciplineLog(const DateTime& gpsUtc, int64_t rtcMinusGpsSeconds, bool hadPriorRtc) { - if (!ensureGpsLogPathReady()) return false; - - File f = SD.open("/gps/discipline_rtc.log", FILE_APPEND); - if (!f) return false; - - char ts[24]; - snprintf(ts, - sizeof(ts), - "%04u%02u%02u_%02u%02u%02u_z", - (unsigned)gpsUtc.year, - (unsigned)gpsUtc.month, - (unsigned)gpsUtc.day, - (unsigned)gpsUtc.hour, - (unsigned)gpsUtc.minute, - (unsigned)gpsUtc.second); - - char line[256]; - if (hadPriorRtc) { - snprintf(line, - sizeof(line), - "%s\t set RTC to GPS for FiveTalk\trtc-gps drift=%+lld s; sats=%u; fw_build_utc=%s", - ts, - (long long)rtcMinusGpsSeconds, - (unsigned)bestSatelliteCount(), - FW_BUILD_UTC); - } else { - snprintf(line, - sizeof(line), - "%s\t set RTC to GPS for FiveTalk\trtc-gps drift=RTC_unset; sats=%u; fw_build_utc=%s", - ts, - (unsigned)bestSatelliteCount(), - FW_BUILD_UTC); - } - - size_t wrote = f.println(line); - f.close(); - return wrote > 0; -} - -static bool disciplineRtcToGps() { - if (!gpsUtcIsFresh()) return false; - - DateTime prior{}; - bool lowV = false; - bool havePriorRtc = rtcRead(prior, lowV) && !lowV && isValidDateTime(prior); - - DateTime gpsSnap = g_gps.utc; - if (!waitForNextPps(kPpsWaitTimeoutMs)) return false; - - int64_t snapEpoch = toEpochSeconds(gpsSnap); - DateTime target{}; - if (!fromEpochSeconds(snapEpoch + 1, target)) return false; - if (!rtcWrite(target)) return false; - - int64_t driftSec = 0; - if (havePriorRtc) driftSec = toEpochSeconds(prior) - toEpochSeconds(target); - - if (!appendDisciplineLog(target, driftSec, havePriorRtc)) { - logf("WARN: Failed to append /gps/discipline_rtc.log"); - } - - g_lastDisciplineEpoch = toEpochSeconds(target); - char human[32]; - formatUtcHuman(target, human, sizeof(human)); - logf("RTC disciplined to GPS (%s), sats=%u", human, (unsigned)bestSatelliteCount()); - return true; -} - -static bool parseLogTimestampToken(const char* token, int64_t& epochOut) { - if (!token) return false; - - unsigned y = 0, m = 0, d = 0, hh = 0, mm = 0, ss = 0; - if (sscanf(token, "%4u%2u%2u_%2u%2u%2u", &y, &m, &d, &hh, &mm, &ss) != 6) return false; - - DateTime dt{}; - dt.year = (uint16_t)y; - dt.month = (uint8_t)m; - dt.day = (uint8_t)d; - dt.hour = (uint8_t)hh; - dt.minute = (uint8_t)mm; - dt.second = (uint8_t)ss; - if (!isValidDateTime(dt)) return false; - - epochOut = toEpochSeconds(dt); - return true; -} - -static bool loadLastDisciplineEpoch(int64_t& epochOut) { - epochOut = -1; - if (!g_sd.isMounted()) return false; - if (!SD.exists("/gps/discipline_rtc.log")) return false; - - File f = SD.open("/gps/discipline_rtc.log", FILE_READ); - if (!f) return false; - - while (f.available()) { - String line = f.readStringUntil('\n'); - line.trim(); - if (line.length() == 0) continue; - - int sep = line.indexOf('\t'); - String token = (sep >= 0) ? line.substring(0, sep) : line; - - char buf[32]; - size_t n = token.length(); - if (n >= sizeof(buf)) n = sizeof(buf) - 1; - memcpy(buf, token.c_str(), n); - buf[n] = '\0'; - - int64_t parsed = -1; - if (parseLogTimestampToken(buf, parsed)) epochOut = parsed; - } - - f.close(); - return epochOut >= 0; -} - -static bool isDisciplineStale() { - DateTime now{}; - int64_t nowEpoch = 0; - if (!getCurrentUtc(now, nowEpoch)) return true; - - int64_t lastEpoch = -1; - if (!loadLastDisciplineEpoch(lastEpoch)) { - if (g_lastDisciplineEpoch < 0) return true; - lastEpoch = g_lastDisciplineEpoch; - } - - g_lastDisciplineEpoch = lastEpoch; - if (lastEpoch < 0) return true; - - int64_t age = nowEpoch - lastEpoch; - return age < 0 || age > (int64_t)kDisciplineMaxAgeSec; -} - -static void readBattery(float& voltageV, bool& present) { - voltageV = -1.0f; - present = false; - if (!g_pmu) return; - - present = g_pmu->isBatteryConnect(); - voltageV = g_pmu->getBattVoltage() / 1000.0f; -} - -static void closeSessionLogs() { - if (g_sentFile) g_sentFile.close(); - if (g_recvFile) g_recvFile.close(); - g_sessionReady = false; -} - -static bool openSessionLogs() { - closeSessionLogs(); - - DateTime now{}; - int64_t nowEpoch = 0; - if (!getCurrentUtc(now, nowEpoch)) { - logf("Cannot open session logs: RTC unavailable"); - return false; - } - - formatUtcCompact(now, g_sessionStamp, sizeof(g_sessionStamp)); - snprintf(g_sentPath, sizeof(g_sentPath), "/%s_sent_%s.log", NODE_SHORT, g_sessionStamp); - snprintf(g_recvPath, sizeof(g_recvPath), "/%s_received_%s.log", NODE_SHORT, g_sessionStamp); - - g_sentFile = SD.open(g_sentPath, FILE_APPEND); - g_recvFile = SD.open(g_recvPath, FILE_APPEND); - if (!g_sentFile || !g_recvFile) { - logf("Failed to open session logs: %s | %s", g_sentPath, g_recvPath); - closeSessionLogs(); - return false; - } - - char human[32]; - formatUtcHuman(now, human, sizeof(human)); - g_sentFile.printf("# session_start_epoch=%lld utc=%s node=%s (%s) fw_build=%s\n", - (long long)nowEpoch, - human, - NODE_SHORT, - NODE_LABEL, - FW_BUILD_UTC); - g_recvFile.printf("# session_start_epoch=%lld utc=%s node=%s (%s) fw_build=%s\n", - (long long)nowEpoch, - human, - NODE_SHORT, - NODE_LABEL, - FW_BUILD_UTC); - g_sentFile.flush(); - g_recvFile.flush(); - - logf("Session logs ready: %s | %s", g_sentPath, g_recvPath); - g_sessionReady = true; - return true; -} - -static void writeSentLog(int64_t epoch, const DateTime& dt) { - if (!g_sessionReady || !g_sentFile) return; - - float battV = -1.0f; - bool battPresent = false; - readBattery(battV, battPresent); - - char human[32]; - formatUtcHuman(dt, human, sizeof(human)); - - g_sentFile.printf("epoch=%lld\tutc=%s\tunit=%s\tmsg=%s\ttx_count=%lu\tbatt_present=%u\tbatt_v=%.3f\n", - (long long)epoch, - human, - NODE_SHORT, - NODE_SHORT, - (unsigned long)g_txCount, - battPresent ? 1U : 0U, - battV); - g_sentFile.flush(); -} - -static void writeRecvLog(int64_t epoch, const DateTime& dt, const char* msg, float rssi, float snr) { - if (!g_sessionReady || !g_recvFile) return; - - float battV = -1.0f; - bool battPresent = false; - readBattery(battV, battPresent); - - char human[32]; - formatUtcHuman(dt, human, sizeof(human)); - - g_recvFile.printf("epoch=%lld\tutc=%s\tunit=%s\trx_msg=%s\trssi=%.1f\tsnr=%.1f\tbatt_present=%u\tbatt_v=%.3f\n", - (long long)epoch, - human, - NODE_SHORT, - msg ? msg : "", - rssi, - snr, - battPresent ? 1U : 0U, - battV); - g_recvFile.flush(); -} - -static void onLoRaDio1Rise() { - g_rxFlag = true; -} - -static bool initRadio() { - SPI.begin(LORA_SCK, LORA_MISO, LORA_MOSI, LORA_CS); - - int state = g_radio.begin(915.0, 125.0, 7, 5, 0x12, 14); - if (state != RADIOLIB_ERR_NONE) { - logf("radio.begin failed code=%d", state); - return false; - } - - g_radio.setDio1Action(onLoRaDio1Rise); - state = g_radio.startReceive(); - if (state != RADIOLIB_ERR_NONE) { - logf("radio.startReceive failed code=%d", state); - return false; - } - - logf("Radio ready for %s (%s), slot=%d sec=%d", NODE_LABEL, NODE_SHORT, NODE_SLOT_INDEX, NODE_SLOT_INDEX * 2); - return true; -} - -static void showRxOnOled(const DateTime& dt, const char* msg) { - char hhmmss[16]; - snprintf(hhmmss, sizeof(hhmmss), "%02u:%02u:%02u", (unsigned)dt.hour, (unsigned)dt.minute, (unsigned)dt.second); - - char line[32]; - snprintf(line, sizeof(line), "[%s, %s]", msg ? msg : "", NODE_SHORT); - oledShowLines(hhmmss, line); -} - -static void runTxScheduler() { - DateTime now{}; - int64_t epoch = 0; - if (!getCurrentUtc(now, epoch)) return; - - int slotSecond = NODE_SLOT_INDEX * (int)kSlotSeconds; - int secInFrame = now.second % 10; - if (secInFrame != slotSecond) return; - - int64_t epochSecond = epoch; - if (epochSecond == g_lastTxEpochSecond) return; - - g_lastTxEpochSecond = epochSecond; - - g_rxFlag = false; - g_radio.clearDio1Action(); - - int tx = g_radio.transmit(NODE_SHORT); - if (tx == RADIOLIB_ERR_NONE) { - g_txCount++; - writeSentLog(epoch, now); - logf("TX %s count=%lu", NODE_SHORT, (unsigned long)g_txCount); - } else { - logf("TX failed code=%d", tx); - } - - g_rxFlag = false; - g_radio.setDio1Action(onLoRaDio1Rise); - g_radio.startReceive(); -} - -static void runRxHandler() { - if (!g_rxFlag) return; - g_rxFlag = false; - - String rx; - int rc = g_radio.readData(rx); - if (rc != RADIOLIB_ERR_NONE) { - g_radio.startReceive(); - return; - } - - DateTime now{}; - int64_t epoch = 0; - if (getCurrentUtc(now, epoch)) { - writeRecvLog(epoch, now, rx.c_str(), g_radio.getRSSI(), g_radio.getSNR()); - showRxOnOled(now, rx.c_str()); - } - - g_radio.startReceive(); -} - -static void enterWaitSdState() { - if (g_phase == AppPhase::WAIT_SD) return; - g_phase = AppPhase::WAIT_SD; - closeSessionLogs(); - logf("State -> WAIT_SD"); -} - -static void enterWaitDisciplineState() { - if (g_phase == AppPhase::WAIT_DISCIPLINE) return; - g_phase = AppPhase::WAIT_DISCIPLINE; - closeSessionLogs(); - logf("State -> WAIT_DISCIPLINE"); -} - -static void enterRunState() { - if (g_phase == AppPhase::RUN) return; - if (!openSessionLogs()) return; - g_lastTxEpochSecond = -1; - g_lastHealthCheckMs = millis(); - g_phase = AppPhase::RUN; - logf("State -> RUN"); -} - -static void updateWaitSd() { - if (g_sd.isMounted()) { - g_lastWarnMs = 0; - g_gpsPathReady = false; - enterWaitDisciplineState(); - return; - } - - uint32_t now = millis(); - if ((uint32_t)(now - g_lastWarnMs) >= kSdMessagePeriodMs) { - g_lastWarnMs = now; - oledShowLines("Reinsert SD Card", NODE_SHORT, NODE_LABEL); - } -} - -static void updateWaitDiscipline() { - if (!g_sd.isMounted()) { - enterWaitSdState(); - return; - } - - if (!isDisciplineStale()) { - enterRunState(); - return; - } - - uint32_t now = millis(); - if ((uint32_t)(now - g_lastWarnMs) >= kNoGpsMessagePeriodMs) { - g_lastWarnMs = now; - char satsLine[24]; - snprintf(satsLine, sizeof(satsLine), "Satellites: %u", (unsigned)bestSatelliteCount()); - oledShowLines("Take me outside", "Need GPS time sync", satsLine); - } - - if ((uint32_t)(now - g_lastDisciplineTryMs) < kDisciplineRetryMs) return; - g_lastDisciplineTryMs = now; - - if (disciplineRtcToGps()) { - g_lastWarnMs = 0; - enterRunState(); - } -} - -static void updateRun() { - if (!g_sd.isMounted()) { - enterWaitSdState(); - return; - } - - uint32_t now = millis(); - if ((uint32_t)(now - g_lastHealthCheckMs) >= kHealthCheckPeriodMs) { - g_lastHealthCheckMs = now; - if (isDisciplineStale()) { - enterWaitDisciplineState(); - return; - } - } - - runTxScheduler(); - runRxHandler(); -} - -void setup() { - Serial.begin(115200); - delay(kSerialDelayMs); - - Serial.println("\r\n=================================================="); - Serial.println("Exercise 12: FiveTalk"); - Serial.println("=================================================="); - - if (!tbeam_supreme::initPmuForPeripherals(g_pmu, &Serial)) { - logf("WARN: PMU init failed"); - } - - Wire.begin(OLED_SDA, OLED_SCL); - g_oled.setI2CAddress(OLED_ADDR << 1); - g_oled.begin(); - oledShowLines("Exercise 12", "FiveTalk startup", NODE_SHORT, NODE_LABEL); - - SdWatcherConfig sdCfg{}; - if (!g_sd.begin(sdCfg, nullptr)) { - logf("WARN: SD watcher begin failed"); - } - -#ifdef GPS_1PPS_PIN - pinMode(GPS_1PPS_PIN, INPUT); - attachInterrupt(digitalPinToInterrupt(GPS_1PPS_PIN), onPpsEdge, RISING); -#endif -#ifdef GPS_WAKEUP_PIN - pinMode(GPS_WAKEUP_PIN, INPUT); -#endif - - g_gpsSerial.setRxBufferSize(1024); - g_gpsSerial.begin(GPS_BAUD, SERIAL_8N1, GPS_RX_PIN, GPS_TX_PIN); - - g_radioReady = initRadio(); - if (!g_radioReady) { - oledShowLines("LoRa init failed", "Check radio pins"); - } - - g_phase = g_sd.isMounted() ? AppPhase::WAIT_DISCIPLINE : AppPhase::WAIT_SD; -} - -void loop() { - pollGpsSerial(); - g_sd.update(); - - if (g_sd.consumeMountedEvent()) { - logf("SD mounted"); - g_gpsPathReady = false; - } - if (g_sd.consumeRemovedEvent()) { - logf("SD removed"); - g_gpsPathReady = false; - } - - if (!g_radioReady) { - delay(50); - return; - } - - switch (g_phase) { - case AppPhase::WAIT_SD: - updateWaitSd(); - break; - case AppPhase::WAIT_DISCIPLINE: - updateWaitDiscipline(); - break; - case AppPhase::RUN: - updateRun(); - break; - } - - delay(5); -} diff --git a/tools/99-ttyt-tbeam.rules b/tools/99-ttyt-tbeam.rules deleted file mode 100644 index 9e00a48..0000000 --- a/tools/99-ttyt-tbeam.rules +++ /dev/null @@ -1,66 +0,0 @@ -# 99-ttyt-tbeam.rules -# LilyGO T-Beam SUPREME (ESP32-S3 USB JTAG/serial debug unit) -# Stable symlinks for grep: /dev/ttytAMY, /dev/ttytBOB, ... -# -# Created 2//19/26 with ChatGTP after tallying units one-by-one -# -SUBSYSTEM=="tty", ATTRS{idVendor}=="303a", ATTRS{idProduct}=="1001", ATTRS{serial}=="48:CA:43:5B:BF:68", MODE:="0660", GROUP:="dialout", SYMLINK+="ttytAMY" -SUBSYSTEM=="tty", ATTRS{idVendor}=="303a", ATTRS{idProduct}=="1001", ATTRS{serial}=="48:CA:43:5A:93:DC", MODE:="0660", GROUP:="dialout", SYMLINK+="ttytBOB" -SUBSYSTEM=="tty", ATTRS{idVendor}=="303a", ATTRS{idProduct}=="1001", ATTRS{serial}=="48:CA:43:5A:91:44", MODE:="0660", GROUP:="dialout", SYMLINK+="ttytCY" -SUBSYSTEM=="tty", ATTRS{idVendor}=="303a", ATTRS{idProduct}=="1001", ATTRS{serial}=="48:CA:43:5A:93:A0", MODE:="0660", GROUP:="dialout", SYMLINK+="ttytDAN" -SUBSYSTEM=="tty", ATTRS{idVendor}=="303a", ATTRS{idProduct}=="1001", ATTRS{serial}=="48:CA:43:5A:90:D0", MODE:="0660", GROUP:="dialout", SYMLINK+="ttytED" - -# -# to load and test: -# sudo udevadm control --reload-rules -# sudo udevadm trigger --subsystem-match=tty -# ls -l /dev/ttyt* -# - -# Derived from: -# -# Bob: -# (rnsenv) jlpoole@jp /usr/local/src/microreticulum/microReticulumTbeam/exercises/12_FiveTalk $ date; pio device list |grep -A3 ttyA -# Thu Feb 19 08:26:36 PST 2026 -# /dev/ttyACM0 -# ------------ -# Hardware ID: USB VID:PID=303A:1001 SER=48:CA:43:5A:93:DC LOCATION=2-2.2.4.4.3:1.0 -# Description: USB JTAG/serial debug unit -# (rnsenv) jlpoole@jp /usr/local/src/microreticulum/microReticulumTbeam/exercises/12_FiveTalk $ -# -# Amy: -# (rnsenv) jlpoole@jp /usr/local/src/microreticulum/microReticulumTbeam/exercises/12_FiveTalk $ date; pio device list |grep -A3 ttyA -# Thu Feb 19 08:27:29 PST 2026 -# /dev/ttyACM0 -# ------------ -# Hardware ID: USB VID:PID=303A:1001 SER=48:CA:43:5B:BF:68 LOCATION=2-2.2.4.4.4:1.0 -# Description: USB JTAG/serial debug unit -# (rnsenv) jlpoole@jp /usr/local/src/microreticulum/microReticulumTbeam/exercises/12_FiveTalk $ # above is Amy -# -# Cy: -# (rnsenv) jlpoole@jp /usr/local/src/microreticulum/microReticulumTbeam/exercises/12_FiveTalk $ date; pio device list |grep -A3 ttyA # Cy -# Thu Feb 19 08:28:57 PST 2026 -# /dev/ttyACM0 -# ------------ -# Hardware ID: USB VID:PID=303A:1001 SER=48:CA:43:5A:91:44 LOCATION=2-2.2.4.4.2:1.0 -# Description: USB JTAG/serial debug unit -# (rnsenv) jlpoole@jp /usr/local/src/microreticulum/microReticulumTbeam/exercises/12_FiveTalk $ -# -# Dan: -# (rnsenv) jlpoole@jp /usr/local/src/microreticulum/microReticulumTbeam/exercises/12_FiveTalk $ date; pio device list |grep -A3 ttyA # Dan -# Thu Feb 19 08:30:04 PST 2026 -# /dev/ttyACM0 -# ------------ -# Hardware ID: USB VID:PID=303A:1001 SER=48:CA:43:5A:93:A0 LOCATION=2-2.2.4.3:1.0 -# Description: USB JTAG/serial debug unit -# (rnsenv) jlpoole@jp /usr/local/src/microreticulum/microReticulumTbeam/exercises/12_FiveTalk $ -# -# Ed: -# (rnsenv) jlpoole@jp /usr/local/src/microreticulum/microReticulumTbeam/exercises/12_FiveTalk $ date; pio device list |grep -A3 ttyA # Ed -# Thu Feb 19 08:30:59 PST 2026 -# /dev/ttyACM0 -# ------------ -# Hardware ID: USB VID:PID=303A:1001 SER=48:CA:43:5A:90:D0 LOCATION=2-2.2.4.4.1:1.0 -# Description: USB JTAG/serial debug unit -# (rnsenv) jlpoole@jp /usr/local/src/microreticulum/microReticulumTbeam/exercises/12_FiveTalk $ -#