Compare commits

...

6 commits

35 changed files with 4136 additions and 80 deletions

1
.gitignore vendored
View file

@ -17,3 +17,4 @@
/hold/
.platformio_local/
.codex

0
docs/gps.md Normal file
View file

134
docs/pio.md Normal file
View file

@ -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 youre 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.”

View file

@ -1,4 +1,4 @@
## Exercise 09: GPS Time (L76K)
## Exercise 09: GPS Time (L76K + UBLOX)
This exercise boots the T-Beam Supreme and verifies GPS behavior at startup.
@ -13,11 +13,10 @@ Implemented behavior:
1. Initializes PMU, OLED, and SD startup watcher (same startup SD path used in Exercise 08).
2. Probes GPS at startup for NMEA traffic, module identity, satellite count, and UTC time availability.
- Uses explicit GPS UART pins and an active startup probe (multi-baud + common GPS query commands), aligned with the approach validated in Exercise 10.
3. If L76K is detected, normal GPS-time flow continues.
4. If L76K is not detected and Quectel-style module text is detected, OLED shows a hard TODO error:
- Quectel detected
- L76K required
- Quectel support is TODO
3. Supports both module profiles via `platformio.ini` build flags:
- `node_a` / `node_b`: `GPS_L76K`
- `node_c`: `GPS_UBLOX`
4. If detected module data conflicts with the selected node profile, OLED shows a `GPS module mismatch` error.
5. Every minute:
- If GPS UTC is valid: shows GPS UTC time and satellites on OLED.
- If satellites are seen but UTC is not valid yet: shows that condition and RTC time.
@ -31,8 +30,12 @@ Implemented behavior:
Notes:
- GPS time displayed is UTC from NMEA RMC with valid status.
- Satellite count uses best available from GGA/GSV.
- Satellite count uses best available from GGA/GSA/GSV.
- RTC fallback reads PCF8563 via Wire1.
- For UBLOX hardware use `-e node_c`.
- The UBLOX MAX-M10S path is given a longer startup window than L76K because cold starts are slower, especially if backup power/orbit data are unavailable.
- On T-Beam Supreme, `GPS_WAKEUP_PIN=7` is relevant for the L76K variant; the UBLOX MAX-M10S does not use that wake pin in the same way.
- For fastest UBLOX reacquisition, test with the 18650 attached so the GNSS backup domain can preserve assistance state across resets/power cycles.
## Build

View file

@ -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 =
@ -39,3 +40,9 @@ build_flags =
build_flags =
${env.build_flags}
-D NODE_LABEL=\"B\"
[env:node_c]
build_flags =
${env.build_flags}
-D NODE_LABEL=\"C\"
-D GPS_UBLOX

File diff suppressed because it is too large Load diff

View file

@ -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 =
@ -38,3 +39,9 @@ build_flags =
build_flags =
${env.build_flags}
-D NODE_LABEL=\"B\"
[env:node_c]
build_flags =
${env.build_flags}
-D NODE_LABEL=\"C\"
-D GPS_UBLOX

View file

@ -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

View file

@ -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.

View file

@ -27,6 +27,8 @@ build_flags =
-D GPS_TX_PIN=8
-D GPS_WAKEUP_PIN=7
-D GPS_1PPS_PIN=6
-D GPS_L76K
-D NODE_SLOT_COUNT=7
-D LORA_CS=10
-D LORA_MOSI=11
-D LORA_SCK=12
@ -77,3 +79,22 @@ 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
-D GPS_UBLOX

View file

@ -27,6 +27,10 @@
#define NODE_SHORT "?"
#endif
#ifndef NODE_SLOT_COUNT
#define NODE_SLOT_COUNT 7
#endif
#ifndef NODE_SLOT_INDEX
#define NODE_SLOT_INDEX 0
#endif
@ -63,8 +67,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 >= NODE_SLOT_COUNT)
#error "NODE_SLOT_INDEX must be 0..NODE_SLOT_COUNT-1"
#endif
static const uint32_t kSerialDelayMs = 1000;
@ -156,6 +160,20 @@ static uint8_t bestSatelliteCount()
return (g_gps.satsUsed > g_gps.satsInView) ? g_gps.satsUsed : g_gps.satsInView;
}
static uint32_t computeFrameSeconds(uint32_t requiredSeconds)
{
uint32_t frame = ((requiredSeconds + 4U) / 5U) * 5U; // round up to 5s
while (frame <= 60U && (60U % frame) != 0U)
{
frame += 5U;
}
if (frame == 0U || frame > 60U)
{
frame = 60U; // fallback
}
return frame;
}
static void logf(const char *fmt, ...)
{
char msg[256];
@ -1030,7 +1048,7 @@ static bool initRadio()
return false;
}
logf("Radio ready for %s (%s), slot=%d sec=%d", NODE_LABEL, NODE_SHORT, NODE_SLOT_INDEX, NODE_SLOT_INDEX * 2);
logf("Radio ready for %s (%s), slot=%d/%d (2s each)", NODE_LABEL, NODE_SHORT, NODE_SLOT_INDEX, NODE_SLOT_COUNT);
return true;
}
@ -1051,8 +1069,15 @@ static void runTxScheduler()
if (!getCurrentUtc(now, epoch))
return;
int slotSecond = NODE_SLOT_INDEX * (int)kSlotSeconds;
int secInFrame = now.second % 10;
uint32_t requiredTxSeconds = (uint32_t)NODE_SLOT_COUNT * kSlotSeconds;
uint32_t frameSeconds = computeFrameSeconds(requiredTxSeconds);
uint32_t slotSecond = (uint32_t)NODE_SLOT_INDEX * kSlotSeconds;
if (slotSecond >= frameSeconds)
return;
uint32_t secInFrame = (uint32_t)now.second % frameSeconds;
if (secInFrame >= requiredTxSeconds)
return; // idle guard interval
if (secInFrame != slotSecond)
return;

View file

@ -0,0 +1,16 @@
# Exercise 15: RAM
This exercise shows available RAM on the console and on the OLED display on a T-Beam Supreme.
Behavior:
- Reports heap statistics every second over serial.
- Shows live heap status on the OLED display.
- Designed as the first step toward volatile /tmp RAM-backed storage.
Build and upload:
```bash
cd /usr/local/src/microreticulum/microReticulumTbeam/exercises/15_RAM
pio run -e amy -t upload
pio device monitor -b 115200
```

View file

@ -0,0 +1,55 @@
; 20260403 ChatGPT
; Exercise 15_RAM
[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 =
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\"

View file

@ -0,0 +1,12 @@
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),
]
)

View file

@ -0,0 +1,272 @@
// 20260403 ChatGPT
// Exercise 15_RAM
#include <Arduino.h>
#include <Wire.h>
#include <U8g2lib.h>
#include <time.h>
#ifndef NODE_LABEL
#define NODE_LABEL "RAM"
#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
static U8G2_SH1106_128X64_NONAME_F_HW_I2C g_oled(U8G2_R0, U8X8_PIN_NONE);
static const char *kTmpPath = "/tmp/AMY_output.log";
static const size_t kTmpFileCapacity = 32768;
static char g_tmpFileBuffer[kTmpFileCapacity];
static size_t g_tmpFileSize = 0;
static unsigned g_tmpLineNumber = 0;
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 size_t getAvailableRamBytes()
{
return ESP.getFreeHeap();
}
static void getTimestamp(char *out, size_t outSize)
{
const time_t now = time(nullptr);
if (now > 1700000000) {
struct tm tmNow;
localtime_r(&now, &tmNow);
snprintf(out, outSize, "%04d-%02d-%02d %02d:%02d:%02d",
tmNow.tm_year + 1900,
tmNow.tm_mon + 1,
tmNow.tm_mday,
tmNow.tm_hour,
tmNow.tm_min,
tmNow.tm_sec);
return;
}
const uint32_t sec = millis() / 1000;
const uint32_t hh = sec / 3600;
const uint32_t mm = (sec % 3600) / 60;
const uint32_t ss = sec % 60;
snprintf(out, outSize, "uptime %02u:%02u:%02u", (unsigned)hh, (unsigned)mm, (unsigned)ss);
}
static void appendTimestampLine()
{
char timestamp[32];
getTimestamp(timestamp, sizeof(timestamp));
char line[96];
const int written = snprintf(line, sizeof(line), "%u, %s\r\n", g_tmpLineNumber + 1, timestamp);
if (written <= 0) {
return;
}
const size_t lineLen = (size_t)written;
if (g_tmpFileSize + lineLen > kTmpFileCapacity - 1) {
Serial.println("Warning: /tmp log full, stopping writes");
return;
}
memcpy(g_tmpFileBuffer + g_tmpFileSize, line, lineLen);
g_tmpFileSize += lineLen;
g_tmpLineNumber++;
}
static void printRamStatus()
{
const size_t freeBytes = getAvailableRamBytes();
const size_t totalBytes = ESP.getHeapSize();
const size_t maxAlloc = ESP.getMaxAllocHeap();
Serial.printf("RAM total=%u free=%u maxAlloc=%u\r\n", (unsigned)totalBytes, (unsigned)freeBytes, (unsigned)maxAlloc);
char line1[32];
char line2[32];
char line3[32];
snprintf(line1, sizeof(line1), "Exercise 15 RAM");
snprintf(line2, sizeof(line2), "Node: %s", NODE_LABEL);
snprintf(line3, sizeof(line3), "Free: %u KB", (unsigned)(freeBytes / 1024U));
char line4[32];
snprintf(line4, sizeof(line4), "Total: %u KB", (unsigned)(totalBytes / 1024U));
char line5[32];
snprintf(line5, sizeof(line5), "Lines: %u", (unsigned)g_tmpLineNumber);
oledShowLines(line1, line2, line3, line4, line5);
}
static void showHelp()
{
Serial.println("RAM command list:");
Serial.println(" help - show this menu");
Serial.println(" stat - show /tmp file state");
Serial.println(" read - read /tmp contents");
Serial.println(" clear - clear /tmp contents");
Serial.println(" write <text> - write text to /tmp");
Serial.println(" append <text> - append text to /tmp");
}
static void printTmpFileStat()
{
Serial.printf("Path: %s\r\n", kTmpPath);
Serial.printf("Size: %u bytes\r\n", (unsigned)g_tmpFileSize);
Serial.printf("Lines: %u\r\n", (unsigned)g_tmpLineNumber);
Serial.printf("Capacity: %u bytes\r\n", (unsigned)kTmpFileCapacity);
}
static void printTmpFileContents()
{
if (g_tmpFileSize == 0) {
Serial.println("/tmp file is empty");
return;
}
Serial.print("/tmp contents: ");
Serial.write((const uint8_t *)g_tmpFileBuffer, g_tmpFileSize);
if (g_tmpFileBuffer[g_tmpFileSize - 1] != '\n')
Serial.println();
}
static void setTmpFileContent(const char *text)
{
if (!text) {
g_tmpFileSize = 0;
return;
}
const size_t newLen = strlen(text);
if (newLen > kTmpFileCapacity - 1) {
Serial.printf("Error: content too large (%u/%u)\r\n", (unsigned)newLen, (unsigned)kTmpFileCapacity);
return;
}
memcpy(g_tmpFileBuffer, text, newLen);
g_tmpFileSize = newLen;
}
static void appendTmpFileContent(const char *text)
{
if (!text || text[0] == '\0') return;
const size_t textLen = strlen(text);
if (g_tmpFileSize + textLen > kTmpFileCapacity - 1) {
Serial.printf("Error: append would exceed %u bytes\r\n", (unsigned)kTmpFileCapacity);
return;
}
memcpy(g_tmpFileBuffer + g_tmpFileSize, text, textLen);
g_tmpFileSize += textLen;
}
static void processSerialCommand(const char *line)
{
if (!line || line[0] == '\0') return;
char tmp[384];
strncpy(tmp, line, sizeof(tmp) - 1);
tmp[sizeof(tmp) - 1] = '\0';
char *cmd = strtok(tmp, " \t\r\n");
if (!cmd) return;
if (strcasecmp(cmd, "help") == 0) {
showHelp();
return;
}
if (strcasecmp(cmd, "stat") == 0) {
printTmpFileStat();
return;
}
if (strcasecmp(cmd, "read") == 0) {
printTmpFileContents();
return;
}
if (strcasecmp(cmd, "clear") == 0) {
g_tmpFileSize = 0;
Serial.println("/tmp cleared");
return;
}
if (strcasecmp(cmd, "write") == 0 || strcasecmp(cmd, "append") == 0) {
const char *payload = line + strlen(cmd);
while (*payload == ' ' || *payload == '\t') payload++;
if (strcasecmp(cmd, "write") == 0)
setTmpFileContent(payload);
else
appendTmpFileContent(payload);
Serial.printf("%s: %u bytes\r\n", cmd,
(unsigned)g_tmpFileSize);
return;
}
Serial.println("Unknown command (help for list)");
}
void setup()
{
Serial.begin(115200);
delay(800);
Serial.println("Exercise 15_RAM boot");
Wire.begin(OLED_SDA, OLED_SCL);
g_oled.setI2CAddress(OLED_ADDR << 1);
g_oled.begin();
oledShowLines("Exercise 15_RAM", "Node: " NODE_LABEL, "Booting...");
delay(1000);
}
void loop()
{
static uint32_t lastMs = 0;
const uint32_t now = millis();
// check serial commands at all times
static char rxLine[384];
static size_t rxLen = 0;
while (Serial.available()) {
int c = Serial.read();
if (c <= 0) continue;
if (c == '\r' || c == '\n') {
if (rxLen > 0) {
rxLine[rxLen] = '\0';
processSerialCommand(rxLine);
rxLen = 0;
}
} else if (rxLen + 1 < sizeof(rxLine)) {
rxLine[rxLen++] = (char)c;
}
}
if (now - lastMs < 1000) {
delay(10);
return;
}
lastMs = now;
appendTimestampLine();
printRamStatus();
}

