diff --git a/docs/pio.md b/docs/pio.md new file mode 100644 index 0000000..bdcb30e --- /dev/null +++ b/docs/pio.md @@ -0,0 +1,134 @@ +# pio +pio stands for "PlatformIO". + +In your context (ESP32 / T-Beam / PlatformIO workflows), **“pio” stands for:** + +> **PlatformIO** + +More precisely, `pio` is the **command-line interface (CLI)** for the PlatformIO environment. + +--- + +## What PlatformIO is (practically speaking) + +PlatformIO is an embedded development framework that wraps: + +* toolchains (GCC for ESP32, ARM, etc.) +* build system +* library manager +* uploader (esptool, etc.) +* serial monitor + +So instead of juggling: + +```bash +xtensa-esp32-elf-gcc +esptool.py +arduino-cli +``` + +…you use: + +```bash +pio run +pio run -t upload +pio device monitor +``` + +--- + +## What `pio` actually does in your workflow + +Given your T-Beam usage (as shown in the LilyGO docs, which explicitly include PlatformIO as a supported environment ), `pio` is orchestrating: + +### Build + +```bash +pio run +``` + +* compiles your firmware using the environment in `platformio.ini` + +### Upload + +```bash +pio run -t upload +``` + +* invokes the correct uploader (ESP32 → `esptool`) +* uses the selected `/dev/tty*` device + +### Monitor + +```bash +pio device monitor +``` + +* serial console at configured baud + +--- + +## Why it exists (the “systems view”) + +PlatformIO abstracts away: + +* board definitions +* pin mappings +* compiler flags +* library dependencies + +In your `platformio.ini`, for example: + +```ini +[env:node_a] +platform = espressif32 +board = esp32-s3-devkitc-1 +``` + +That single block drives: + +* toolchain selection +* linker scripts +* upload protocol +* memory layout + +--- + +## Important nuance (since you’re working low-level) + +`pio` is **not**: + +* a compiler +* a flasher +* an IDE + +It is a **build orchestration layer** (think “make + package manager + uploader + board database”). + +--- + +## Quick mental model + +If we translate to Unix analogies: + +| Concept | Analogy | +| ------------------- | -------------------------------------------- | +| PlatformIO | `portage + make + gcc + flash tool` combined | +| `pio run` | `make` | +| `pio run -t upload` | `make install` (but to hardware) | +| `platformio.ini` | hybrid of `Makefile` + config | + +--- + +## Bottom line + +When you type: + +```bash +pio run -t upload +``` + +You are saying: + +> “Use PlatformIO to build this firmware for the specified environment and flash it to the target device.” + + diff --git a/exercises/09_GPS_Time/platformio.ini b/exercises/09_GPS_Time/platformio.ini index fff0d30..b4d48d9 100644 --- a/exercises/09_GPS_Time/platformio.ini +++ b/exercises/09_GPS_Time/platformio.ini @@ -29,6 +29,7 @@ build_flags = -D GPS_1PPS_PIN=6 -D ARDUINO_USB_MODE=1 -D ARDUINO_USB_CDC_ON_BOOT=1 + -D GPS_L76K [env:node_a] build_flags = diff --git a/exercises/09_GPS_Time/src/main.cpp b/exercises/09_GPS_Time/src/main.cpp index 36b0624..22d81d7 100644 --- a/exercises/09_GPS_Time/src/main.cpp +++ b/exercises/09_GPS_Time/src/main.cpp @@ -53,7 +53,7 @@ static size_t g_gpsLineLen = 0; enum class GpsModuleKind : uint8_t { UNKNOWN = 0, L76K, - QUECTEL_TODO + UBLOX }; struct RtcDateTime { @@ -169,8 +169,8 @@ static String gpsModuleToString(GpsModuleKind kind) { if (kind == GpsModuleKind::L76K) { return "L76K"; } - if (kind == GpsModuleKind::QUECTEL_TODO) { - return "Quectel/TODO"; + if (kind == GpsModuleKind::UBLOX) { + return "UBLOX"; } return "Unknown"; } @@ -198,7 +198,7 @@ static void detectModuleFromText(const char* text) { if (t.indexOf("QUECTEL") >= 0 || t.indexOf("2021-10-20") >= 0 || t.indexOf("2021/10/20") >= 0) { if (g_gps.module != GpsModuleKind::L76K) { - g_gps.module = GpsModuleKind::QUECTEL_TODO; + g_gps.module = GpsModuleKind::UBLOX; } } } @@ -386,8 +386,8 @@ static uint8_t bestSatelliteCount() { return (g_gps.satsUsed > g_gps.satsInView) ? g_gps.satsUsed : g_gps.satsInView; } -static bool isUnsupportedQuectelMode() { - return g_gps.module == GpsModuleKind::QUECTEL_TODO; +static bool isUnsupportedGpsMode() { + return g_gps.module == GpsModuleKind::UBLOX; } static void reportStatusToSerial() { @@ -402,7 +402,7 @@ static void reportStatusToSerial() { } static void maybeAnnounceGpsTransitions() { - if (isUnsupportedQuectelMode()) { + if (isUnsupportedGpsMode()) { return; } @@ -432,7 +432,7 @@ static void maybeAnnounceGpsTransitions() { (unsigned)g_gps.utcMinute, (unsigned)g_gps.utcSecond); snprintf(line3, sizeof(line3), "Satellites: %u", (unsigned)sats); - oledShowLines("GPS UTC acquired", line2, line3, "Source: L76K"); + oledShowLines("GPS UTC acquired", line2, line3, ("Source: " + gpsModuleToString(g_gps.module)).c_str()); logf("Transition: GPS UTC acquired: %s", line2); g_timeAcquiredAnnounced = true; } @@ -442,9 +442,9 @@ static void maybeAnnounceGpsTransitions() { } static void drawMinuteStatus() { - if (isUnsupportedQuectelMode()) { - oledShowLines("GPS module mismatch", "Quectel detected", "L76K required", "TODO: implement", "Quectel support"); - logf("GPS module mismatch: Quectel detected but this exercise currently supports only L76K (TODO)"); + if (isUnsupportedGpsMode()) { + oledShowLines("GPS module mismatch", "UBLOX detected", "L76K required", "TODO: implement", "UBLOX support"); + logf("GPS module mismatch: UBLOX detected but this exercise currently supports only L76K (TODO)"); return; } @@ -462,7 +462,7 @@ static void drawMinuteStatus() { (unsigned)g_gps.utcMinute, (unsigned)g_gps.utcSecond); snprintf(line3, sizeof(line3), "Satellites: %u", (unsigned)sats); - oledShowLines("GPS time (UTC)", line2, line3, "Source: L76K"); + oledShowLines("GPS time (UTC)", line2, line3, ("Source: " + gpsModuleToString(g_gps.module)).c_str()); logf("GPS time (UTC): %s satellites=%u", line2, (unsigned)sats); return; } diff --git a/exercises/10_Simple_GPS/platformio.ini b/exercises/10_Simple_GPS/platformio.ini index a183c3b..02b2f71 100644 --- a/exercises/10_Simple_GPS/platformio.ini +++ b/exercises/10_Simple_GPS/platformio.ini @@ -28,6 +28,7 @@ build_flags = -D GPS_1PPS_PIN=6 -D ARDUINO_USB_MODE=1 -D ARDUINO_USB_CDC_ON_BOOT=1 + -D GPS_L76K [env:node_a] build_flags = diff --git a/exercises/11_Set_RTC2GPS/platformio.ini b/exercises/11_Set_RTC2GPS/platformio.ini index a91b0bc..8c7c7f3 100644 --- a/exercises/11_Set_RTC2GPS/platformio.ini +++ b/exercises/11_Set_RTC2GPS/platformio.ini @@ -28,6 +28,7 @@ build_flags = -D GPS_TX_PIN=8 -D GPS_WAKEUP_PIN=7 -D GPS_1PPS_PIN=6 + -D GPS_L76K -D ARDUINO_USB_MODE=1 -D ARDUINO_USB_CDC_ON_BOOT=1 diff --git a/exercises/12_FiveTalk/READEME.md b/exercises/12_FiveTalk/READEME.md new file mode 100644 index 0000000..36960c5 --- /dev/null +++ b/exercises/12_FiveTalk/READEME.md @@ -0,0 +1,11 @@ + + +main.cpp needs to be modified to reflect the number of units. It is a zero-based array, so for 7 possible unite, the value of 6 is used in both lines below: + + #if (NODE_SLOT_INDEX < 0) || (NODE_SLOT_INDEX > 6) + #error "NODE_SLOT_INDEX must be 0..6" + #endif + + + +INSERT SCREENSHOT HERE. \ No newline at end of file diff --git a/exercises/12_FiveTalk/platformio.ini b/exercises/12_FiveTalk/platformio.ini index 8bb9199..0eff093 100644 --- a/exercises/12_FiveTalk/platformio.ini +++ b/exercises/12_FiveTalk/platformio.ini @@ -27,6 +27,7 @@ build_flags = -D GPS_TX_PIN=8 -D GPS_WAKEUP_PIN=7 -D GPS_1PPS_PIN=6 + -D GPS_L76K -D LORA_CS=10 -D LORA_MOSI=11 -D LORA_SCK=12 @@ -77,3 +78,20 @@ build_flags = -D NODE_LABEL=\"Ed\" -D NODE_SHORT=\"E\" -D NODE_SLOT_INDEX=4 + +[env:flo] +extends = env +build_flags = + ${env.build_flags} + -D NODE_LABEL=\"Flo\" + -D NODE_SHORT=\"F\" + -D NODE_SLOT_INDEX=5 + +[env:guy] +extends = env +build_flags = + ${env.build_flags} + -D NODE_LABEL=\"Guy\" + -D NODE_SHORT=\"G\" + -D NODE_SLOT_INDEX=6 + diff --git a/exercises/12_FiveTalk/src/main.cpp b/exercises/12_FiveTalk/src/main.cpp index 4a77047..8c612cd 100644 --- a/exercises/12_FiveTalk/src/main.cpp +++ b/exercises/12_FiveTalk/src/main.cpp @@ -63,8 +63,8 @@ #define FW_BUILD_UTC "unknown" #endif -#if (NODE_SLOT_INDEX < 0) || (NODE_SLOT_INDEX > 4) -#error "NODE_SLOT_INDEX must be 0..4" +#if (NODE_SLOT_INDEX < 0) || (NODE_SLOT_INDEX > 6) +#error "NODE_SLOT_INDEX must be 0..6" #endif static const uint32_t kSerialDelayMs = 1000; diff --git a/tools/constantTFCard/raw_probe/.gitignore b/tools/constantTFCard/raw_probe/.gitignore new file mode 100644 index 0000000..cfce1ad --- /dev/null +++ b/tools/constantTFCard/raw_probe/.gitignore @@ -0,0 +1,2 @@ +*.log + diff --git a/tools/constantTFCard/raw_probe/platformio.ini b/tools/constantTFCard/raw_probe/platformio.ini new file mode 100644 index 0000000..889b197 --- /dev/null +++ b/tools/constantTFCard/raw_probe/platformio.ini @@ -0,0 +1,55 @@ +; 20260401 Codex +; constantTFCard raw SPI/SD probe + +[platformio] +default_envs = amy + +[env] +platform = espressif32 +framework = arduino +board = esp32-s3-devkitc-1 +monitor_speed = 115200 +lib_deps = + 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 ARDUINO_USB_MODE=1 + -D ARDUINO_USB_CDC_ON_BOOT=1 + +[env:amy] +extends = env +build_flags = + ${env.build_flags} + -D NODE_LABEL=\"AMY\" + +[env:bob] +extends = env +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\" diff --git a/tools/constantTFCard/raw_probe/src/main.cpp b/tools/constantTFCard/raw_probe/src/main.cpp new file mode 100644 index 0000000..68196c4 --- /dev/null +++ b/tools/constantTFCard/raw_probe/src/main.cpp @@ -0,0 +1,463 @@ +// 20260401 Codex + +#include +#include +#include +#include +#include +#include + +#include "tbeam_supreme_adapter.h" + +#ifndef NODE_LABEL +#define NODE_LABEL "NODE" +#endif + +#ifndef OLED_SDA +#define OLED_SDA 17 +#endif + +#ifndef OLED_SCL +#define OLED_SCL 18 +#endif + +static const uint32_t kSerialDelayMs = 1500; +static const uint32_t kPollIntervalMs = 200; +static const uint32_t kSpiHz = 400000; +static const uint32_t kReadyHeartbeatMs = 2000; + +enum class RawState : uint8_t { + PMU_FAIL = 0, + RAIL_OFF, + BUS_FLOAT, + BUS_LOW, + CMD0_TIMEOUT, + CMD0_NOT_IDLE, + CMD8_TIMEOUT, + CMD8_BAD_R1, + ACMD41_TIMEOUT, + CMD58_TIMEOUT, + READY +}; + +struct PinSnapshot { + int cs = -1; + int sck = -1; + int miso = -1; + int mosi = -1; +}; + +struct ProbeSummary { + uint8_t ffCount = 0; + uint8_t zeroCount = 0; + uint8_t otherCount = 0; + uint8_t bytes[8] = {0}; +}; + +struct RawSnapshot { + RawState state = RawState::PMU_FAIL; + bool pmuOk = false; + bool railOn = false; + PinSnapshot pins{}; + ProbeSummary idle{}; + uint8_t cmd0 = 0xFF; + uint8_t cmd8r1 = 0xFF; + uint8_t cmd8data[4] = {0xFF, 0xFF, 0xFF, 0xFF}; + uint8_t acmd41 = 0xFF; + uint8_t cmd58r1 = 0xFF; + uint8_t ocr[4] = {0xFF, 0xFF, 0xFF, 0xFF}; +}; + +static XPowersLibInterface* g_pmu = nullptr; +static U8G2_SH1106_128X64_NONAME_F_HW_I2C g_oled(U8G2_R0, U8X8_PIN_NONE); +static SPIClass g_spi(HSPI); +static uint32_t g_sampleCount = 0; +static uint32_t g_markCount = 0; +static char g_inputLine[32] = {0}; +static uint8_t g_inputLen = 0; + +static void forceSpiDeselected() { + pinMode(tbeam_supreme::sdCs(), OUTPUT); + digitalWrite(tbeam_supreme::sdCs(), HIGH); + pinMode(tbeam_supreme::imuCs(), OUTPUT); + digitalWrite(tbeam_supreme::imuCs(), HIGH); +} + +static PinSnapshot readPins() { + PinSnapshot s; + s.cs = gpio_get_level((gpio_num_t)tbeam_supreme::sdCs()); + s.sck = gpio_get_level((gpio_num_t)tbeam_supreme::sdSck()); + s.miso = gpio_get_level((gpio_num_t)tbeam_supreme::sdMiso()); + s.mosi = gpio_get_level((gpio_num_t)tbeam_supreme::sdMosi()); + return s; +} + +static void beginBus() { + SD.end(); + g_spi.end(); + delay(2); + forceSpiDeselected(); + g_spi.begin( + tbeam_supreme::sdSck(), + tbeam_supreme::sdMiso(), + tbeam_supreme::sdMosi(), + tbeam_supreme::sdCs() + ); + digitalWrite(tbeam_supreme::sdCs(), HIGH); +} + +static ProbeSummary idleProbe() { + ProbeSummary out; + beginBus(); + delay(1); + for (int i = 0; i < 8; ++i) { + uint8_t b = g_spi.transfer(0xFF); + out.bytes[i] = b; + if (b == 0xFF) out.ffCount++; + else if (b == 0x00) out.zeroCount++; + else out.otherCount++; + } + return out; +} + +static uint8_t waitR1(uint16_t tries = 16) { + for (uint16_t i = 0; i < tries; ++i) { + uint8_t r = g_spi.transfer(0xFF); + if ((r & 0x80) == 0) { + return r; + } + } + return 0xFF; +} + +static uint8_t sendCommand(uint8_t cmd, uint32_t arg, uint8_t crc) { + g_spi.transfer(0xFF); + digitalWrite(tbeam_supreme::sdCs(), LOW); + g_spi.transfer(0x40 | cmd); + g_spi.transfer((arg >> 24) & 0xFF); + g_spi.transfer((arg >> 16) & 0xFF); + g_spi.transfer((arg >> 8) & 0xFF); + g_spi.transfer(arg & 0xFF); + g_spi.transfer(crc); + uint8_t r1 = waitR1(); + return r1; +} + +static void endCommand() { + digitalWrite(tbeam_supreme::sdCs(), HIGH); + g_spi.transfer(0xFF); +} + +static const char* stateToString(RawState state) { + switch (state) { + case RawState::PMU_FAIL: return "PMU_FAIL"; + case RawState::RAIL_OFF: return "RAIL_OFF"; + case RawState::BUS_FLOAT: return "BUS_FLOAT"; + case RawState::BUS_LOW: return "BUS_LOW"; + case RawState::CMD0_TIMEOUT: return "CMD0_TO"; + case RawState::CMD0_NOT_IDLE: return "CMD0_BAD"; + case RawState::CMD8_TIMEOUT: return "CMD8_TO"; + case RawState::CMD8_BAD_R1: return "CMD8_BAD"; + case RawState::ACMD41_TIMEOUT: return "ACMD41_TO"; + case RawState::CMD58_TIMEOUT: return "CMD58_TO"; + case RawState::READY: return "READY"; + default: return "UNKNOWN"; + } +} + +static RawSnapshot captureSnapshot() { + RawSnapshot snap; + snap.pins = readPins(); + snap.pmuOk = (g_pmu != nullptr); + if (!snap.pmuOk) { + snap.state = RawState::PMU_FAIL; + return snap; + } + + snap.railOn = g_pmu->isPowerChannelEnable(XPOWERS_BLDO1); + if (!snap.railOn) { + snap.state = RawState::RAIL_OFF; + return snap; + } + + snap.idle = idleProbe(); + if (snap.idle.ffCount == 8) { + snap.state = RawState::BUS_FLOAT; + } else if (snap.idle.zeroCount == 8) { + snap.state = RawState::BUS_LOW; + } + + beginBus(); + delay(1); + for (int i = 0; i < 10; ++i) { + g_spi.transfer(0xFF); + } + + snap.cmd0 = sendCommand(0, 0, 0x95); + endCommand(); + if (snap.cmd0 == 0xFF) { + snap.state = RawState::CMD0_TIMEOUT; + return snap; + } + if (snap.cmd0 != 0x01) { + snap.state = RawState::CMD0_NOT_IDLE; + return snap; + } + + snap.cmd8r1 = sendCommand(8, 0x000001AAUL, 0x87); + if (snap.cmd8r1 == 0xFF) { + endCommand(); + snap.state = RawState::CMD8_TIMEOUT; + return snap; + } + for (int i = 0; i < 4; ++i) { + snap.cmd8data[i] = g_spi.transfer(0xFF); + } + endCommand(); + if (!(snap.cmd8r1 == 0x01 || snap.cmd8r1 == 0x05)) { + snap.state = RawState::CMD8_BAD_R1; + return snap; + } + + uint8_t ready = 0xFF; + for (int attempt = 0; attempt < 12; ++attempt) { + uint8_t r1 = sendCommand(55, 0, 0x65); + endCommand(); + if (r1 == 0xFF) { + continue; + } + ready = sendCommand(41, 0x40000000UL, 0x77); + endCommand(); + if (ready == 0x00) { + break; + } + delay(10); + } + snap.acmd41 = ready; + if (snap.acmd41 != 0x00) { + snap.state = RawState::ACMD41_TIMEOUT; + return snap; + } + + snap.cmd58r1 = sendCommand(58, 0, 0xFD); + if (snap.cmd58r1 == 0xFF) { + endCommand(); + snap.state = RawState::CMD58_TIMEOUT; + return snap; + } + for (int i = 0; i < 4; ++i) { + snap.ocr[i] = g_spi.transfer(0xFF); + } + endCommand(); + + snap.state = RawState::READY; + return snap; +} + +static void printSnapshot(const RawSnapshot& snap) { + Serial.printf( + "sample=%lu state=%s rail=%s pins=%d/%d/%d/%d " + "idle(ff=%u z=%u o=%u %02X %02X %02X %02X) " + "cmd0=%02X cmd8=%02X [%02X %02X %02X %02X] " + "acmd41=%02X cmd58=%02X [%02X %02X %02X %02X]\r\n", + (unsigned long)g_sampleCount, + stateToString(snap.state), + snap.railOn ? "ON" : "OFF", + snap.pins.cs, + snap.pins.sck, + snap.pins.miso, + snap.pins.mosi, + (unsigned)snap.idle.ffCount, + (unsigned)snap.idle.zeroCount, + (unsigned)snap.idle.otherCount, + snap.idle.bytes[0], + snap.idle.bytes[1], + snap.idle.bytes[2], + snap.idle.bytes[3], + snap.cmd0, + snap.cmd8r1, + snap.cmd8data[0], + snap.cmd8data[1], + snap.cmd8data[2], + snap.cmd8data[3], + snap.acmd41, + snap.cmd58r1, + snap.ocr[0], + snap.ocr[1], + snap.ocr[2], + snap.ocr[3] + ); +} + +static void printReadyHeartbeat() { + Serial.printf("[%10lu] READY heartbeat\r\n", (unsigned long)millis()); +} + +static bool sameSnapshot(const RawSnapshot& a, const RawSnapshot& b) { + if (a.state != b.state) return false; + if (a.railOn != b.railOn) return false; + if (a.pins.cs != b.pins.cs || a.pins.sck != b.pins.sck || + a.pins.miso != b.pins.miso || a.pins.mosi != b.pins.mosi) return false; + if (a.idle.ffCount != b.idle.ffCount || + a.idle.zeroCount != b.idle.zeroCount || + a.idle.otherCount != b.idle.otherCount) return false; + if (a.cmd0 != b.cmd0 || a.cmd8r1 != b.cmd8r1 || + a.acmd41 != b.acmd41 || a.cmd58r1 != b.cmd58r1) return false; + + for (int i = 0; i < 4; ++i) { + if (a.cmd8data[i] != b.cmd8data[i]) return false; + if (a.ocr[i] != b.ocr[i]) return false; + } + + return true; +} + +static void showSnapshot(const RawSnapshot& snap) { + char l1[24]; + char l2[24]; + char l3[24]; + char l4[24]; + char l5[24]; + + snprintf(l1, sizeof(l1), "%s RAW SD", NODE_LABEL); + snprintf(l2, sizeof(l2), "STATE %s", stateToString(snap.state)); + snprintf(l3, sizeof(l3), "CMD0 %02X C8 %02X A41 %02X", snap.cmd0, snap.cmd8r1, snap.acmd41); + snprintf(l4, sizeof(l4), "OCR %02X%02X%02X%02X", + snap.ocr[0], snap.ocr[1], snap.ocr[2], snap.ocr[3]); + snprintf(l5, sizeof(l5), "IDL %u/%u/%u #%lu", + (unsigned)snap.idle.ffCount, + (unsigned)snap.idle.zeroCount, + (unsigned)snap.idle.otherCount, + (unsigned long)g_sampleCount); + + g_oled.clearBuffer(); + g_oled.setFont(u8g2_font_5x8_tf); + g_oled.drawUTF8(0, 12, l1); + g_oled.drawUTF8(0, 24, l2); + g_oled.drawUTF8(0, 36, l3); + g_oled.drawUTF8(0, 48, l4); + g_oled.drawUTF8(0, 60, l5); + g_oled.sendBuffer(); +} + +static void handleSerialInput() { + auto handleCommand = []() { + if (g_inputLen == 0) { + Serial.println(); + return; + } + + g_inputLine[g_inputLen] = '\0'; + if (strcmp(g_inputLine, "m") == 0 || strcmp(g_inputLine, "M") == 0) { + g_markCount++; + Serial.printf("----- MARK %lu -----\r\n", (unsigned long)g_markCount); + } else if (strcmp(g_inputLine, "ls") == 0 || strcmp(g_inputLine, "LS") == 0) { + Serial.println("----- LS / -----"); + beginBus(); + for (int i = 0; i < 10; ++i) { + g_spi.transfer(0xFF); + } + + if (!SD.begin(tbeam_supreme::sdCs(), g_spi, kSpiHz)) { + Serial.println("ls: SD.begin failed"); + g_inputLen = 0; + return; + } + + File root = SD.open("/", FILE_READ); + if (!root) { + Serial.println("ls: open / failed"); + SD.end(); + g_inputLen = 0; + return; + } + + File entry = root.openNextFile(); + if (!entry) { + Serial.println("ls: root empty or unreadable"); + } + + while (entry) { + Serial.printf("%s%s %lu\r\n", + entry.name(), + entry.isDirectory() ? "/" : "", + (unsigned long)entry.size()); + entry.close(); + entry = root.openNextFile(); + } + + root.close(); + SD.end(); + Serial.println("----- END LS -----"); + } else { + Serial.printf("unknown command: %s\r\n", g_inputLine); + } + + g_inputLen = 0; + }; + + while (Serial.available() > 0) { + int ch = Serial.read(); + if (ch == '\r' || ch == '\n') { + handleCommand(); + } else if (ch == 0x08 || ch == 0x7F) { + if (g_inputLen > 0) { + g_inputLen--; + } + } else if (g_inputLen + 1 < sizeof(g_inputLine)) { + g_inputLine[g_inputLen++] = (char)ch; + } + } +} + +void setup() { + Serial.begin(115200); + delay(kSerialDelayMs); + + Wire.begin(OLED_SDA, OLED_SCL); + g_oled.begin(); + g_oled.clearDisplay(); + + tbeam_supreme::initPmuForPeripherals(g_pmu, &Serial); + forceSpiDeselected(); + + Serial.println(); + Serial.println("constantTFCard raw probe"); + Serial.printf("Node: %s\r\n", NODE_LABEL); + Serial.printf("Poll interval: %lu ms\r\n", (unsigned long)kPollIntervalMs); + Serial.println("States: PMU_FAIL RAIL_OFF BUS_FLOAT BUS_LOW CMD0_TO CMD0_BAD CMD8_TO CMD8_BAD ACMD41_TO CMD58_TO READY"); + Serial.println("Input: Enter=blank line, m=mark, ls=list root"); +} + +void loop() { + static uint32_t lastPollMs = 0; + static bool haveLastPrinted = false; + static RawSnapshot lastPrinted{}; + static uint32_t lastReadyHeartbeatMs = 0; + + handleSerialInput(); + + uint32_t now = millis(); + if (now - lastPollMs < kPollIntervalMs) { + delay(5); + return; + } + + lastPollMs = now; + g_sampleCount++; + + RawSnapshot snap = captureSnapshot(); + bool changed = (!haveLastPrinted || !sameSnapshot(snap, lastPrinted)); + if (changed) { + printSnapshot(snap); + lastPrinted = snap; + haveLastPrinted = true; + if (snap.state == RawState::READY) { + lastReadyHeartbeatMs = now; + } + } else if (snap.state == RawState::READY && now - lastReadyHeartbeatMs >= kReadyHeartbeatMs) { + printReadyHeartbeat(); + lastReadyHeartbeatMs = now; + } + showSnapshot(snap); +}