View file

@ -0,0 +1,32 @@
# Exercise 16: PSRAM
This exercise demonstrates usage of PSRAM (Pseudo SRAM) on an ESP32-S3 board, alongside regular RAM metrics.
Behavior:
- Reports heap and PSRAM statistics every second over serial.
- Shows live heap and PSRAM status on the OLED display (both on same line).
- Allows you to write/append/read/clear data in a PSRAM-backed buffer (up to ~2MB).
- Designed as an extension of Exercise 15_RAM to explore larger volatile storage.
Note: the exercise now targets a PSRAM-enabled ESP32-S3 board definition (`freenove_esp32_s3_wroom`). This board profile has 8MB flash + 8MB PSRAM, matching the T-Beam Supreme specifications. If your hardware differs, adjust accordingly.
Sources:
- LilyGo T-Beam SUPREME datasheet/wiki: https://wiki.lilygo.cc/get_started/en/LoRa_GPS/T-Beam-SUPREME/T-Beam-SUPREME.html
- PlatformIO board definition: https://docs.platformio.org/page/boards/espressif32/freenove_esp32_s3_wroom.html
- Local PlatformIO board metadata: ~/.platformio/platforms/espressif32/boards/freenove_esp32_s3_wroom.json
Build and upload:
```bash
cd /usr/local/src/microreticulum/microReticulumTbeam/exercises/16_PSRAM
pio run -e amy -t upload
pio device monitor -b 115200
```
Commands:
- `help` - show command menu
- `stat` - show PSRAM buffer state
- `read` - read PSRAM buffer contents
- `clear` - clear PSRAM buffer
- `write <text>` - write text to PSRAM buffer
- `append <text>` - append text to PSRAM buffer

View file

@ -0,0 +1,54 @@
; 20260403 ChatGPT
; Exercise 16_PSRAM
[platformio]
default_envs = amy
[env]
platform = espressif32
framework = arduino
board = freenove_esp32_s3_wroom
monitor_speed = 115200
extra_scripts = pre:scripts/set_build_epoch.py
lib_deps =
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
board_build.flash_mode = qio
board_build.psram = 1
board_build.psram_type = spi
board_build.arduino.memory_type = qio_qspi
[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\"

View file

@ -0,0 +1,12 @@
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),
]
)

View file

@ -0,0 +1,342 @@
// 20260403 ChatGPT
// Exercise 16_PSRAM - Extended Exercise 15_RAM with PSRAM support
#include <Arduino.h>
#include <Wire.h>
#include <U8g2lib.h>
#include <time.h>
#include <esp_heap_caps.h>
#ifndef NODE_LABEL
#define NODE_LABEL "PSRAM"
#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
static U8G2_SH1106_128X64_NONAME_F_HW_I2C g_oled(U8G2_R0, U8X8_PIN_NONE);
static const size_t kTmpFileCapacity = 2097152; // 2MB in PSRAM
static char *g_tmpFileBuffer = nullptr;
static size_t g_tmpFileSize = 0;
static unsigned g_tmpLineNumber = 0;
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 size_t getAvailableRamBytes()
{
return ESP.getFreeHeap();
}
static size_t getTotalRamBytes()
{
return ESP.getHeapSize();
}
static size_t getPSRAMFreeBytes()
{
size_t freeBytes = heap_caps_get_free_size(MALLOC_CAP_SPIRAM);
if (freeBytes == 0 && ESP.getFreePsram() > 0) {
freeBytes = ESP.getFreePsram();
}
return freeBytes;
}
static size_t getPSRAMTotalBytes()
{
size_t totalBytes = heap_caps_get_total_size(MALLOC_CAP_SPIRAM);
if (totalBytes == 0 && ESP.getPsramSize() > 0) {
totalBytes = ESP.getPsramSize();
}
return totalBytes;
}
static void getTimestamp(char *out, size_t outSize)
{
const time_t now = time(nullptr);
if (now > 1700000000) {
struct tm tmNow;
localtime_r(&now, &tmNow);
snprintf(out, outSize, "%04d-%02d-%02d %02d:%02d:%02d",
tmNow.tm_year + 1900,
tmNow.tm_mon + 1,
tmNow.tm_mday,
tmNow.tm_hour,
tmNow.tm_min,
tmNow.tm_sec);
return;
}
const uint32_t sec = millis() / 1000;
const uint32_t hh = sec / 3600;
const uint32_t mm = (sec % 3600) / 60;
const uint32_t ss = sec % 60;
snprintf(out, outSize, "uptime %02u:%02u:%02u", (unsigned)hh, (unsigned)mm, (unsigned)ss);
}
static void appendTimestampLine()
{
if (!g_tmpFileBuffer) return;
char timestamp[32];
getTimestamp(timestamp, sizeof(timestamp));
char line[96];
const int written = snprintf(line, sizeof(line), "%u, %s\r\n", g_tmpLineNumber + 1, timestamp);
if (written <= 0) {
return;
}
const size_t lineLen = (size_t)written;
if (g_tmpFileSize + lineLen > kTmpFileCapacity - 1) {
Serial.println("Warning: PSRAM log full, stopping writes");
return;
}
memcpy(g_tmpFileBuffer + g_tmpFileSize, line, lineLen);
g_tmpFileSize += lineLen;
g_tmpLineNumber++;
}
static void printRamStatus()
{
const size_t freeRam = getAvailableRamBytes();
const size_t totalRam = getTotalRamBytes();
const size_t maxAllocRam = ESP.getMaxAllocHeap();
const size_t freePSRAM = getPSRAMFreeBytes();
const size_t totalPSRAM = getPSRAMTotalBytes();
Serial.printf("RAM total=%u free=%u maxAlloc=%u | PSRAM total=%u free=%u\r\n",
(unsigned)totalRam, (unsigned)freeRam, (unsigned)maxAllocRam,
(unsigned)totalPSRAM, (unsigned)freePSRAM);
char line1[32];
char line2[32];
char line3[32];
char line4[32];
char line5[32];
snprintf(line1, sizeof(line1), "Exercise 16 PSRAM");
snprintf(line2, sizeof(line2), "Node: %s", NODE_LABEL);
// Display format: "Free XXXKb/8.0Mbs"
const float psramMb = totalPSRAM / (1024.0f * 1024.0f);
const size_t ramKb = freeRam / 1024U;
snprintf(line3, sizeof(line3), "Free %uKb/%.1fMbs", (unsigned)ramKb, psramMb);
snprintf(line4, sizeof(line4), "PSRAM: %u KB", (unsigned)(freePSRAM / 1024U));
snprintf(line5, sizeof(line5), "Lines: %u", (unsigned)g_tmpLineNumber);
oledShowLines(line1, line2, line3, line4, line5);
}
static void showHelp()
{
Serial.println("PSRAM command list:");
Serial.println(" help - show this menu");
Serial.println(" stat - show PSRAM buffer state");
Serial.println(" read - read PSRAM buffer contents");
Serial.println(" clear - clear PSRAM buffer contents");
Serial.println(" write <text> - write text to PSRAM buffer");
Serial.println(" append <text> - append text to PSRAM buffer");
}
static void printPSRAMFileStat()
{
Serial.printf("PSRAM Buffer Capacity: %u bytes\r\n", (unsigned)kTmpFileCapacity);
Serial.printf("Current Size: %u bytes\r\n", (unsigned)g_tmpFileSize);
Serial.printf("Lines: %u\r\n", (unsigned)g_tmpLineNumber);
Serial.printf("PSRAM Total: %u bytes (%.2f MB)\r\n", (unsigned)getPSRAMTotalBytes(),
getPSRAMTotalBytes() / (1024.0f * 1024.0f));
Serial.printf("PSRAM Free: %u bytes\r\n", (unsigned)getPSRAMFreeBytes());
}
static void printPSRAMFileContents()
{
if (!g_tmpFileBuffer) {
Serial.println("PSRAM buffer not allocated");
return;
}
if (g_tmpFileSize == 0) {
Serial.println("PSRAM buffer is empty");
return;
}
Serial.print("PSRAM contents: ");
Serial.write((const uint8_t *)g_tmpFileBuffer, g_tmpFileSize);
if (g_tmpFileBuffer[g_tmpFileSize - 1] != '\n')
Serial.println();
}
static void setPSRAMFileContent(const char *text)
{
if (!g_tmpFileBuffer) {
Serial.println("Error: PSRAM buffer not allocated");
return;
}
if (!text) {
g_tmpFileSize = 0;
g_tmpLineNumber = 0;
return;
}
const size_t newLen = strlen(text);
if (newLen > kTmpFileCapacity - 1) {
Serial.printf("Error: content too large (%u/%u)\r\n", (unsigned)newLen, (unsigned)kTmpFileCapacity);
return;
}
memcpy(g_tmpFileBuffer, text, newLen);
g_tmpFileSize = newLen;
g_tmpLineNumber = 0;
}
static void appendPSRAMFileContent(const char *text)
{
if (!g_tmpFileBuffer) {
Serial.println("Error: PSRAM buffer not allocated");
return;
}
if (!text || text[0] == '\0') return;
const size_t textLen = strlen(text);
if (g_tmpFileSize + textLen > kTmpFileCapacity - 1) {
Serial.printf("Error: append would exceed %u bytes\r\n", (unsigned)kTmpFileCapacity);
return;
}
memcpy(g_tmpFileBuffer + g_tmpFileSize, text, textLen);
g_tmpFileSize += textLen;
}
static void processSerialCommand(const char *line)
{
if (!line || line[0] == '\0') return;
char tmp[384];
strncpy(tmp, line, sizeof(tmp) - 1);
tmp[sizeof(tmp) - 1] = '\0';
char *cmd = strtok(tmp, " \t\r\n");
if (!cmd) return;
if (strcasecmp(cmd, "help") == 0) {
showHelp();
return;
}
if (strcasecmp(cmd, "stat") == 0) {
printPSRAMFileStat();
return;
}
if (strcasecmp(cmd, "read") == 0) {
printPSRAMFileContents();
return;
}
if (strcasecmp(cmd, "clear") == 0) {
g_tmpFileSize = 0;
g_tmpLineNumber = 0;
Serial.println("PSRAM buffer cleared");
return;
}
if (strcasecmp(cmd, "write") == 0 || strcasecmp(cmd, "append") == 0) {
const char *payload = line + strlen(cmd);
while (*payload == ' ' || *payload == '\t') payload++;
if (strcasecmp(cmd, "write") == 0)
setPSRAMFileContent(payload);
else
appendPSRAMFileContent(payload);
Serial.printf("%s: %u bytes\r\n", cmd,
(unsigned)g_tmpFileSize);
return;
}
Serial.println("Unknown command (help for list)");
}
void setup()
{
Serial.begin(115200);
delay(800);
Serial.println("Exercise 16_PSRAM boot");
// Boot-time PSRAM diagnostics
Serial.printf("Boot PSRAM size: %u bytes\r\n", (unsigned)ESP.getPsramSize());
Serial.printf("Boot PSRAM free: %u bytes\r\n", (unsigned)ESP.getFreePsram());
// Allocate PSRAM buffer
g_tmpFileBuffer = (char *)heap_caps_malloc(kTmpFileCapacity, MALLOC_CAP_SPIRAM);
if (!g_tmpFileBuffer) {
Serial.println("ERROR: Failed to allocate PSRAM buffer!");
oledShowLines("Exercise 16_PSRAM", "Node: " NODE_LABEL, "PSRAM alloc FAILED");
} else {
Serial.printf("PSRAM buffer allocated: %u bytes\r\n", (unsigned)kTmpFileCapacity);
}
Wire.begin(OLED_SDA, OLED_SCL);
g_oled.setI2CAddress(OLED_ADDR << 1);
g_oled.begin();
oledShowLines("Exercise 16_PSRAM", "Node: " NODE_LABEL, "Booting...");
delay(1000);
}
void loop()
{
static uint32_t lastMs = 0;
const uint32_t now = millis();
// check serial commands at all times
static char rxLine[384];
static size_t rxLen = 0;
while (Serial.available()) {
int c = Serial.read();
if (c <= 0) continue;
if (c == '\r' || c == '\n') {
if (rxLen > 0) {
rxLine[rxLen] = '\0';
processSerialCommand(rxLine);
rxLen = 0;
}
} else if (rxLen + 1 < sizeof(rxLine)) {
rxLine[rxLen++] = (char)c;
}
}
if (now - lastMs < 1000) {
delay(10);
return;
}
lastMs = now;
if (g_tmpFileBuffer) {
appendTimestampLine();
}
printRamStatus();
}

View file

@ -0,0 +1,32 @@
# Exercise 17_Flash
This exercise demonstrates using Flash storage as a persistent directory-like file system on an ESP32-S3 board.
Behavior:
- Mounts SPIFFS at boot and reports total / used / free flash space.
- Ensures a flash directory at `/flash_logs` exists.
- Creates a new log file when the device boots, based on the current timestamp: `YYYYMMDD_HHMM.log`.
- Writes a timestamped line into the new log file once per second.
- Supports console commands to inspect the current file, read it, clear it, append or rewrite it, and list stored files.
- Files persist across reboots and are stored in flash.
Build and upload:
```bash
cd /usr/local/src/microreticulum/microReticulumTbeam/exercises/17_Flash
pio run -e amy -t upload
pio device monitor -b 115200
```
Commands:
- `help` - show command menu
- `stat` - show flash / current file status
- `list` - list files under `/flash_logs`
- `read` - read the current flash file contents
- `clear` - clear the current flash file contents
- `write <text>` - overwrite the current flash file with text
- `append <text>` - append text to the current flash file
Notes:
- If the current timestamp file name already exists, the exercise will append a numeric suffix to keep the file unique.
- On each reboot a new file is created so persistent flash logs accumulate.

View file

@ -0,0 +1,50 @@
; 20260403 ChatGPT
; Exercise 17_Flash
[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 =
Wire
olikraus/U8g2@^2.36.4
lewisxhe/XPowersLib@0.3.3
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\"

View file

@ -0,0 +1,11 @@
import struct
with open('AMY_test_partitions_read.bin', 'rb') as f:
data = f.read()
seq0 = struct.unpack('<I', data[0:4])[0]
seq1 = struct.unpack('<I', data[32:36])[0]
print(f"OTA seq0: {seq0:08x}")
print(f"OTA seq1: {seq1:08x}")
if seq0 > seq1:
print("→ app0 is active, new uploads go to app1")
else:
print("→ app1 is active, new uploads go to app0")

View file

@ -0,0 +1,12 @@
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),
]
)

View file

@ -0,0 +1,26 @@
import struct
with open('partitions_backup.bin', 'rb') as f:
data = f.read()
print("Name | Type | SubType | Offset | Size | Flags")
print("-" * 75)
for i in range(0, len(data), 32):
entry = data[i:i+32]
if len(entry) < 32:
break
magic = struct.unpack('<H', entry[0:2])[0]
if magic == 0x50aa: # Valid partition magic
type_val = entry[2]
subtype = entry[3]
offset = struct.unpack('<I', entry[4:8])[0]
size = struct.unpack('<I', entry[8:12])[0]
flags = struct.unpack('<H', entry[12:14])[0]
name = entry[16:32].rstrip(b'\x00').decode('ascii', errors='ignore')
print(f"{name:<13} | {type_val:02x} | {subtype:02x} | 0x{offset:08x} | 0x{size:08x} | {flags:04x}")
elif magic == 0xebeb:
break # End marker

View file

@ -0,0 +1,608 @@
// 20260403 ChatGPT
// Exercise 17_Flash
#include <Arduino.h>
#include <Wire.h>
#include <U8g2lib.h>
#include <time.h>
#include <SPIFFS.h>
#include "tbeam_supreme_adapter.h"
#ifndef NODE_LABEL
#define NODE_LABEL "FLASH"
#endif
#ifndef RTC_I2C_ADDR
#define RTC_I2C_ADDR 0x51
#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
static U8G2_SH1106_128X64_NONAME_F_HW_I2C g_oled(U8G2_R0, U8X8_PIN_NONE);
static const char *kFlashDir = "/flash_logs";
static char g_currentFilePath[64] = {0};
static File g_flashFile;
static unsigned g_flashLineNumber = 0;
static XPowersLibInterface* g_pmu = nullptr;
static bool g_hasRtc = false;
static bool g_rtcLowVoltage = false;
struct RtcDateTime {
uint16_t year;
uint8_t month;
uint8_t day;
uint8_t hour;
uint8_t minute;
uint8_t second;
uint8_t weekday;
};
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 size_t getFlashTotalBytes()
{
return SPIFFS.totalBytes();
}
static size_t getFlashUsedBytes()
{
return SPIFFS.usedBytes();
}
static size_t getFlashFreeBytes()
{
const size_t total = getFlashTotalBytes();
const size_t used = getFlashUsedBytes();
return total > used ? total - used : 0;
}
static uint8_t toBcd(uint8_t v) {
return ((v / 10U) << 4U) | (v % 10U);
}
static uint8_t fromBcd(uint8_t b) {
return ((b >> 4U) * 10U) + (b & 0x0FU);
}
static bool isRtcDateTimeValid(const RtcDateTime& dt) {
if (dt.year < 2020 || dt.year > 2099) return false;
if (dt.month < 1 || dt.month > 12) return false;
if (dt.day < 1 || dt.day > 31) return false;
if (dt.hour > 23 || dt.minute > 59 || dt.second > 59) return false;
return true;
}
static bool rtcRead(RtcDateTime& 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();
uint8_t weekday = 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.weekday = fromBcd(weekday & 0x07U);
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 initRtc() {
if (!tbeam_supreme::initPmuForPeripherals(g_pmu, &Serial)) {
Serial.println("RTC init: PMU/i2c init failed");
return false;
}
RtcDateTime now{};
if (!rtcRead(now, g_rtcLowVoltage) || !isRtcDateTimeValid(now)) {
Serial.println("RTC init: no valid time available");
return false;
}
g_hasRtc = true;
Serial.printf("RTC init: %04u-%02u-%02u %02u:%02u:%02u%s\r\n",
(unsigned)now.year, (unsigned)now.month, (unsigned)now.day,
(unsigned)now.hour, (unsigned)now.minute, (unsigned)now.second,
g_rtcLowVoltage ? " [LOW_BATT]" : "");
return true;
}
static bool getRtcTimestamp(char *out, size_t outSize) {
if (!g_hasRtc) {
return false;
}
RtcDateTime now{};
bool low = false;
if (!rtcRead(now, low) || !isRtcDateTimeValid(now)) {
return false;
}
g_rtcLowVoltage = low;
snprintf(out, outSize, "%04u-%02u-%02u %02u:%02u:%02u",
now.year,
now.month,
now.day,
now.hour,
now.minute,
now.second);
return true;
}
static void getTimestamp(char *out, size_t outSize)
{
if (getRtcTimestamp(out, outSize)) {
return;
}
const time_t now = time(nullptr);
if (now > 1700000000) {
struct tm tmNow;
localtime_r(&now, &tmNow);
snprintf(out, outSize, "%04d-%02d-%02d %02d:%02d:%02d",
tmNow.tm_year + 1900,
tmNow.tm_mon + 1,
tmNow.tm_mday,
tmNow.tm_hour,
tmNow.tm_min,
tmNow.tm_sec);
return;
}
const uint32_t sec = millis() / 1000;
const uint32_t hh = sec / 3600;
const uint32_t mm = (sec % 3600) / 60;
const uint32_t ss = sec % 60;
snprintf(out, outSize, "uptime %02u:%02u:%02u", (unsigned)hh, (unsigned)mm, (unsigned)ss);
}
static void getFilenameTimestamp(char *out, size_t outSize)
{
if (g_hasRtc) {
RtcDateTime now{};
bool low = false;
if (rtcRead(now, low) && isRtcDateTimeValid(now)) {
snprintf(out, outSize, "%04u%02u%02u_%02u%02u",
now.year,
now.month,
now.day,
now.hour,
now.minute);
return;
}
}
const time_t now = time(nullptr);
if (now > 1700000000) {
struct tm tmNow;
localtime_r(&now, &tmNow);
snprintf(out, outSize, "%04d%02d%02d_%02d%02d",
tmNow.tm_year + 1900,
tmNow.tm_mon + 1,
tmNow.tm_mday,
tmNow.tm_hour,
tmNow.tm_min);
return;
}
const uint32_t sec = millis() / 1000;
const uint32_t hh = sec / 3600;
const uint32_t mm = (sec % 3600) / 60;
const uint32_t ss = sec % 60;
snprintf(out, outSize, "uptime_%02u%02u%02u", (unsigned)hh, (unsigned)mm, (unsigned)ss);
}
static String getNewFlashFilePath()
{
char baseName[64];
getFilenameTimestamp(baseName, sizeof(baseName));
char candidate[96];
snprintf(candidate, sizeof(candidate), "%s/%s.log", kFlashDir, baseName);
if (!SPIFFS.exists(candidate)) {
return String(candidate);
}
int suffix = 1;
do {
snprintf(candidate, sizeof(candidate), "%s/%s-%d.log", kFlashDir, baseName, suffix);
suffix += 1;
} while (SPIFFS.exists(candidate));
return String(candidate);
}
static bool ensureFlashDirectory()
{
if (SPIFFS.exists(kFlashDir)) {
return true;
}
if (!SPIFFS.mkdir(kFlashDir)) {
Serial.printf("Warning: failed to create %s\r\n", kFlashDir);
return false;
}
return true;
}
static bool openCurrentFlashFile(bool truncate = false)
{
if (g_flashFile) {
g_flashFile.close();
}
if (truncate) {
g_flashFile = SPIFFS.open(g_currentFilePath, FILE_WRITE);
} else {
g_flashFile = SPIFFS.open(g_currentFilePath, FILE_APPEND);
}
if (!g_flashFile) {
Serial.printf("ERROR: cannot open %s\r\n", g_currentFilePath);
return false;
}
return true;
}
static bool createFlashLogFile()
{
if (!ensureFlashDirectory()) {
return false;
}
String path = getNewFlashFilePath();
path.toCharArray(g_currentFilePath, sizeof(g_currentFilePath));
g_flashFile = SPIFFS.open(g_currentFilePath, FILE_WRITE);
if (!g_flashFile) {
Serial.printf("ERROR: could not create %s\r\n", g_currentFilePath);
return false;
}
const char *header = "FLASH log file created\r\n";
g_flashFile.print(header);
g_flashFile.flush();
g_flashLineNumber = 0;
return true;
}
static void appendFlashTimestampLine()
{
if (!g_flashFile) {
return;
}
char timestamp[32];
getTimestamp(timestamp, sizeof(timestamp));
char line[96];
const int written = snprintf(line, sizeof(line), "%u, %s\r\n", g_flashLineNumber + 1, timestamp);
if (written <= 0) {
return;
}
const size_t lineLen = (size_t)written;
if (g_flashFile.write(reinterpret_cast<const uint8_t *>(line), lineLen) != lineLen) {
Serial.println("Warning: flash write failed");
return;
}
g_flashFile.flush();
g_flashLineNumber += 1;
}
static void printFlashStatus()
{
const size_t total = getFlashTotalBytes();
const size_t used = getFlashUsedBytes();
const size_t freeBytes = getFlashFreeBytes();
Serial.printf("FLASH total=%u used=%u free=%u\r\n",
(unsigned)total, (unsigned)used, (unsigned)freeBytes);
char line1[32];
char line2[32];
char line3[32];
char line4[32];
char line5[32];
snprintf(line1, sizeof(line1), "Exercise 17 Flash");
snprintf(line2, sizeof(line2), "Node: %s", NODE_LABEL);
snprintf(line3, sizeof(line3), "Free: %u KB", (unsigned)(freeBytes / 1024U));
snprintf(line4, sizeof(line4), "Used: %u KB", (unsigned)(used / 1024U));
snprintf(line5, sizeof(line5), "Lines: %u", (unsigned)g_flashLineNumber);
oledShowLines(line1, line2, line3, line4, line5);
}
static void showHelp()
{
Serial.println("Flash command list:");
Serial.println(" help - show this menu");
Serial.println(" stat - show flash/file state");
Serial.println(" rtc - show RTC time status");
Serial.println(" list - list files in /flash_logs");
Serial.println(" read - read current flash file");
Serial.println(" clear - clear current flash file");
Serial.println(" write <text> - overwrite current flash file");
Serial.println(" append <text> - append text to current flash file");
}
static void printFlashFileStat()
{
Serial.printf("Current file: %s\r\n", g_currentFilePath);
if (!SPIFFS.exists(g_currentFilePath)) {
Serial.println("Current file missing");
return;
}
File file = SPIFFS.open(g_currentFilePath, FILE_READ);
if (!file) {
Serial.println("Unable to open current file for stats");
return;
}
Serial.printf("Size: %u bytes\r\n", (unsigned)file.size());
Serial.printf("Lines written: %u\r\n", (unsigned)g_flashLineNumber);
file.close();
}
static void printFlashFileContents()
{
if (!SPIFFS.exists(g_currentFilePath)) {
Serial.println("Current flash file does not exist");
return;
}
File file = SPIFFS.open(g_currentFilePath, FILE_READ);
if (!file) {
Serial.println("Unable to open current flash file");
return;
}
if (file.size() == 0) {
Serial.println("Current flash file is empty");
file.close();
return;
}
Serial.print("Flash file contents: ");
while (file.available()) {
Serial.write(file.read());
}
if (file.size() > 0) {
Serial.println();
}
file.close();
}
static void clearFlashFileContents()
{
if (!SPIFFS.exists(g_currentFilePath)) {
Serial.println("No current file to clear");
return;
}
if (!openCurrentFlashFile(true)) {
return;
}
g_flashFile.close();
g_flashLineNumber = 0;
openCurrentFlashFile(false);
Serial.println("Current flash file cleared");
}
static void setFlashFileContent(const char *text)
{
if (!text) {
clearFlashFileContents();
return;
}
File file = SPIFFS.open(g_currentFilePath, FILE_WRITE);
if (!file) {
Serial.println("Unable to overwrite current flash file");
return;
}
file.print(text);
file.close();
openCurrentFlashFile(false);
g_flashLineNumber = 0;
}
static void appendFlashFileContent(const char *text)
{
if (!text || text[0] == '\0') {
return;
}
if (!openCurrentFlashFile(false)) {
return;
}
g_flashFile.print(text);
g_flashFile.flush();
}
static void listFlashFiles()
{
File dir = SPIFFS.open(kFlashDir);
if (!dir || !dir.isDirectory()) {
Serial.printf("Unable to list files in %s\r\n", kFlashDir);
return;
}
Serial.printf("Files in %s:\r\n", kFlashDir);
File file = dir.openNextFile();
while (file) {
Serial.printf(" %s (%u bytes)\r\n", file.name(), (unsigned)file.size());
file = dir.openNextFile();
}
dir.close();
}
static void processSerialCommand(const char *line)
{
if (!line || line[0] == '\0') return;
char tmp[384];
strncpy(tmp, line, sizeof(tmp) - 1);
tmp[sizeof(tmp) - 1] = '\0';
char *cmd = strtok(tmp, " \t\r\n");
if (!cmd) return;
if (strcasecmp(cmd, "help") == 0) {
showHelp();
return;
}
if (strcasecmp(cmd, "stat") == 0) {
printFlashFileStat();
return;
}
if (strcasecmp(cmd, "rtc") == 0) {
if (g_hasRtc) {
char ts[32];
if (getRtcTimestamp(ts, sizeof(ts))) {
Serial.printf("RTC now: %s\r\n", ts);
if (g_rtcLowVoltage) {
Serial.println("RTC low-voltage flag is set");
}
} else {
Serial.println("RTC present but time read failed");
}
} else {
Serial.println("RTC unavailable");
}
return;
}
if (strcasecmp(cmd, "list") == 0) {
listFlashFiles();
return;
}
if (strcasecmp(cmd, "read") == 0) {
printFlashFileContents();
return;
}
if (strcasecmp(cmd, "clear") == 0) {
clearFlashFileContents();
return;
}
if (strcasecmp(cmd, "write") == 0 || strcasecmp(cmd, "append") == 0) {
const char *payload = line + strlen(cmd);
while (*payload == ' ' || *payload == '\t') payload++;
if (strcasecmp(cmd, "write") == 0)
setFlashFileContent(payload);
else
appendFlashFileContent(payload);
Serial.printf("%s: %s\r\n", cmd, payload);
return;
}
Serial.println("Unknown command (help for list)");
}
void setup()
{
Serial.begin(115200);
delay(800);
Serial.println("Exercise 17_Flash boot");
initRtc();
if (!SPIFFS.begin(true)) {
Serial.println("ERROR: SPIFFS mount failed");
oledShowLines("Exercise 17_Flash", "Node: " NODE_LABEL, "SPIFFS mount FAILED");
} else {
Serial.println("SPIFFS mounted successfully");
if (createFlashLogFile()) {
Serial.printf("Current flash file: %s\r\n", g_currentFilePath);
}
}
Wire.begin(OLED_SDA, OLED_SCL);
g_oled.setI2CAddress(OLED_ADDR << 1);
g_oled.begin();
oledShowLines("Exercise 17_Flash", "Node: " NODE_LABEL, "Booting...");
delay(1000);
}
void loop()
{
static uint32_t lastMs = 0;
const uint32_t now = millis();
static char rxLine[384];
static size_t rxLen = 0;
while (Serial.available()) {
int c = Serial.read();
if (c <= 0) continue;
if (c == '\r' || c == '\n') {
if (rxLen > 0) {
rxLine[rxLen] = '\0';
processSerialCommand(rxLine);
rxLen = 0;
}
} else if (rxLen + 1 < sizeof(rxLine)) {
rxLine[rxLen++] = (char)c;
}
}
if (now - lastMs < 1000) {
delay(10);
return;
}
lastMs = now;
if (g_flashFile) {
appendFlashTimestampLine();
}
printFlashStatus();
}

95
notes_Feb_18_2026.txt Normal file
View file

@ -0,0 +1,95 @@
A:
pio run -e node_a -t upload --upload-port /dev/ttyACM0
then:
date; screen /dev/ttyACM0 115200
B:
pio run -e node_b -t upload --upload-port /dev/ttyACM1
then:
date; screen /dev/ttyACM1 115200
tbeam:
Console 1:
cd /usr/local/src/sx1302_hal/packet_forwarder
sudo ./lora_pkt_fwd -c global_conf.reticulum_915000000_sf8_bw125.json
Console 2:
cd /usr/local/src/sx1302_hal/util_net_downlink
./net_downlink -P 1730 -l uplinks_$(date +%Y%m%d_%H%M%S).csv
Example capture:
INFO: Received pkt from mote: 65732042 (fcnt=29540)
JSON up: {"rxpk":[{"jver":1,"tmst":413765588,"chan":8,"rfch":0,"freq":915.000000,"mid":16,"stat":1,"modu":"LORA","datr":"SF7BW125","codr":"4/5","rssis":-16,"lsnr":10.8,"foff":-45,"rssi":-15,"size":29,"data":"IEIgc2VuZHMgZ3JlZXRpbmdzLiBpdGVyPTE1MDQ="}]}
INFO: [up] PUSH_ACK received in 31 ms
INFO: Received pkt from mote: 65732041 (fcnt=29540)
JSON up: {"rxpk":[{"jver":1,"tmst":414742074,"chan":8,"rfch":0,"freq":915.000000,"mid":16,"stat":1,"modu":"LORA","datr":"SF7BW125","codr":"4/5","rssis":-16,"lsnr":10.2,"foff":-297,"rssi":-15,"size":29,"data":"IEEgc2VuZHMgZ3JlZXRpbmdzLiBpdGVyPTE0OTg="}]}
INFO: [up] PUSH_ACK received in 31 ms
INFO: Received pkt from mote: 65732042 (fcnt=29540)
JSON up: {"rxpk":[{"jver":1,"tmst":415766626,"chan":8,"rfch":0,"freq":915.000000,"mid":16,"stat":1,"modu":"LORA","datr":"SF7BW125","codr":"4/5","rssis":-16,"lsnr":11.0,"foff":-53,"rssi":-15,"size":29,"data":"IEIgc2VuZHMgZ3JlZXRpbmdzLiBpdGVyPTE1MDU="}]}
INFO: [up] PUSH_ACK received in 31 ms
INFO: Received pkt from mote: 65732041 (fcnt=29540)
JSON up: {"rxpk":[{"jver":1,"tmst":416744088,"chan":8,"rfch":0,"freq":915.000000,"mid":16,"stat":1,"modu":"LORA","datr":"SF7BW125","codr":"4/5","rssis":-16,"lsnr":10.2,"foff":-289,"rssi":-15,"size":29,"data":"IEEgc2VuZHMgZ3JlZXRpbmdzLiBpdGVyPTE0OTk="}]}
INFO: [up] PUSH_ACK received in 31 ms
INFO: [down] PULL_ACK received in 31 ms
tbeam /usr/local/src/sx1302_hal/util_net_downlink # tail uplinks_20260218_155659.csv
445782393,,8,0,915.000000,16,1,LORA,7,125,4/5,-15.0,-16.0,10.8,29,20422073656e6473206772656574696e67732e20697465723d31353230
446764659,,8,0,915.000000,16,1,LORA,7,125,4/5,-15.0,-16.0,10.5,29,20412073656e6473206772656574696e67732e20697465723d31353134
==== With all 5 units ======
Amy:
set the tab:
echo -ne "\033]30;Amy\007"
pio run -e amy -t upload --upload-port /dev/ttyACM0
date; screen /dev/ttyACM0 115200
Bob:
echo -ne "\033]30;Bob\007"
pio run -e bob -t upload --upload-port /dev/ttyACM1
date; screen /dev/ttyACM1 115200
Cy:
source ~/rnsenv/bin/activate
echo -ne "\033]30;Cy\007"
pio run -e cy -t upload --upload-port /dev/ttyACM2
date; screen /dev/ttyACM2 115200
Dan:
source ~/rnsenv/bin/activate
echo -ne "\033]30;Dan\007"
pio run -e cy -t upload --upload-port /dev/ttyACM3
date; screen /dev/ttyACM3 115200
Ed:
source ~/rnsenv/bin/activate
echo -ne "\033]30;Ed\007"
pio run -e cy -t upload --upload-port /dev/ttyACM4
date; screen /dev/ttyACM4 115200
Logging:
cd /usr/local/src/sx1302_hal/util_net_downlink
./net_downlink -P 1730 -l uplinks_$(date +%Y%m%d_%H%M%S).csv
ls -lath /usr/local/src/sx1302_hal/util_net_downlink |head -n 3

View file

@ -0,0 +1,190 @@
John's interaction with the events below:
14149 Pressed down
[ 17543] let up
[ 30325] Pressed down
35143 let up
39993 killed.
Compiled and uploaded: Wed Apr 1 15:07:20 PDT 2026
[ 12793][E][sd_diskio.cpp:199] sdCommand(): Card Failed! cmd: 0x29
[ 12799][E][sd_diskio.cpp:806] sdcard_mount(): f_mount failed: (3) The physical drive cannot work
sample=3 state=BEGIN_FAIL rail=ON vbus=5.18 batt=0.00 pins=1/0/1/0 probeH(ff=8 z=0 o=0 FF FF FF FF) probeF(ff=8 z=0 o=0 FF FF FF FF) mount=none@0 type=NONE size=0MB root=FAIL
[ 13265][E][sd_diskio.cpp:199] sdCommand(): Card Failed! cmd: 0x37
[ 13271][E][sd_diskio.cpp:199] sdCommand(): Card Failed! cmd: 0x29
[ 13277][E][sd_diskio.cpp:806] sdcard_mount(): f_mount failed: (3) The physical drive cannot work
[ 13701][E][sd_diskio.cpp:199] sdCommand(): Card Failed! cmd: 0x37
[ 13707][E][sd_diskio.cpp:199] sdCommand(): Card Failed! cmd: 0x29
[ 13713][E][sd_diskio.cpp:806] sdcard_mount(): f_mount failed: (3) The physical drive cannot work
[ 14137][E][sd_diskio.cpp:199] sdCommand(): Card Failed! cmd: 0x37
[ 14143][E][sd_diskio.cpp:199] sdCommand(): Card Failed! cmd: 0x29
[ 14149][E][sd_diskio.cpp:806] sdcard_mount(): f_mount failed: (3) The physical drive cannot work
sample=4 state=MOUNT_OK rail=ON vbus=5.19 batt=0.00 pins=1/0/1/0 probeH(ff=8 z=0 o=0 FF FF FF FF) probeF(ff=8 z=0 o=0 FF FF FF FF) mount=HSPI@10000000 type=SDHC size=14910MB root=OK
sample=5 state=MOUNT_OK rail=ON vbus=5.18 batt=0.00 pins=1/0/1/0 probeH(ff=8 z=0 o=0 FF FF FF FF) probeF(ff=8 z=0 o=0 FF FF FF FF) mount=HSPI@400000 type=SDHC size=14910MB root=OK
sample=6 state=MOUNT_OK rail=ON vbus=5.18 batt=0.00 pins=1/0/1/0 probeH(ff=8 z=0 o=0 FF FF FF FF) probeF(ff=8 z=0 o=0 FF FF FF FF) mount=HSPI@400000 type=SDHC size=14910MB root=OK
sample=7 state=MOUNT_OK rail=ON vbus=5.18 batt=0.00 pins=1/0/1/0 probeH(ff=8 z=0 o=0 FF FF FF FF) probeF(ff=8 z=0 o=0 FF FF FF FF) mount=HSPI@400000 type=SDHC size=14910MB root=OK
sample=8 state=MOUNT_OK rail=ON vbus=5.18 batt=0.00 pins=1/0/1/0 probeH(ff=8 z=0 o=0 FF FF FF FF) probeF(ff=8 z=0 o=0 FF FF FF FF) mount=HSPI@400000 type=SDHC size=14910MB root=OK
sample=9 state=MOUNT_OK rail=ON vbus=5.18 batt=0.00 pins=1/0/1/0 probeH(ff=8 z=0 o=0 FF FF FF FF) probeF(ff=8 z=0 o=0 FF FF FF FF) mount=HSPI@400000 type=SDHC size=14910MB root=OK
sample=10 state=MOUNT_OK rail=ON vbus=5.18 batt=0.00 pins=1/0/1/0 probeH(ff=8 z=0 o=0 FF FF FF FF) probeF(ff=8 z=0 o=0 FF FF FF FF) mount=HSPI@400000 type=SDHC size=14910MB root=OK
sample=11 state=MOUNT_OK rail=ON vbus=5.18 batt=0.00 pins=1/0/1/0 probeH(ff=8 z=0 o=0 FF FF FF FF) probeF(ff=8 z=0 o=0 FF FF FF FF) mount=HSPI@400000 type=SDHC size=14910MB root=OK
sample=12 state=MOUNT_OK rail=ON vbus=5.18 batt=0.00 pins=1/0/1/0 probeH(ff=8 z=0 o=0 FF FF FF FF) probeF(ff=8 z=0 o=0 FF FF FF FF) mount=HSPI@400000 type=SDHC size=14910MB root=OK
sample=13 state=MOUNT_OK rail=ON vbus=5.18 batt=0.00 pins=1/0/1/0 probeH(ff=8 z=0 o=0 FF FF FF FF) probeF(ff=8 z=0 o=0 FF FF FF FF) mount=HSPI@400000 type=SDHC size=14910MB root=OK
sample=14 state=MOUNT_OK rail=ON vbus=5.18 batt=0.00 pins=1/0/1/0 probeH(ff=8 z=0 o=0 FF FF FF FF) probeF(ff=8 z=0 o=0 FF FF FF FF) mount=HSPI@400000 type=SDHC size=14910MB root=OK
sample=15 state=MOUNT_OK rail=ON vbus=5.18 batt=0.00 pins=1/0/1/0 probeH(ff=8 z=0 o=0 FF FF FF FF) probeF(ff=8 z=0 o=0 FF FF FF FF) mount=HSPI@400000 type=SDHC size=14910MB root=OK
sample=16 state=MOUNT_OK rail=ON vbus=5.18 batt=0.00 pins=1/0/1/0 probeH(ff=8 z=0 o=0 FF FF FF FF) probeF(ff=8 z=0 o=0 FF FF FF FF) mount=HSPI@400000 type=SDHC size=14910MB root=OK
[ 17543][E][sd_diskio.cpp:199] sdCommand(): Card Failed! cmd: 0xsample=17 state=MOUNT_OK rail=ON vbus=5.18 batt=0.00 pins=1/0/1/0 probeH(ff=8 z=0 o=0 FF FF FF FF) probeF(ff=8 z=0 o=0 FF FF FF FF) mount=HSPI@400000 type=SDHC size=14910MB root=OK
37
[ 17549][E][sd_diskio.cpp:199] sdCommand(): Card Failed! cmd: 0x29
[ 17555][E][sd_diskio.cpp:806] sdcard_mount(): f_mount failed: (3) The physical drive cannot work
[ 17979][E][sd_diskio.cpp:199] sdCommand(): Card Failed! cmd: 0x37
[ 17985][E][sd_diskio.cpp:199] sdCommand(): Card Failed! cmd: 0x29
[ 17991][E][sd_diskio.cpp:806] sdcard_mount(): f_mount failed: (3) The physical drive cannot work
[ 18415][E][sd_diskio.cpp:199] sdCommand(): Card Failed! cmd: 0x37
[ 18421][E][sd_diskio.cpp:199] sdCommand(): Card Failed! cmd: 0x29
[ 18427][E][sd_diskio.cpp:806] sdcard_mount(): f_mount failed: (3) The physical drive cannot work
[ 18851][E][sd_diskio.cpp:199] sdCommand(): Card Failed! cmd: 0x37
[ 18857][E][sd_diskio.cpp:199] sdCommand(): Card Failed! cmd: 0x29
[ 18863][E][sd_diskio.cpp:806] sdcard_mount(): f_mount failed: (3) The physical drive cannot work
[ 19287][E][sd_diskio.cpp:199] sdCommand(): Card Failed! cmd: 0x37
[ 19293][E][sd_diskio.cpp:199] sdCommand(): Card Failed! cmd: 0x29
[ 19299][E][sd_diskio.cpp:806] sdcard_mount(): f_mount failed: (3) The physical drive cannot work
[ 19723][E][sd_diskio.cpp:199] sdCommand(): Card Failed! cmd: 0x37
[ 19729][E][sd_diskio.cpp:199] sdCommand(): Card Failed! cmd: 0x29
[ 19735][E][sd_diskio.cpp:806] sdcard_mount(): f_mount failed: (3) The physical drive cannot work
[ 20159][E][sd_diskio.cpp:199] sdCommand(): Card Failed! cmd: 0x37
[ 20165][E][sd_diskio.cpp:199] sdCommand(): Card Failed! cmd: 0x29
[ 20171][E][sd_diskio.cpp:806] sdcard_mount(): f_mount failed: (3) The physical drive cannot work
[ 20595][E][sd_diskio.cpp:199] sdCommand(): Card Failed! cmd: 0x37
[ 20601][E][sd_diskio.cpp:199] sdCommand(): Card Failed! cmd: 0x29
[ 20607][E][sd_diskio.cpp:806] sdcard_mount(): f_mount failed: (3) The physical drive cannot work
sample=18 state=BEGIN_FAIL rail=ON vbus=5.18 batt=0.00 pins=1/0/1/0 probeH(ff=8 z=0 o=0 FF FF FF FF) probeF(ff=8 z=0 o=0 FF FF FF FF) mount=none@0 type=NONE size=0MB root=FAIL
[ 21073][E][sd_diskio.cpp:199] sdCommand(): Card Failed! cmd: 0x37
[ 21079][E][sd_diskio.cpp:199] sdCommand(): Card Failed! cmd: 0x29
[ 21085][E][sd_diskio.cpp:806] sdcard_mount(): f_mount failed: (3) The physical drive cannot work
[ 21509][E][sd_diskio.cpp:199] sdCommand(): Card Failed! cmd: 0x37
[ 21515][E][sd_diskio.cpp:199] sdCommand(): Card Failed! cmd: 0x29
[ 21521][E][sd_diskio.cpp:806] sdcard_mount(): f_mount failed: (3) The physical drive cannot work
[ 21945][E][sd_diskio.cpp:199] sdCommand(): Card Failed! cmd: 0x37
[ 21951][E][sd_diskio.cpp:199] sdCommand(): Card Failed! cmd: 0x29
[ 21957][E][sd_diskio.cpp:806] sdcard_mount(): f_mount failed: (3) The physical drive cannot work
[ 22381][E][sd_diskio.cpp:199] sdCommand(): Card Failed! cmd: 0x37
[ 22387][E][sd_diskio.cpp:199] sdCommand(): Card Failed! cmd: 0x29
[ 22393][E][sd_diskio.cpp:806] sdcard_mount(): f_mount failed: (3) The physical drive cannot work
[ 22817][E][sd_diskio.cpp:199] sdCommand(): Card Failed! cmd: 0x37
[ 22823][E][sd_diskio.cpp:199] sdCommand(): Card Failed! cmd: 0x29
[ 22829][E][sd_diskio.cpp:806] sdcard_mount(): f_mount failed: (3) The physical drive cannot work
[ 23253][E][sd_diskio.cpp:199] sdCommand(): Card Failed! cmd: 0x37
[ 23259][E][sd_diskio.cpp:199] sdCommand(): Card Failed! cmd: 0x29
[ 23265][E][sd_diskio.cpp:806] sdcard_mount(): f_mount failed: (3) The physical drive cannot work
[ 23689][E][sd_diskio.cpp:199] sdCommand(): Card Failed! cmd: 0x37
[ 23695][E][sd_diskio.cpp:199] sdCommand(): Card Failed! cmd: 0x29
[ 23701][E][sd_diskio.cpp:806] sdcard_mount(): f_mount failed: (3) The physical drive cannot work
[ 24125][E][sd_diskio.cpp:199] sdCommand(): Card Failed! cmd: 0x37
[ 24131][E][sd_diskio.cpp:199] sdCommand(): Card Failed! cmd: 0x29
[ 24137][E][sd_diskio.cpp:806] sdcard_mount(): f_mount failed: (3) The physical drive cannot work
[ 24603][E][sd_diskio.cpp:199] sdCommand(): Card Failed! cmd: 0xsample=19 state=BEGIN_FAIL rail=ON vbus=5.19 batt=0.00 pins=1/0/1/0 probeH(ff=8 z=0 o=0 FF FF FF FF) probeF(ff=8 z=0 o=0 FF FF FF FF) mount=none@0 type=NONE size=0MB root=FAIL
37
[ 24609][E][sd_diskio.cpp:199] sdCommand(): Card Failed! cmd: 0x29
[ 24615][E][sd_diskio.cpp:806] sdcard_mount(): f_mount failed: (3) The physical drive cannot work
[ 25039][E][sd_diskio.cpp:199] sdCommand(): Card Failed! cmd: 0x37
[ 25045][E][sd_diskio.cpp:199] sdCommand(): Card Failed! cmd: 0x29
[ 25051][E][sd_diskio.cpp:806] sdcard_mount(): f_mount failed: (3) The physical drive cannot work
[ 25475][E][sd_diskio.cpp:199] sdCommand(): Card Failed! cmd: 0x37
[ 25481][E][sd_diskio.cpp:199] sdCommand(): Card Failed! cmd: 0x29
[ 25487][E][sd_diskio.cpp:806] sdcard_mount(): f_mount failed: (3) The physical drive cannot work
[ 25911][E][sd_diskio.cpp:199] sdCommand(): Card Failed! cmd: 0x37
[ 25917][E][sd_diskio.cpp:199] sdCommand(): Card Failed! cmd: 0x29
[ 25923][E][sd_diskio.cpp:806] sdcard_mount(): f_mount failed: (3) The physical drive cannot work
[ 26347][E][sd_diskio.cpp:199] sdCommand(): Card Failed! cmd: 0x37
[ 26353][E][sd_diskio.cpp:199] sdCommand(): Card Failed! cmd: 0x29
[ 26359][E][sd_diskio.cpp:806] sdcard_mount(): f_mount failed: (3) The physical drive cannot work
[ 26783][E][sd_diskio.cpp:199] sdCommand(): Card Failed! cmd: 0x37
[ 26789][E][sd_diskio.cpp:199] sdCommand(): Card Failed! cmd: 0x29
[ 26795][E][sd_diskio.cpp:806] sdcard_mount(): f_mount failed: (3) The physical drive cannot work
[ 27219][E][sd_diskio.cpp:199] sdCommand(): Card Failed! cmd: 0x37
[ 27225][E][sd_diskio.cpp:199] sdCommand(): Card Failed! cmd: 0x29
[ 27231][E][sd_diskio.cpp:806] sdcard_mount(): f_mount failed: (3) The physical drive cannot work
[ 27655][E][sd_diskio.cpp:199] sdCommand(): Card Failed! cmd: 0x37
[ 27661][E][sd_diskio.cpp:199] sdCommand(): Card Failed! cmd: 0x29
[ 27667][E][sd_diskio.cpp:806] sdcard_mount(): f_mount failed: (3) The physical drive cannot work
sample=20 state=BEGIN_FAIL rail=ON vbus=5.19 batt=0.00 pins=1/0/1/0 probeH(ff=8 z=0 o=0 FF FF FF FF) probeF(ff=8 z=0 o=0 FF FF FF FF) mount=none@0 type=NONE size=0MB root=FAIL
[ 28133][E][sd_diskio.cpp:199] sdCommand(): Card Failed! cmd: 0x37
[ 28139][E][sd_diskio.cpp:199] sdCommand(): Card Failed! cmd: 0x29
[ 28145][E][sd_diskio.cpp:806] sdcard_mount(): f_mount failed: (3) The physical drive cannot work
[ 28569][E][sd_diskio.cpp:199] sdCommand(): Card Failed! cmd: 0x37
[ 28575][E][sd_diskio.cpp:199] sdCommand(): Card Failed! cmd: 0x29
[ 28581][E][sd_diskio.cpp:806] sdcard_mount(): f_mount failed: (3) The physical drive cannot work
[ 29005][E][sd_diskio.cpp:199] sdCommand(): Card Failed! cmd: 0x37
[ 29011][E][sd_diskio.cpp:199] sdCommand(): Card Failed! cmd: 0x29
[ 29017][E][sd_diskio.cpp:806] sdcard_mount(): f_mount failed: (3) The physical drive cannot work
[ 29441][E][sd_diskio.cpp:199] sdCommand(): Card Failed! cmd: 0x37
[ 29447][E][sd_diskio.cpp:199] sdCommand(): Card Failed! cmd: 0x29
[ 29453][E][sd_diskio.cpp:806] sdcard_mount(): f_mount failed: (3) The physical drive cannot work
[ 29877][E][sd_diskio.cpp:199] sdCommand(): Card Failed! cmd: 0x37
[ 29883][E][sd_diskio.cpp:199] sdCommand(): Card Failed! cmd: 0x29
[ 29889][E][sd_diskio.cpp:806] sdcard_mount(): f_mount failed: (3) The physical drive cannot work
[ 30313][E][sd_diskio.cpp:199] sdCommand(): Card Failed! cmd: 0x37
[ 30319][E][sd_diskio.cpp:199] sdCommand(): Card Failed! cmd: 0x29
[ 30325][E][sd_diskio.cpp:806] sdcard_mount(): f_mount failed: (3) The physical drive cannot work
sample=21 state=MOUNT_OK rail=ON vbus=5.19 batt=0.00 pins=1/0/1/0 probeH(ff=8 z=0 o=0 FF FF FF FF) probeF(ff=8 z=0 o=0 FF FF FF FF) mount=FSPI@4000000 type=SDHC size=14910MB root=OK
sample=22 state=MOUNT_OK rail=ON vbus=5.18 batt=0.00 pins=1/0/1/0 probeH(ff=8 z=0 o=0 FF FF FF FF) probeF(ff=8 z=0 o=0 FF FF FF FF) mount=HSPI@400000 type=SDHC size=14910MB root=OK
sample=23 state=MOUNT_OK rail=ON vbus=5.18 batt=0.00 pins=1/0/1/0 probeH(ff=8 z=0 o=0 FF FF FF FF) probeF(ff=8 z=0 o=0 FF FF FF FF) mount=HSPI@400000 type=SDHC size=14910MB root=OK
sample=24 state=MOUNT_OK rail=ON vbus=5.18 batt=0.00 pins=1/0/1/0 probeH(ff=8 z=0 o=0 FF FF FF FF) probeF(ff=8 z=0 o=0 FF FF FF FF) mount=HSPI@400000 type=SDHC size=14910MB root=OK
sample=25 state=MOUNT_OK rail=ON vbus=5.18 batt=0.00 pins=1/0/1/0 probeH(ff=8 z=0 o=0 FF FF FF FF) probeF(ff=8 z=0 o=0 FF FF FF FF) mount=HSPI@400000 type=SDHC size=14910MB root=OK
sample=26 state=MOUNT_OK rail=ON vbus=5.18 batt=0.00 pins=1/0/1/0 probeH(ff=8 z=0 o=0 FF FF FF FF) probeF(ff=8 z=0 o=0 FF FF FF FF) mount=HSPI@400000 type=SDHC size=14910MB root=OK
sample=27 state=MOUNT_OK rail=ON vbus=5.18 batt=0.00 pins=1/0/1/0 probeH(ff=8 z=0 o=0 FF FF FF FF) probeF(ff=8 z=0 o=0 FF FF FF FF) mount=HSPI@400000 type=SDHC size=14910MB root=OK
sample=28 state=MOUNT_OK rail=ON vbus=5.18 batt=0.00 pins=1/0/1/0 probeH(ff=8 z=0 o=0 FF FF FF FF) probeF(ff=8 z=0 o=0 FF FF FF FF) mount=HSPI@400000 type=SDHC size=14910MB root=OK
sample=29 state=MOUNT_OK rail=ON vbus=5.18 batt=0.00 pins=1/0/1/0 probeH(ff=8 z=0 o=0 FF FF FF FF) probeF(ff=8 z=0 o=0 FF FF FF FF) mount=HSPI@400000 type=SDHC size=14910MB root=OK
sample=30 state=MOUNT_OK rail=ON vbus=5.18 batt=0.00 pins=1/0/1/0 probeH(ff=8 z=0 o=0 FF FF FF FF) probeF(ff=8 z=0 o=0 FF FF FF FF) mount=HSPI@400000 type=SDHC size=14910MB root=OK
sample=31 state=MOUNT_OK rail=ON vbus=5.18 batt=0.00 pins=1/0/1/0 probeH(ff=8 z=0 o=0 FF FF FF FF) probeF(ff=8 z=0 o=0 FF FF FF FF) mount=HSPI@400000 type=SDHC size=14910MB root=OK
sample=32 state=MOUNT_OK rail=ON vbus=5.18 batt=0.00 pins=1/0/1/0 probeH(ff=8 z=0 o=0 FF FF FF FF) probeF(ff=8 z=0 o=0 FF FF FF FF) mount=HSPI@400000 type=SDHC size=14910MB root=OK
sample=33 state=MOUNT_OK rail=ON vbus=5.18 batt=0.00 pins=1/0/1/0 probeH(ff=8 z=0 o=0 FF FF FF FF) probeF(ff=8 z=0 o=0 FF FF FF FF) mount=HSPI@400000 type=SDHC size=14910MB root=OK
sample=34 state=MOUNT_OK rail=ON vbus=5.18 batt=0.00 pins=1/0/1/0 probeH(ff=8 z=0 o=0 FF FF FF FF) probeF(ff=8 z=0 o=0 FF FF FF FF) mount=HSPI@400000 type=SDHC size=14910MB root=OK
sample=35 state=MOUNT_OK rail=ON vbus=5.18 batt=0.00 pins=1/0/1/0 probeH(ff=8 z=0 o=0 FF FF FF FF) probeF(ff=8 z=0 o=0 FF FF FF FF) mount=HSPI@400000 type=SDHC size=14910MB root=OK
sample=36 state=MOUNT_OK rail=ON vbus=5.18 batt=0.00 pins=1/0/1/0 probeH(ff=8 z=0 o=0 FF FF FF FF) probeF(ff=8 z=0 o=0 FF FF FF FF) mount=HSPI@400000 type=SDHC size=14910MB root=OK
sample=37 state=MOUNT_OK rail=ON vbus=5.18 batt=0.00 pins=1/0/1/0 probeH(ff=8 z=0 o=0 FF FF FF FF) probeF(ff=8 z=0 o=0 FF FF FF FF) mount=HSPI@400000 type=SDHC size=14910MB root=OK
sample=38 state=MOUNT_OK rail=ON vbus=5.18 batt=0.00 pins=1/0/1/0 probeH(ff=8 z=0 o=0 FF FF FF FF) probeF(ff=8 z=0 o=0 FF FF FF FF) mount=HSPI@400000 type=SDHC size=14910MB root=OK
sample=39 state=MOUNT_OK rail=ON vbus=5.18 batt=0.00 pins=1/0/1/0 probeH(ff=8 z=0 o=0 FF FF FF FF) probeF(ff=8 z=0 o=0 FF FF FF FF) mount=HSPI@400000 type=SDHC size=14910MB root=OK
sample=40 state=MOUNT_OK rail=ON vbus=5.18 batt=0.00 pins=1/0/1/0 probeH(ff=8 z=0 o=0 FF FF FF FF) probeF(ff=8 z=0 o=0 FF FF FF FF) mount=HSPI@400000 type=SDHC size=14910MB root=OK
[ 35143][E][sd_diskio.cpp:199] sdCommand(): Card Failed! cmd: 0xsample=41 state=MOUNT_OK rail=ON vbus=5.18 batt=0.00 pins=1/0/1/0 probeH(ff=8 z=0 o=0 FF FF FF FF) probeF(ff=8 z=0 o=0 FF FF FF FF) mount=HSPI@400000 type=SDHC size=14910MB root=OK
37
[ 35149][E][sd_diskio.cpp:199] sdCommand(): Card Failed! cmd: 0x29
[ 35155][E][sd_diskio.cpp:806] sdcard_mount(): f_mount failed: (3) The physical drive cannot work
[ 35579][E][sd_diskio.cpp:199] sdCommand(): Card Failed! cmd: 0x37
[ 35585][E][sd_diskio.cpp:199] sdCommand(): Card Failed! cmd: 0x29
[ 35591][E][sd_diskio.cpp:806] sdcard_mount(): f_mount failed: (3) The physical drive cannot work
[ 36015][E][sd_diskio.cpp:199] sdCommand(): Card Failed! cmd: 0x37
[ 36021][E][sd_diskio.cpp:199] sdCommand(): Card Failed! cmd: 0x29
[ 36027][E][sd_diskio.cpp:806] sdcard_mount(): f_mount failed: (3) The physical drive cannot work
[ 36451][E][sd_diskio.cpp:199] sdCommand(): Card Failed! cmd: 0x37
[ 36457][E][sd_diskio.cpp:199] sdCommand(): Card Failed! cmd: 0x29
[ 36463][E][sd_diskio.cpp:806] sdcard_mount(): f_mount failed: (3) The physical drive cannot work
[ 36887][E][sd_diskio.cpp:199] sdCommand(): Card Failed! cmd: 0x37
[ 36893][E][sd_diskio.cpp:199] sdCommand(): Card Failed! cmd: 0x29
[ 36899][E][sd_diskio.cpp:806] sdcard_mount(): f_mount failed: (3) The physical drive cannot work
[ 37323][E][sd_diskio.cpp:199] sdCommand(): Card Failed! cmd: 0x37
[ 37329][E][sd_diskio.cpp:199] sdCommand(): Card Failed! cmd: 0x29
[ 37335][E][sd_diskio.cpp:806] sdcard_mount(): f_mount failed: (3) The physical drive cannot work
[ 37759][E][sd_diskio.cpp:199] sdCommand(): Card Failed! cmd: 0x37
[ 37765][E][sd_diskio.cpp:199] sdCommand(): Card Failed! cmd: 0x29
[ 37771][E][sd_diskio.cpp:806] sdcard_mount(): f_mount failed: (3) The physical drive cannot work
[ 38195][E][sd_diskio.cpp:199] sdCommand(): Card Failed! cmd: 0x37
[ 38201][E][sd_diskio.cpp:199] sdCommand(): Card Failed! cmd: 0x29
[ 38207][E][sd_diskio.cpp:806] sdcard_mount(): f_mount failed: (3) The physical drive cannot work
sample=42 state=BEGIN_FAIL rail=ON vbus=5.18 batt=0.00 pins=1/0/1/0 probeH(ff=8 z=0 o=0 FF FF FF FF) probeF(ff=8 z=0 o=0 FF FF FF FF) mount=none@0 type=NONE size=0MB root=FAIL
[ 38673][E][sd_diskio.cpp:199] sdCommand(): Card Failed! cmd: 0x37
[ 38679][E][sd_diskio.cpp:199] sdCommand(): Card Failed! cmd: 0x29
[ 38685][E][sd_diskio.cpp:806] sdcard_mount(): f_mount failed: (3) The physical drive cannot work
[ 39109][E][sd_diskio.cpp:199] sdCommand(): Card Failed! cmd: 0x37
[ 39115][E][sd_diskio.cpp:199] sdCommand(): Card Failed! cmd: 0x29
[ 39121][E][sd_diskio.cpp:806] sdcard_mount(): f_mount failed: (3) The physical drive cannot work
[ 39545][E][sd_diskio.cpp:199] sdCommand(): Card Failed! cmd: 0x37
[ 39551][E][sd_diskio.cpp:199] sdCommand(): Card Failed! cmd: 0x29
[ 39557][E][sd_diskio.cpp:806] sdcard_mount(): f_mount failed: (3) The physical drive cannot work
[ 39981][E][sd_diskio.cpp:199] sdCommand(): Card Failed! cmd: 0x37
[ 39987][E][sd_diskio.cpp:199] sdCommand(): Card Failed! cmd: 0x29
[ 39993][E][sd_diskio.cpp:806] sdcard_mount(): f_mount failed: (3) The physical drive cannot work
(rnsenv) jlpoole@jp /usr/local/src/microreticulum/microReticulumTbeam $ date
Wed Apr 1 15:08:35 PDT 2026
(rnsenv) jlpoole@jp /usr/local/src/microreticulum/microReticulumTbeam $

View file

@ -0,0 +1,44 @@
April 1, 2026 ~ 15:34
From AMY (broken SD Card):
[ 15549][E][sd_diskio.cpp:806] sdcard_mount(): f_mount failed: (13) There is no valid FAT volume
STARTUP REPLAY
uptime_ms=15438
BOARD IDENTITY
chip_model=ESP32-S3
chip_revision=0
chip_cores=2
sdk_version=v4.4.7-dirty
cpu_mhz=240
flash_size=8388608
flash_speed=80000000
flash_mode=QIO
efuse_mac=68BF5B43CA48
chip_id=5BBF68
reset_reason=UNKNOWN
arduino_board=Espressif ESP32-S3-DevKitC-1-N8 (8 MB QD, No PSRAM)
sample=34 state=BEGIN_FAIL rail=ON vbus=5.18 batt=0.00 pins=1/0/1/0 probeH(ff=8 z=0 o=0 FF FF FF FF) probeF(ff=8 z=0 o=0 FF FF FF FF) mount=none@0 type=NONE size=0MB root=FAIL
[ 15618][E][sd_diskio.cpp:806] sdcard_mount(): f_mount failed: (13) There is no valid FAT volume
from BOB:
sample=30 state=MOUNT_OK rail=ON vbus=5.03 batt=3.86 pins=1/0/1/0 probeH(ff=8 z=0 o=0 FF FF FF FF) probeF(ff=8 z=0 o=0 FF FF FF FF) mount=HSPI@400000 type=SDHC size=14910MB root=OK
STARTUP REPLAY
uptime_ms=13167
BOARD IDENTITY
chip_model=ESP32-S3
chip_revision=0
chip_cores=2
sdk_version=v4.4.7-dirty
cpu_mhz=240
flash_size=8388608
flash_speed=80000000
flash_mode=QIO
efuse_mac=DC935A43CA48
chip_id=5A93DC
reset_reason=UNKNOWN
arduino_board=Espressif ESP32-S3-DevKitC-1-N8 (8 MB QD, No PSRAM)
sample=31 state=MOUNT_OK rail=ON vbus=5.03 batt=3.86 pins=1/0/1/0 probeH(ff=8 z=0 o=0 FF FF FF FF) probeF(ff=8 z=0 o=0 FF FF FF FF) mount=HSPI@400000 type=SDHC size=14910MB root=OK

View file

@ -0,0 +1,31 @@
; 20260401 Codex
; constantTFCard hardware debug monitor
[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\"

View file

@ -0,0 +1,443 @@
// 20260401 Codex
#include <Arduino.h>
#include <esp_system.h>
#include <SPI.h>
#include <SD.h>
#include <Wire.h>
#include <U8g2lib.h>
#include <driver/gpio.h>
#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 kStartupQuietMs = 5000;
static const uint32_t kStartupReplayWindowMs = 20000;
static const uint32_t kStartupReplayPeriodMs = 2000;
static const uint32_t kFreqs[] = {
400000,
1000000,
4000000,
10000000
};
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 firstBytes[8] = {0};
};
enum class DebugState : uint8_t {
PMU_FAIL = 0,
RAIL_OFF,
BUS_FLOAT,
BUS_LOW,
BUS_CHATTER,
SD_BEGIN_FAIL,
CARD_NONE,
FS_FAIL,
MOUNT_OK
};
struct DebugSnapshot {
DebugState state = DebugState::PMU_FAIL;
bool pmuOk = false;
bool railOn = false;
float vbusV = -1.0f;
float battV = -1.0f;
PinSnapshot pins{};
ProbeSummary probeH{};
ProbeSummary probeF{};
const char* mountBus = "none";
uint32_t mountHz = 0;
uint8_t cardType = CARD_NONE;
uint64_t cardSizeMB = 0;
bool rootOk = false;
};
static XPowersLibInterface* g_pmu = nullptr;
static U8G2_SH1106_128X64_NONAME_F_HW_I2C g_oled(U8G2_R0, U8X8_PIN_NONE);
static SPIClass g_spiH(HSPI);
static SPIClass g_spiF(FSPI);
static uint32_t g_sampleCount = 0;
static uint32_t g_bootMs = 0;
static uint32_t g_lastStartupReplayMs = 0;
static const char* resetReasonToString(esp_reset_reason_t reason) {
switch (reason) {
case ESP_RST_UNKNOWN: return "UNKNOWN";
case ESP_RST_POWERON: return "POWERON";
case ESP_RST_EXT: return "EXT";
case ESP_RST_SW: return "SW";
case ESP_RST_PANIC: return "PANIC";
case ESP_RST_INT_WDT: return "INT_WDT";
case ESP_RST_TASK_WDT: return "TASK_WDT";
case ESP_RST_WDT: return "WDT";
case ESP_RST_DEEPSLEEP: return "DEEPSLEEP";
case ESP_RST_BROWNOUT: return "BROWNOUT";
case ESP_RST_SDIO: return "SDIO";
default: return "OTHER";
}
}
static const char* flashModeToString(FlashMode_t mode) {
switch (mode) {
case FM_QIO: return "QIO";
case FM_QOUT: return "QOUT";
case FM_DIO: return "DIO";
case FM_DOUT: return "DOUT";
case FM_FAST_READ: return "FAST";
case FM_SLOW_READ: return "SLOW";
default: return "UNKNOWN";
}
}
static void printBoardIdentity() {
uint64_t mac = ESP.getEfuseMac();
uint32_t chipId = 0;
for (int i = 0; i < 17; i += 8) {
chipId |= ((mac >> (40 - i)) & 0xFF) << i;
}
Serial.println("BOARD IDENTITY");
Serial.printf("chip_model=%s\r\n", ESP.getChipModel());
Serial.printf("chip_revision=%u\r\n", (unsigned)ESP.getChipRevision());
Serial.printf("chip_cores=%u\r\n", (unsigned)ESP.getChipCores());
Serial.printf("sdk_version=%s\r\n", ESP.getSdkVersion());
Serial.printf("cpu_mhz=%u\r\n", (unsigned)ESP.getCpuFreqMHz());
Serial.printf("flash_size=%u\r\n", (unsigned)ESP.getFlashChipSize());
Serial.printf("flash_speed=%u\r\n", (unsigned)ESP.getFlashChipSpeed());
Serial.printf("flash_mode=%s\r\n", flashModeToString(ESP.getFlashChipMode()));
Serial.printf("efuse_mac=%012llX\r\n", mac);
Serial.printf("chip_id=%06lX\r\n", (unsigned long)chipId);
Serial.printf("reset_reason=%s\r\n", resetReasonToString(esp_reset_reason()));
Serial.printf("arduino_board=%s\r\n", ARDUINO_BOARD);
}
static void printStartupBanner() {
Serial.println();
Serial.println("STARTUP REPLAY");
Serial.printf("uptime_ms=%lu\r\n", (unsigned long)(millis() - g_bootMs));
printBoardIdentity();
}
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 readPmu(DebugSnapshot& snap) {
snap.pmuOk = (g_pmu != nullptr);
if (!g_pmu) {
return;
}
snap.railOn = g_pmu->isPowerChannelEnable(XPOWERS_BLDO1);
snap.vbusV = g_pmu->getVbusVoltage() / 1000.0f;
snap.battV = g_pmu->getBattVoltage() / 1000.0f;
}
static ProbeSummary runIdleProbe(SPIClass& bus) {
ProbeSummary out;
SD.end();
bus.end();
delay(2);
forceSpiDeselected();
bus.begin(
tbeam_supreme::sdSck(),
tbeam_supreme::sdMiso(),
tbeam_supreme::sdMosi(),
tbeam_supreme::sdCs()
);
digitalWrite(tbeam_supreme::sdCs(), HIGH);
delay(1);
for (int i = 0; i < 8; ++i) {
uint8_t b = bus.transfer(0xFF);
out.firstBytes[i] = b;
if (b == 0xFF) out.ffCount++;
else if (b == 0x00) out.zeroCount++;
else out.otherCount++;
}
return out;
}
static const char* cardTypeToString(uint8_t type) {
switch (type) {
case CARD_MMC: return "MMC";
case CARD_SD: return "SDSC";
case CARD_SDHC: return "SDHC";
default: return "NONE";
}
}
static bool tryMount(SPIClass& bus,
const char* busName,
uint32_t hz,
DebugSnapshot& snap) {
SD.end();
bus.end();
delay(2);
forceSpiDeselected();
bus.begin(
tbeam_supreme::sdSck(),
tbeam_supreme::sdMiso(),
tbeam_supreme::sdMosi(),
tbeam_supreme::sdCs()
);
digitalWrite(tbeam_supreme::sdCs(), HIGH);
delay(1);
for (int i = 0; i < 10; ++i) {
bus.transfer(0xFF);
}
if (!SD.begin(tbeam_supreme::sdCs(), bus, hz)) {
snap.state = DebugState::SD_BEGIN_FAIL;
return false;
}
snap.cardType = SD.cardType();
snap.mountBus = busName;
snap.mountHz = hz;
if (snap.cardType == CARD_NONE) {
SD.end();
snap.state = DebugState::CARD_NONE;
return false;
}
snap.cardSizeMB = SD.cardSize() / (1024ULL * 1024ULL);
File root = SD.open("/", FILE_READ);
snap.rootOk = (bool)root;
if (root) {
root.close();
}
SD.end();
snap.state = snap.rootOk ? DebugState::MOUNT_OK : DebugState::FS_FAIL;
return snap.rootOk;
}
static DebugState classifyProbe(const ProbeSummary& probe) {
if (probe.ffCount == 8) return DebugState::BUS_FLOAT;
if (probe.zeroCount == 8) return DebugState::BUS_LOW;
return DebugState::BUS_CHATTER;
}
static DebugSnapshot captureSnapshot() {
DebugSnapshot snap;
readPmu(snap);
snap.pins = readPins();
if (!snap.pmuOk) {
snap.state = DebugState::PMU_FAIL;
return snap;
}
if (!snap.railOn) {
snap.state = DebugState::RAIL_OFF;
return snap;
}
snap.probeH = runIdleProbe(g_spiH);
snap.probeF = runIdleProbe(g_spiF);
snap.state = classifyProbe(snap.probeH);
for (size_t i = 0; i < (sizeof(kFreqs) / sizeof(kFreqs[0])); ++i) {
if (tryMount(g_spiH, "HSPI", kFreqs[i], snap)) {
return snap;
}
}
for (size_t i = 0; i < (sizeof(kFreqs) / sizeof(kFreqs[0])); ++i) {
if (tryMount(g_spiF, "FSPI", kFreqs[i], snap)) {
return snap;
}
}
return snap;
}
static const char* stateToString(DebugState state) {
switch (state) {
case DebugState::PMU_FAIL: return "PMU_FAIL";
case DebugState::RAIL_OFF: return "RAIL_OFF";
case DebugState::BUS_FLOAT: return "NO_RESP";
case DebugState::BUS_LOW: return "BUS_LOW";
case DebugState::BUS_CHATTER: return "BUS_CHAT";
case DebugState::SD_BEGIN_FAIL: return "BEGIN_FAIL";
case DebugState::CARD_NONE: return "CARD_NONE";
case DebugState::FS_FAIL: return "FS_FAIL";
case DebugState::MOUNT_OK: return "MOUNT_OK";
default: return "UNKNOWN";
}
}
static void printSnapshot(const DebugSnapshot& snap) {
Serial.printf(
"sample=%lu state=%s rail=%s vbus=%.2f batt=%.2f pins=%d/%d/%d/%d "
"probeH(ff=%u z=%u o=%u %02X %02X %02X %02X) "
"probeF(ff=%u z=%u o=%u %02X %02X %02X %02X) "
"mount=%s@%lu type=%s size=%lluMB root=%s\r\n",
(unsigned long)g_sampleCount,
stateToString(snap.state),
snap.railOn ? "ON" : "OFF",
snap.vbusV,
snap.battV,
snap.pins.cs,
snap.pins.sck,
snap.pins.miso,
snap.pins.mosi,
(unsigned)snap.probeH.ffCount,
(unsigned)snap.probeH.zeroCount,
(unsigned)snap.probeH.otherCount,
snap.probeH.firstBytes[0],
snap.probeH.firstBytes[1],
snap.probeH.firstBytes[2],
snap.probeH.firstBytes[3],
(unsigned)snap.probeF.ffCount,
(unsigned)snap.probeF.zeroCount,
(unsigned)snap.probeF.otherCount,
snap.probeF.firstBytes[0],
snap.probeF.firstBytes[1],
snap.probeF.firstBytes[2],
snap.probeF.firstBytes[3],
snap.mountBus,
(unsigned long)snap.mountHz,
cardTypeToString(snap.cardType),
snap.cardSizeMB,
snap.rootOk ? "OK" : "FAIL"
);
}
static void showSnapshot(const DebugSnapshot& snap) {
char line1[24];
char line2[24];
char line3[24];
char line4[24];
char line5[24];
snprintf(line1, sizeof(line1), "%s TF HWDBG", NODE_LABEL);
snprintf(line2, sizeof(line2), "STATE %s", stateToString(snap.state));
snprintf(line3, sizeof(line3), "H %u/%u/%u F %u/%u/%u",
(unsigned)snap.probeH.ffCount,
(unsigned)snap.probeH.zeroCount,
(unsigned)snap.probeH.otherCount,
(unsigned)snap.probeF.ffCount,
(unsigned)snap.probeF.zeroCount,
(unsigned)snap.probeF.otherCount);
snprintf(line4, sizeof(line4), "%s %luk %s",
snap.mountBus,
(unsigned long)(snap.mountHz / 1000UL),
cardTypeToString(snap.cardType));
snprintf(line5, sizeof(line5), "P %d%d%d%d R%s %lu",
snap.pins.cs,
snap.pins.sck,
snap.pins.miso,
snap.pins.mosi,
snap.railOn ? "1" : "0",
(unsigned long)g_sampleCount);
g_oled.clearBuffer();
g_oled.setFont(u8g2_font_5x8_tf);
g_oled.drawUTF8(0, 12, line1);
g_oled.drawUTF8(0, 24, line2);
g_oled.drawUTF8(0, 36, line3);
g_oled.drawUTF8(0, 48, line4);
g_oled.drawUTF8(0, 60, line5);
g_oled.sendBuffer();
}
void setup() {
Serial.begin(115200);
g_bootMs = millis();
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 hardware debug");
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 NO_RESP BUS_LOW BUS_CHAT BEGIN_FAIL CARD_NONE FS_FAIL MOUNT_OK");
Serial.printf("Startup quiet delay: %lu ms\r\n", (unsigned long)kStartupQuietMs);
Serial.printf("Startup replay window: %lu ms\r\n", (unsigned long)kStartupReplayWindowMs);
g_oled.clearBuffer();
g_oled.setFont(u8g2_font_5x8_tf);
g_oled.drawUTF8(0, 12, "TF HWDBG");
g_oled.drawUTF8(0, 24, "startup hold");
g_oled.drawUTF8(0, 36, "attach monitor");
g_oled.drawUTF8(0, 48, "waiting...");
g_oled.sendBuffer();
delay(kStartupQuietMs);
printStartupBanner();
}
void loop() {
static uint32_t lastPollMs = 0;
uint32_t now = millis();
if (now - lastPollMs < kPollIntervalMs) {
delay(5);
return;
}
lastPollMs = now;
g_sampleCount++;
DebugSnapshot snap = captureSnapshot();
if (now - g_bootMs <= kStartupReplayWindowMs &&
now - g_lastStartupReplayMs >= kStartupReplayPeriodMs) {
g_lastStartupReplayMs = now;
printStartupBanner();
}
printSnapshot(snap);
showSnapshot(snap);
}

View file

@ -0,0 +1,31 @@
; 20260401 Codex
; constantTFCard
[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\"

View file

@ -0,0 +1,2 @@
*.log

View file

@ -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\"

View file

@ -0,0 +1,463 @@
// 20260401 Codex
#include <Arduino.h>
#include <SD.h>
#include <SPI.h>
#include <Wire.h>
#include <U8g2lib.h>
#include <driver/gpio.h>
#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);
}

View file

@ -0,0 +1,257 @@
// 20260401 Codex
#include <Arduino.h>
#include <esp_system.h>
#include <SPI.h>
#include <SD.h>
#include <Wire.h>
#include <U8g2lib.h>
#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 kSdFreqHz = 400000;
static const uint32_t kBootSettleMs = 2000;
static const uint32_t kSdRailOffMs = 300;
static const uint32_t kSdRailOnSettleMs = 1200;
static const uint8_t kOutVotesBeforeRailCycle = 10;
static const uint32_t kMinRailCycleGapMs = 5000;
static const uint32_t kDelayedRetryOffsetsMs[] = {
1000,
3000,
7000,
15000
};
static XPowersLibInterface* g_pmu = nullptr;
static U8G2_SH1106_128X64_NONAME_F_HW_I2C g_oled(U8G2_R0, U8X8_PIN_NONE);
static SPIClass g_sdSpi(HSPI);
static uint32_t g_bootMs = 0;
static size_t g_nextDelayedRetry = 0;
static uint32_t g_lastRailCycleMs = 0;
static uint8_t g_consecutiveOut = 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 void oledShowStatus(const char* status, uint32_t sampleCount) {
char line1[24];
char line2[24];
char line3[24];
snprintf(line1, sizeof(line1), "%s TF CARD", NODE_LABEL);
snprintf(line2, sizeof(line2), "%s", status);
snprintf(line3, sizeof(line3), "sample %lu", (unsigned long)sampleCount);
g_oled.clearBuffer();
g_oled.setFont(u8g2_font_6x12_tf);
g_oled.drawUTF8(0, 14, line1);
g_oled.setFont(u8g2_font_logisoso20_tf);
g_oled.drawUTF8(0, 42, status);
g_oled.setFont(u8g2_font_6x12_tf);
g_oled.drawUTF8(0, 62, line3);
g_oled.sendBuffer();
}
static void oledShowBootPhase(const char* line2, const char* line3) {
char line1[24];
snprintf(line1, sizeof(line1), "%s TF CARD", NODE_LABEL);
g_oled.clearBuffer();
g_oled.setFont(u8g2_font_6x12_tf);
g_oled.drawUTF8(0, 14, line1);
if (line2) g_oled.drawUTF8(0, 32, line2);
if (line3) g_oled.drawUTF8(0, 50, line3);
g_oled.sendBuffer();
}
static bool cardReadable() {
SD.end();
g_sdSpi.end();
delay(2);
forceSpiDeselected();
g_sdSpi.begin(
tbeam_supreme::sdSck(),
tbeam_supreme::sdMiso(),
tbeam_supreme::sdMosi(),
tbeam_supreme::sdCs()
);
digitalWrite(tbeam_supreme::sdCs(), HIGH);
delay(1);
for (int i = 0; i < 10; ++i) {
g_sdSpi.transfer(0xFF);
}
if (!SD.begin(tbeam_supreme::sdCs(), g_sdSpi, kSdFreqHz)) {
return false;
}
if (SD.cardType() == CARD_NONE) {
SD.end();
return false;
}
File root = SD.open("/", FILE_READ);
bool ok = (bool)root;
if (root) {
root.close();
}
SD.end();
return ok;
}
static bool cycleSdRail() {
if (!g_pmu) {
Serial.println("rail cycle skipped: no PMU");
return false;
}
SD.end();
g_sdSpi.end();
forceSpiDeselected();
g_pmu->disablePowerOutput(XPOWERS_BLDO1);
delay(kSdRailOffMs);
g_pmu->setPowerChannelVoltage(XPOWERS_BLDO1, 3300);
g_pmu->enablePowerOutput(XPOWERS_BLDO1);
delay(kSdRailOnSettleMs);
forceSpiDeselected();
g_lastRailCycleMs = millis();
Serial.printf("rail cycle: off=%lu on_settle=%lu\r\n",
(unsigned long)kSdRailOffMs,
(unsigned long)kSdRailOnSettleMs);
return true;
}
static void runDelayedRetry(const char* label) {
bool readable = cardReadable();
const char* status = readable ? "card IN" : "card OUT";
Serial.printf("delayed retry %s -> %s\r\n", label, status);
oledShowStatus(status, 0);
}
static void handleSerialCommands() {
while (Serial.available() > 0) {
int ch = Serial.read();
if (ch == 'r' || ch == 'R') {
Serial.println("manual command: SD rail reset");
oledShowBootPhase("manual SD reset", "re-probing");
if (cycleSdRail()) {
bool readable = cardReadable();
Serial.printf("manual SD reset -> %s\r\n", readable ? "card IN" : "card OUT");
oledShowStatus(readable ? "card IN" : "card OUT", 0);
}
} else if (ch == 'b' || ch == 'B') {
Serial.println("manual command: full reboot");
Serial.println("restarting now...");
oledShowBootPhase("manual reboot", "restarting");
delay(250);
ESP.restart();
} else if (ch == '\r' || ch == '\n') {
continue;
} else {
Serial.printf("commands: r=sd reset, b=reboot (got '%c')\r\n", (char)ch);
}
}
}
void setup() {
Serial.begin(115200);
delay(kSerialDelayMs);
Wire.begin(OLED_SDA, OLED_SCL);
g_oled.begin();
g_oled.clearDisplay();
oledShowBootPhase("BOOT", "init");
tbeam_supreme::initPmuForPeripherals(g_pmu, &Serial);
forceSpiDeselected();
g_bootMs = millis();
Serial.println();
Serial.println("constantTFCard");
Serial.printf("Node: %s\r\n", NODE_LABEL);
Serial.printf("Polling every %lu ms\r\n", (unsigned long)kPollIntervalMs);
Serial.printf("Initial settle delay %lu ms\r\n", (unsigned long)kBootSettleMs);
Serial.println("Commands: r=SD rail reset, b=full reboot");
oledShowBootPhase("BOOT", "settling SD rail");
delay(kBootSettleMs);
runDelayedRetry("after_settle");
}
void loop() {
static uint32_t lastPollMs = 0;
static uint32_t sampleCount = 0;
handleSerialCommands();
uint32_t now = millis();
if (g_nextDelayedRetry < (sizeof(kDelayedRetryOffsetsMs) / sizeof(kDelayedRetryOffsetsMs[0])) &&
now - g_bootMs >= kDelayedRetryOffsetsMs[g_nextDelayedRetry]) {
char label[20];
snprintf(label, sizeof(label), "t+%lus", (unsigned long)(kDelayedRetryOffsetsMs[g_nextDelayedRetry] / 1000));
runDelayedRetry(label);
g_nextDelayedRetry++;
}
if (now - lastPollMs < kPollIntervalMs) {
delay(5);
return;
}
lastPollMs = now;
sampleCount++;
bool readable = cardReadable();
if (readable) {
g_consecutiveOut = 0;
} else if (g_consecutiveOut < 255) {
g_consecutiveOut++;
}
if (!readable &&
g_consecutiveOut >= kOutVotesBeforeRailCycle &&
now - g_lastRailCycleMs >= kMinRailCycleGapMs) {
Serial.printf("persistent OUT: %u polls, forcing SD rail cycle\r\n",
(unsigned)g_consecutiveOut);
oledShowBootPhase("OUT -> rail reset", "re-probing SD");
if (cycleSdRail()) {
bool retryReadable = cardReadable();
readable = retryReadable;
g_consecutiveOut = retryReadable ? 0 : kOutVotesBeforeRailCycle;
Serial.printf("after rail cycle -> %s\r\n", retryReadable ? "card IN" : "card OUT");
}
}
const char* status = readable ? "card IN" : "card OUT";
Serial.println(status);
oledShowStatus(status, sampleCount);
}