Compare commits

..

15 commits

Author SHA1 Message Date
2aec641fc2 exercises: add Exercise 07 SD startup watcher with OLED status and hot-insert/removal handling 2026-02-15 12:25:03 -08:00
0217ece5e5 Fixed Issue #1, see explanations 2026-02-14 14:17:58 -08:00
ee8b42a020 This fails... totally. Preserving for posterity. Chat states: Root cause of the regression
In the “full main.cpp” I gave you, dumpSdPins() did this:

pinMode(SCK,  INPUT_PULLUP);
pinMode(MISO, INPUT_PULLUP);
pinMode(MOSI, INPUT_PULLUP);

…and you were calling dumpSdPins("after-idle-clocks") inside tryMountWithBus(), after bus.begin() and the 0xFF idle clocks, but before SD.begin().

That means: right before SD.begin(), you were accidentally turning the SPI pins back into inputs. The card then can’t respond, so you get endless:

sdCommand(): Card Failed! cmd: 0x00

f_mount failed: (3) The physical drive cannot work

That matches your new log perfectly.
2026-02-14 14:03:07 -08:00
a349130858 my changes, untested, before ChatGTP complete revision... for posterity 2026-02-14 13:55:41 -08:00
432f17b2be Modified by ChatGPT for migration to microRecticulum_Firmware, not verified by me... yet, but I am preserving to document this stage. 2026-02-14 10:10:31 -08:00
1be5b59c7a Adding firmware which was tested using microReticulum as a sub modules only, **not** microReticulum_Firmware. Preserving the fieldtest_beacon for posterity even though it will be modified to utilize microReticulm_Firmware going forward." 2026-02-14 09:25:22 -08:00
d0e5fc9ab7 Start of migration to microReticulumFirmware from microReticulm after Chad comment on Matrix clarifying the two projects and my conflating the two after returning from a test in Meshtastic. ChatGPT has handled the integration, but we have not tested the migration against any of the exercises. Note: the SD card exercise needs an Issue logged about its failure at start-up and the need to remove and re-insert a card before it can be read. 2026-02-14 09:22:55 -08:00
544d459c9b RTC keeps time between POWER OFF & ON, SD Card at start still needs work -- if card is in the slot, it is not readable until it is pulled on and then inserted. 2026-02-13 18:52:17 -08:00
a83684d0cb Exercise 02 working: basic screen print out 2026-02-13 15:51:57 -08:00
8cf97e0e5a Exercises 00 & 01 working, Documentation still in progress, moving to my source control server. 2026-02-13 14:03:09 -08:00
84d947a3f0 Add host-side rns-provision tool and portable build infrastructure
- Add tools/keygen/rns-provision CLI
- Generate Reticulum identities with TSV/JSON output
- Add provisioning bundle support (--outdir, --bundle)
- Write manifest.json + per-device identity.json/bin + label.txt
- Enforce 0600 permissions on private key material
- Add -h/--help and version output
- Make build portable across distros (msgpack shim targets)
- Integrate ArduinoJson include paths
- Disable ReticulumShared build (static-only)
2026-02-12 11:17:48 -08:00
18e8d2c8ea Compiles, needs some extra tweaking 2026-02-12 10:53:31 -08:00
4a9cc72b6a Add ArxContainer dependency for DebugLog 2026-02-12 10:16:13 -08:00
52fc683fa9 Add ArxTypeTraits dependency for DebugLog 2026-02-12 10:14:20 -08:00
222934c7c1 Add DebugLog dependency for native CMake builds 2026-02-12 09:24:56 -08:00
48 changed files with 3568 additions and 0 deletions

19
.gitignore vendored Normal file
View file

@ -0,0 +1,19 @@
/build/
/provisioning/
/*.log
.pio/
.pio
.vscode/
*.elf
*.bin
*.map
# Emacs backup files
*~
\#*\#
.\#*
# Emacs dir locals (optional)
.dir-locals.el
/hold/
.platformio_local/

12
.gitmodules vendored
View file

@ -1,3 +1,15 @@
[submodule "external/microReticulum"]
path = external/microReticulum
url = https://github.com/attermann/microReticulum.git
[submodule "external/DebugLog"]
path = external/DebugLog
url = https://github.com/hideakitai/DebugLog.git
[submodule "external/ArxTypeTraits"]
path = external/ArxTypeTraits
url = https://github.com/hideakitai/ArxTypeTraits.git
[submodule "external/ArxContainer"]
path = external/ArxContainer
url = https://github.com/hideakitai/ArxContainer.git
[submodule "external/microReticulum_Firmware"]
path = external/microReticulum_Firmware
url = https://github.com/attermann/microReticulum_Firmware

68
CMakeLists.txt Normal file
View file

@ -0,0 +1,68 @@
cmake_minimum_required(VERSION 3.16)
project(microReticulumTbeam LANGUAGES C CXX)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
# --------------------------------------------------------------------
# Portability shims
#
# microReticulum's CMake currently links against "msgpackc-cxx" and "MsgPack"
# as if they were system libraries, which breaks on machines that don't have
# those exact libs installed.
#
# Define shim targets so CMake treats them as targets (no "-l...").
# If/when you want a real msgpack-cxx dependency, replace the shim with
# FetchContent/find_package and link to that instead.
# --------------------------------------------------------------------
if(NOT TARGET msgpackc-cxx)
add_library(msgpackc-cxx INTERFACE)
endif()
if(NOT TARGET MsgPack)
add_library(MsgPack INTERFACE)
endif()
# Pull in the microReticulum submodule build
add_subdirectory(external/microReticulum)
# Provide DebugLog.h for microReticulum's MsgPack dependency
#set(DEBUGLOG_DIR ${CMAKE_SOURCE_DIR}/external/DebugLog)
#if(TARGET ReticulumShared)
# target_include_directories(ReticulumShared PUBLIC ${DEBUGLOG_DIR})
#endif()
#if(TARGET ReticulumStatic)
# target_include_directories(ReticulumStatic PUBLIC ${DEBUGLOG_DIR})
#endif()
set(DEBUGLOG_DIR ${CMAKE_SOURCE_DIR}/external/DebugLog)
set(ARX_TYPETRAITS_DIR ${CMAKE_SOURCE_DIR}/external/ArxTypeTraits)
set(ARX_CONTAINER_DIR ${CMAKE_SOURCE_DIR}/external/ArxContainer)
if(TARGET ReticulumShared)
target_include_directories(ReticulumShared PUBLIC
${DEBUGLOG_DIR}
${ARX_TYPETRAITS_DIR}
${ARX_CONTAINER_DIR}
)
endif()
if(TARGET ReticulumStatic)
target_include_directories(ReticulumStatic PUBLIC
${DEBUGLOG_DIR}
${ARX_TYPETRAITS_DIR}
${ARX_CONTAINER_DIR}
)
endif()
# We only need the static library for host-side tooling.
# The shared lib target requires system msgpack libs on some systems.
if(TARGET ReticulumShared)
set_target_properties(ReticulumShared PROPERTIES EXCLUDE_FROM_ALL YES)
endif()
# Build our host-side tools
add_subdirectory(tools)

View file

@ -2,3 +2,14 @@
microReticulum For Field Testing With LilyGo T-Beam SUPREMES
Field Testing Only. Used to specially program a group of T-Beams, each having the others' contact information and keys, which are then deployed in the field with people moving about to capture what was successfully sent and received and at what coordinates. Data is stored on SD cards and then retrieved at the end of the test an dumped into a PostgreSQL databse for analysis.
## Dependency Direction
This repo is migrating from `external/microReticulum` to `external/microReticulum_Firmware`.
Goal:
- Reuse upstream T-Beam SUPREME integration work in `microReticulum_Firmware`.
- Avoid reimplementing already-solved board integration (PMU, SD, RTC, GPS, LoRa setup).
Status:
- Migration plan is tracked in `docs/microreticulum_firmware_migration.md`.
- Existing exercises remain functional during migration.

View file

@ -0,0 +1,45 @@
# microReticulum Firmware Migration Plan
## Objective
Migrate this project from the current submodule:
- `external/microReticulum`
to:
- `external/microReticulum_Firmware`
so this repo consumes existing T-Beam SUPREME integration instead of duplicating it.
## Current State
- `.gitmodules` currently declares `external/microReticulum`.
- `.gitmodules` now also declares `external/microReticulum_Firmware`:
- URL: `https://github.com/attermann/microReticulum_Firmware`
- Current commit: `5dc607fc7227c46ccb19244e456782fbb7775eae`
- Exercises under `exercises/` are self-contained and currently compile independently.
## Planned Migration Steps
1. Add the new submodule at `external/microReticulum_Firmware`.
2. Keep `external/microReticulum` temporarily for side-by-side validation.
3. Inventory reusable components from `microReticulum_Firmware`:
- board init / PMU power sequencing
- LoRa interface setup
- SD/RTC/GPS integration glue
4. Refactor local firmware entry points to call upstream components where possible.
5. Update exercise docs to distinguish:
- hardware smoke tests (local exercises)
- integration paths (from `microReticulum_Firmware`)
6. After parity validation, remove or archive `external/microReticulum`.
Step-2 inventory output:
- `docs/microreticulum_firmware_step2_adoption_matrix.md`
## Validation Checklist
- Build passes for all key exercises.
- SD/RTC/GPS startup behavior remains stable.
- LoRa send/receive smoke tests still pass.
- Fieldtest beacon path compiles and boots.
## Submodule Commands Used
```bash
git submodule add https://github.com/attermann/microReticulum_Firmware external/microReticulum_Firmware
git submodule update --init --recursive
```

View file

@ -0,0 +1,62 @@
# Step 2 Adoption Matrix
## Scope
Repository: `microReticulumTbeam`
Submodule source: `external/microReticulum_Firmware`
## What Step 2 Means
Identify and wire the first low-risk points where this repo should consume existing board-integration logic from `microReticulum_Firmware`, instead of maintaining duplicate local assumptions.
## High-Value Upstream Sources
- `external/microReticulum_Firmware/Boards.h`
- T-Beam Supreme pin map and feature flags.
- Includes SD pins (`SD_CS=47`, `SD_CLK=36`, `SD_MISO=37`, `SD_MOSI=35`), PMU I2C pins (`I2C_SDA=42`, `I2C_SCL=41`), LoRa pins.
- `external/microReticulum_Firmware/Power.h`
- AXP2101 setup sequence for T-Beam Supreme (`BOARD_TBEAM_S_V1`), including SD rail (BLDO1), ALDO rails, charging config.
- `external/microReticulum_Firmware/platformio.ini`
- `env:ttgo-t-beam-supreme` build model and dependency pattern.
## Local Targets And First Consumers
1. `firmware/fieldtest_beacon/src/main.cpp`
- Why first: this is the integration entry point, not just a smoke test.
- Step-2 change made: SD CS now comes from `Boards.h` instead of hardcoded `10`.
2. `exercises/04_SD_card/src/main.cpp`
- Why second: duplicates PMU + SD pin assumptions already present upstream.
- Planned consume-first item: PMU rail setup pattern from `Power.h`.
3. `exercises/05_SD_Card_Watcher/src/main.cpp`
- Why third: extends `04` and should share the same PMU/pin source strategy.
- Planned consume-first item: same board/power source as `04`.
4. `exercises/06_RTC_check/src/main.cpp`
- Why fourth: depends on PMU + I2C pin assumptions that overlap upstream.
- Planned consume-first item: board I2C pin source and PMU readiness sequence.
## Current Wiring Done
- Added submodule:
- `external/microReticulum_Firmware`
- Wired one concrete consumer:
- `firmware/fieldtest_beacon/src/main.cpp` now includes `Boards.h` and uses `SD_CS`.
- `firmware/fieldtest_beacon/platformio.ini` now includes `external/microReticulum_Firmware` headers and sets `BOARD_MODEL=BOARD_TBEAM_S_V1`.
- Added local adapter:
- `shared/boards/tbeam_supreme_adapter.h`
- Exposes board pins and PMU setup based on upstream T-Beam Supreme definitions.
- Refactored exercises to consume adapter:
- `exercises/04_SD_card`
- `exercises/05_SD_Card_Watcher`
- `exercises/06_RTC_check`
## Validation Status
- `firmware/fieldtest_beacon` now builds after adding required deps:
- `ArduinoJson` 7.x
- `MsgPack`
- `Crypto` (provides `Ed25519.h` and `Curve25519.h`)
- `exercises/04_SD_card` builds.
- `exercises/05_SD_Card_Watcher` builds.
- `exercises/06_RTC_check` builds.
## Next Step Candidate
Reduce macro redefinition warnings emitted by `Boards.h` in adapter consumers:
- Option A: small local board-map header with only required constants copied from upstream board model.
- Option B: upstream contribution to make board capability macros conditional/override-safe.

View file

@ -0,0 +1,116 @@
Command:
pio run -e node_a -t upload --upload-port /dev/ttyACM0
Here's a sample compile & upload session:
(rnsenv) jlpoole@jp /usr/local/src/microreticulum/microReticulumTbeam/exercises/00_usb_radio_check $ pio run -e node_a -t upload --upload-port /dev/ttyACM0
Processing node_a (platform: espressif32; framework: arduino; board: esp32-s3-devkitc-1)
------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
Verbose mode can be enabled via `-v, --verbose` option
CONFIGURATION: https://docs.platformio.org/page/boards/espressif32/esp32-s3-devkitc-1.html
PLATFORM: Espressif 32 (6.12.0) > Espressif ESP32-S3-DevKitC-1-N8 (8 MB QD, No PSRAM)
HARDWARE: ESP32S3 240MHz, 320KB RAM, 8MB Flash
DEBUG: Current (esp-builtin) On-board (esp-builtin) External (cmsis-dap, esp-bridge, esp-prog, iot-bus-jtag, jlink, minimodule, olimex-arm-usb-ocd, olimex-arm-usb-ocd-h, olimex-arm-usb-tiny-h, olimex-jtag-tiny, tumpa)
PACKAGES:
- framework-arduinoespressif32 @ 3.20017.241212+sha.dcc1105b
- tool-esptoolpy @ 2.40900.250804 (4.9.0)
- tool-mkfatfs @ 2.0.1
- tool-mklittlefs @ 1.203.210628 (2.3)
- tool-mkspiffs @ 2.230.0 (2.30)
- toolchain-riscv32-esp @ 8.4.0+2021r2-patch5
- toolchain-xtensa-esp32s3 @ 8.4.0+2021r2-patch5
LDF: Library Dependency Finder -> https://bit.ly/configure-pio-ldf
LDF Modes: Finder ~ chain, Compatibility ~ soft
Found 34 compatible libraries
Scanning dependencies...
Dependency Graph
|-- RadioLib @ 6.6.0
|-- SPI @ 2.0.0
Building in release mode
Retrieving maximum program size .pio/build/node_a/firmware.elf
Checking size .pio/build/node_a/firmware.elf
Advanced Memory Usage is available via "PlatformIO Home > Project Inspect"
RAM: [= ] 6.0% (used 19768 bytes from 327680 bytes)
Flash: [= ] 8.8% (used 294065 bytes from 3342336 bytes)
Configuring upload protocol...
AVAILABLE: cmsis-dap, esp-bridge, esp-builtin, esp-prog, espota, esptool, iot-bus-jtag, jlink, minimodule, olimex-arm-usb-ocd, olimex-arm-usb-ocd-h, olimex-arm-usb-tiny-h, olimex-jtag-tiny, tumpa
CURRENT: upload_protocol = esptool
Looking for upload port...
Using manually specified: /dev/ttyACM0
Uploading .pio/build/node_a/firmware.bin
esptool.py v4.9.0
Serial port /dev/ttyACM0
Connecting...
Chip is ESP32-S3 (QFN56) (revision v0.2)
Features: WiFi, BLE, Embedded Flash 8MB (GD)
Crystal is 40MHz
USB mode: USB-Serial/JTAG
MAC: 48:ca:43:5a:93:a0
Uploading stub...
Running stub...
Stub running...
Changing baud rate to 460800
Changed.
Configuring flash size...
Flash will be erased from 0x00000000 to 0x00003fff...
Flash will be erased from 0x00008000 to 0x00008fff...
Flash will be erased from 0x0000e000 to 0x0000ffff...
Flash will be erased from 0x00010000 to 0x00057fff...
SHA digest in image updated
Compressed 15104 bytes to 10430...
Writing at 0x00000000... (100 %)
Wrote 15104 bytes (10430 compressed) at 0x00000000 in 0.2 seconds (effective 519.1 kbit/s)...
Hash of data verified.
Compressed 3072 bytes to 146...
Writing at 0x00008000... (100 %)
Wrote 3072 bytes (146 compressed) at 0x00008000 in 0.0 seconds (effective 584.3 kbit/s)...
Hash of data verified.
Compressed 8192 bytes to 47...
Writing at 0x0000e000... (100 %)
Wrote 8192 bytes (47 compressed) at 0x0000e000 in 0.1 seconds (effective 721.9 kbit/s)...
Hash of data verified.
Compressed 294432 bytes to 164378...
Writing at 0x00010000... (9 %)
Writing at 0x0001bc31... (18 %)
Writing at 0x00024a76... (27 %)
Writing at 0x0002a8b3... (36 %)
Writing at 0x0002fd85... (45 %)
Writing at 0x000350b4... (54 %)
Writing at 0x0003b4b4... (63 %)
Writing at 0x000455f6... (72 %)
Writing at 0x0004c5eb... (81 %)
Writing at 0x00051c54... (90 %)
Writing at 0x00057b42... (100 %)
Wrote 294432 bytes (164378 compressed) at 0x00010000 in 1.9 seconds (effective 1241.1 kbit/s)...
Hash of data verified.
Leaving...
Hard resetting via RTS pin...
==================================================================================== [SUCCESS] Took 8.73 seconds ====================================================================================
Environment Status Duration
------------- -------- ------------
node_a SUCCESS 00:00:08.731
==================================================================================== 1 succeeded in 00:00:08.731 ====================================================================================
(rnsenv) jlpoole@jp /usr/local/src/microreticulum/microReticulumTbeam/exercises/00_usb_radio_check $
Here's an example of what displays in the console:
Booting LoRa test...
Initializing radio...
Radio chip: SX1262
Frequency: 915.000 MHz
SF: 7 BW: 125 CR: 5
radio.begin returned: 0
alive 0
Sending test frame...
TX state: 0
Starting receive...
startReceive returned: 0
alive 1
Sending test frame...
TX state: 0
Starting receive...
startReceive returned: 0
alive 2

View file

@ -0,0 +1,43 @@
; 20260212 ChatGPT
; $Id$
; $HeadURL$
[platformio]
default_envs = node_a
[env]
platform = espressif32
framework = arduino
board = esp32-s3-devkitc-1
monitor_speed = 115200
lib_deps =
jgromes/RadioLib@^6.6.0
; Common build flags (pins from Meshtastic tbeam-s3-core variant.h)
build_flags =
-D LORA_CS=10
-D LORA_MOSI=11
-D LORA_SCK=12
-D LORA_MISO=13
-D LORA_RESET=5
-D LORA_DIO1=1
-D LORA_BUSY=4
-D LORA_TCXO_VOLTAGE=1.8
-D ARDUINO_USB_MODE=1
-D ARDUINO_USB_CDC_ON_BOOT=1
; Radio params for println/printf + RadioLib init
-D LORA_FREQ=915.000
-D LORA_SF=7
-D LORA_BW=125
-D LORA_CR=5
[env:node_a]
build_flags =
${env.build_flags}
-D NODE_LABEL=\"A\"
[env:node_b]
build_flags =
${env.build_flags}
-D NODE_LABEL=\"B\"

View file

@ -0,0 +1,60 @@
#include <Arduino.h>
#include <SPI.h>
#include <RadioLib.h>
#ifndef LORA_FREQ
#define LORA_FREQ 915.000
#endif
#ifndef LORA_SF
#define LORA_SF 7
#endif
#ifndef LORA_BW
#define LORA_BW 125
#endif
#ifndef LORA_CR
#define LORA_CR 5
#endif
// SX1262 on T-Beam Supreme (tbeam-s3-core pinout)
SX1262 radio = new Module(LORA_CS, LORA_DIO1, LORA_RESET, LORA_BUSY);
int state; // = radio.begin(915.0, 125.0, 7, 5, 0x12, 14);
void setup() {
Serial.begin(115200);
delay(2000); // give USB time to enumerate
Serial.println("Booting LoRa test...");
Serial.println();
Serial.println("Initializing radio...");
SPI.begin(LORA_SCK, LORA_MISO, LORA_MOSI, LORA_CS);
Serial.printf("Radio chip: SX1262\r\n");
Serial.printf("Frequency: %.3f MHz\r\n", (double)LORA_FREQ);
Serial.printf("SF: %d BW: %d CR: %d\r\n", LORA_SF, LORA_BW, LORA_CR);
int state = radio.begin(915.0, 125.0, 7, 5, 0x12, 14);
Serial.printf("radio.begin returned: %d\r\n", state);
}
void loop() {
static uint32_t counter = 0;
Serial.printf("alive %lu\n", counter++);
Serial.println("Sending test frame...");
int tx = radio.transmit("USB RADIO CHECK");
Serial.printf("TX state: %d\r\n", tx);
// we're not expecting to receive anything, just testing that we
// can call Receive()
Serial.println("Starting receive...");
state = radio.startReceive();
Serial.printf("startReceive returned: %d\r\n", state);
delay(1000);
}

View file

@ -0,0 +1,37 @@
; 20260212 ChatGPT
; $Id$
; $HeadURL$
[platformio]
default_envs = node_a
[env]
platform = espressif32
framework = arduino
board = esp32-s3-devkitc-1
monitor_speed = 115200
lib_deps =
jgromes/RadioLib@^6.6.0
; Common build flags (pins from Meshtastic tbeam-s3-core variant.h)
build_flags =
-D LORA_CS=10
-D LORA_MOSI=11
-D LORA_SCK=12
-D LORA_MISO=13
-D LORA_RESET=5
-D LORA_DIO1=1
-D LORA_BUSY=4
-D LORA_TCXO_VOLTAGE=1.8
-D ARDUINO_USB_MODE=1
-D ARDUINO_USB_CDC_ON_BOOT=1
[env:node_a]
build_flags =
${env.build_flags}
-D NODE_LABEL=\"A\"
[env:node_b]
build_flags =
${env.build_flags}
-D NODE_LABEL=\"B\"

View file

@ -0,0 +1,148 @@
// 20260212 ChatGPT
// $Id$
// $HeadURL$
#include <Arduino.h>
#include <SPI.h>
#include <RadioLib.h>
// --- Compile-time label ---
#ifndef NODE_LABEL
#define NODE_LABEL "?"
#endif
// --- Pins injected via platformio.ini build_flags ---
#ifndef LORA_CS
#error "LORA_CS not defined"
#endif
#ifndef LORA_DIO1
#error "LORA_DIO1 not defined"
#endif
#ifndef LORA_RESET
#error "LORA_RESET not defined"
#endif
#ifndef LORA_BUSY
#error "LORA_BUSY not defined"
#endif
// SX1262 on T-Beam Supreme (tbeam-s3-core pinout)
SX1262 radio = new Module(LORA_CS, LORA_DIO1, LORA_RESET, LORA_BUSY);
static volatile bool g_rx_flag = false;
static void onDio1Rise() {
g_rx_flag = true;
}
static void print_config() {
Serial.printf("Node=%s\n", NODE_LABEL);
Serial.printf("Pins: CS=%d DIO1=%d RST=%d BUSY=%d SCK=%d MISO=%d MOSI=%d\r\n",
(int)LORA_CS, (int)LORA_DIO1, (int)LORA_RESET, (int)LORA_BUSY,
(int)LORA_SCK, (int)LORA_MISO, (int)LORA_MOSI);
Serial.printf("LoRa: freq=915.0 BW=125 SF=7 CR=5 txp=14\r\n");
}
void setup() {
Serial.begin(115200);
delay(250);
Serial.println();
Serial.println("Exercise 01: LoRa ASCII ping-pong (serial only)");
// Ensure SPI pins match the variant
SPI.begin(LORA_SCK, LORA_MISO, LORA_MOSI, LORA_CS);
int state = radio.begin(915.0 /* MHz */, 125.0 /* kHz */, 7 /* SF */, 5 /* CR */, 0x12 /* sync */, 14 /* dBm */);
if (state != RADIOLIB_ERR_NONE) {
Serial.printf("ERROR: radio.begin failed, code=%d\r\n", state);
while (true) delay(1000);
}
// Match Meshtastic-like wiring assumptions for SX1262 modules:
// - DIO2 used as RF switch
// - TCXO at 1.8V
//OLD radio.setDio2AsRfSwitch(true);
//OLD radio.setTcxoVoltage((float)LORA_TCXO_VOLTAGE);
// below is replacement for above 2 lines:
//maybe_setDio2AsRfSwitch(radio, true);
//maybe_setTcxoVoltage(radio, (float)LORA_TCXO_VOLTAGE);
// end of replacement
// Set up RX interrupt
radio.setDio1Action(onDio1Rise);
// Start receiving
state = radio.startReceive();
if (state != RADIOLIB_ERR_NONE) {
Serial.printf("ERROR: startReceive failed, code=%d\r\n", state);
while (true) delay(1000);
}
print_config();
}
void loop() {
// Periodic TX (with a label-based offset to reduce collisions)
static uint32_t next_tx_ms = 0;
static uint32_t iter = 0;
uint32_t now = millis();
if (next_tx_ms == 0) {
// Offset A and B so they don't always collide
uint32_t offset = (NODE_LABEL[0] == 'A') ? 500 : 1500;
next_tx_ms = now + offset;
}
if ((int32_t)(now - next_tx_ms) >= 0) {
next_tx_ms = now + 2000; // 2 seconds for this smoke test
// String msg = String("I am ") + NODE_LABEL + " iter=" + String(iter++);
String msg = String(" ") + NODE_LABEL + " sends greetings. iter=" + String(iter++);
Serial.printf("TX: %s\r\n", msg.c_str());
//int tx = radio.transmit(msg);
//if (tx != RADIOLIB_ERR_NONE) {
// Serial.printf("TX ERROR code=%d\r\n", tx);
// }
// After transmit, resume RX
//radio.startReceive();
// DIO1 triggers on TX-done as well as RX-done.
// If left armed, TX completion looks like RX.
g_rx_flag = false;
radio.clearDio1Action();
int tx = radio.transmit(msg);
if (tx != RADIOLIB_ERR_NONE) {
Serial.printf("TX ERROR code=%d\r\n", tx);
}
// Re-arm RX interrupt and resume RX
g_rx_flag = false;
radio.setDio1Action(onDio1Rise);
radio.startReceive();
}
// RX handling
if (g_rx_flag) {
g_rx_flag = false;
String rx;
int state = radio.readData(rx);
if (state == RADIOLIB_ERR_NONE) {
Serial.printf("RX: %s | RSSI=%.1f SNR=%.1f\r\n",
rx.c_str(), radio.getRSSI(), radio.getSNR());
} else {
Serial.printf("RX ERROR code=%d\r\n", state);
}
// Keep receiving
radio.startReceive();
}
delay(5);
}

View file

@ -0,0 +1,55 @@
## Exercise 02: OLED Display
This exercise demonstrates multiple OLED rendering patterns on the LilyGO T-Beam Supreme (SH1106).
The firmware runs 3 demos in sequence, then shows a restart banner and repeats forever.
## Demo Sequence
### Demo 1: Scrolling Text
- Shows `Hello #<n>` lines.
- New lines are added below prior lines.
- Up to 5 lines are visible; old lines roll off the top.
### Demo 2: Fill Then Erase
- Displays `Count 1` through `Count 10`.
- Shows a short `Clearing screen...` message.
- Erases the OLED to demonstrate explicit clear behavior.
### Demo 3: Multi-Page Rotation
- Rotates through 3 distinct text pages.
- Each page has different content/layout to demonstrate refresh transitions.
- Includes a changing packet counter field on one page.
### Restart Banner
- Shows `Restarting 3 demos` for 5 seconds.
- Sequence restarts at Demo 1.
## Expected Serial Output
Serial output (115200) prints phase changes, for example:
- `Demo 1/3: scrolling Hello #`
- `Demo 2/3: clear screen after 10`
- `Demo 3/3: three-page rotation`
- `Restarting 3 demos`
### Build
From this directory:
```bash
source /home/jlpoole/rnsenv/bin/activate
pio run -e node_a
```
### Upload
Set your USB port:
```bash
source /home/jlpoole/rnsenv/bin/activate
pio run -e node_a -t upload --upload-port /dev/ttyACM0
```
### Serial Monitor
```bash
screen /dev/ttyACM0 115200
```

View file

@ -0,0 +1,32 @@
; 20260213 ChatGPT
; $Id$
; $HeadURL$
[platformio]
default_envs = node_a
[env]
platform = espressif32
framework = arduino
board = esp32-s3-devkitc-1
monitor_speed = 115200
lib_deps =
olikraus/U8g2@^2.36.4
; Common build flags (pins from Meshtastic tbeam-s3-core variant.h)
build_flags =
-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:node_a]
build_flags =
${env.build_flags}
-D NODE_LABEL=\"A\"
[env:node_b]
build_flags =
${env.build_flags}
-D NODE_LABEL=\"B\"

View file

@ -0,0 +1,162 @@
// 20260213 ChatGPT
// $Id$
// $HeadURL$
#include <Arduino.h>
#include <Wire.h>
#include <U8g2lib.h>
#ifndef OLED_SDA
#define OLED_SDA 17
#endif
#ifndef OLED_SCL
#define OLED_SCL 18
#endif
#ifndef OLED_ADDR
#define OLED_ADDR 0x3C
#endif
// LilyGO T-Beam Supreme uses SH1106 OLED on I2C.
U8G2_SH1106_128X64_NONAME_F_HW_I2C oled(U8G2_R0, /* reset=*/U8X8_PIN_NONE);
static const uint8_t kMaxLines = 5;
static String g_lines[kMaxLines];
static uint32_t g_iteration = 1;
static uint32_t g_pageCounter = 1;
static void addLine(const String& line) {
for (uint8_t i = 0; i < kMaxLines - 1; ++i) {
g_lines[i] = g_lines[i + 1];
}
g_lines[kMaxLines - 1] = line;
}
static void drawLines() {
oled.clearBuffer();
oled.setFont(u8g2_font_6x10_tf);
const int yStart = 12;
const int yStep = 12;
for (uint8_t i = 0; i < kMaxLines; ++i) {
if (g_lines[i].length() == 0) {
continue;
}
oled.drawUTF8(0, yStart + (i * yStep), g_lines[i].c_str());
}
oled.sendBuffer();
}
static void clearLines() {
for (uint8_t i = 0; i < kMaxLines; ++i) {
g_lines[i] = "";
}
}
static void showCentered(const char* line1, const char* line2 = nullptr, const char* line3 = nullptr) {
oled.clearBuffer();
oled.setFont(u8g2_font_6x10_tf);
if (line1) oled.drawUTF8(0, 16, line1);
if (line2) oled.drawUTF8(0, 32, line2);
if (line3) oled.drawUTF8(0, 48, line3);
oled.sendBuffer();
}
static void demo1_scrollingHello() {
Serial.println("Demo 1/3: scrolling Hello #");
clearLines();
for (uint8_t i = 0; i < 10; ++i) {
String line = "Hello #" + String(g_iteration++);
addLine(line);
drawLines();
delay(1000);
}
}
static void demo2_clearAfterTen() {
Serial.println("Demo 2/3: clear screen after 10");
clearLines();
for (uint8_t i = 1; i <= 10; ++i) {
String line = "Count " + String(i);
addLine(line);
drawLines();
delay(500);
}
showCentered("Demo 2", "Clearing screen...");
delay(1000);
oled.clearDisplay();
oled.sendBuffer();
delay(1000);
}
static void drawPage(uint8_t page) {
oled.clearBuffer();
oled.setFont(u8g2_font_6x10_tf);
if (page == 0) {
oled.drawUTF8(0, 12, "Demo 3 - Page 1/3");
oled.drawUTF8(0, 28, "GPS: 37.7749 N");
oled.drawUTF8(0, 40, "LON: 122.4194 W");
oled.drawUTF8(0, 56, "Fix: 3D");
} else if (page == 1) {
oled.drawUTF8(0, 12, "Demo 3 - Page 2/3");
oled.drawUTF8(0, 28, "LoRa RSSI: -92 dBm");
oled.drawUTF8(0, 40, "LoRa SNR: +8.5 dB");
oled.drawUTF8(0, 56, "Pkts: " );
oled.setCursor(38, 56);
oled.print(g_pageCounter++);
} else {
oled.drawUTF8(0, 12, "Demo 3 - Page 3/3");
oled.drawUTF8(0, 28, "Node: TBM-SUPREME");
oled.drawUTF8(0, 40, "Mode: Field Test");
oled.drawUTF8(0, 56, "OLED refresh demo");
}
oled.sendBuffer();
}
static void demo3_threePages() {
Serial.println("Demo 3/3: three-page rotation");
for (uint8_t round = 0; round < 3; ++round) {
drawPage(0);
delay(700);
drawPage(1);
delay(700);
drawPage(2);
delay(700);
}
}
static void showRestartBanner() {
Serial.println("Restarting 3 demos");
showCentered("Restarting 3 demos", "Cycle begins again", "in 5 seconds...");
delay(5000);
}
void setup() {
Serial.begin(115200);
delay(250);
Wire.begin(OLED_SDA, OLED_SCL);
oled.setI2CAddress(OLED_ADDR << 1); // U8g2 expects 8-bit address.
oled.begin();
oled.clearBuffer();
oled.setFont(u8g2_font_6x10_tf);
oled.drawUTF8(0, 12, "Exercise 02 OLED");
oled.sendBuffer();
Serial.println("Exercise 02: OLED display loop");
Serial.printf("OLED SDA=%d SCL=%d ADDR=0x%02X\r\n", OLED_SDA, OLED_SCL, OLED_ADDR);
}
void loop() {
demo1_scrollingHello();
demo2_clearAfterTen();
demo3_threePages();
showRestartBanner();
}

View file

@ -0,0 +1,37 @@
## Exercise 04: SD Card
This exercise loops forever. Each cycle:
1. Prints `Sleeping 10 seconds` and waits 10 seconds.
2. Detects and mounts the SD card.
3. Prints card and filesystem info (type/size/used).
4. Writes `/Exercise_04_test.txt` with:
- `This is a test`
5. Ensures nested directories exist:
- `/test/testsub1/testsubsub1`
6. Writes nested file:
- `/test/testsub1/testsubsub1/Exercise_04_test.txt`
7. If either target file already exists, prints warning, erases it, then recreates it.
8. Demonstrates permission behavior:
- Notes FAT does not provide Unix `chmod/chown`.
- Shows access behavior via `FILE_READ` vs `FILE_WRITE` modes.
## Build
```bash
source /home/jlpoole/rnsenv/bin/activate
pio run -e node_a
```
## Upload
```bash
source /home/jlpoole/rnsenv/bin/activate
pio run -e node_a -t upload --upload-port /dev/ttyACM0
```
## Monitor
```bash
screen /dev/ttyACM0 115200
```

View file

@ -0,0 +1,33 @@
; 20260213 ChatGPT
; $Id$
; $HeadURL$
[platformio]
default_envs = node_a
[env]
platform = espressif32
framework = arduino
board = esp32-s3-devkitc-1
monitor_speed = 115200
lib_deps =
lewisxhe/XPowersLib@0.3.3
Wire
; SD pins based on T-Beam S3 core pin mapping
build_flags =
-I ../../shared/boards
-I ../../external/microReticulum_Firmware
-D BOARD_MODEL=BOARD_TBEAM_S_V1
-D ARDUINO_USB_MODE=1
-D ARDUINO_USB_CDC_ON_BOOT=1
[env:node_a]
build_flags =
${env.build_flags}
-D NODE_LABEL=\"A\"
[env:node_b]
build_flags =
${env.build_flags}
-D NODE_LABEL=\"B\"

View file

@ -0,0 +1,228 @@
// 20260213 ChatGPT
// $Id$
// $HeadURL$
#include <Arduino.h>
#include <FS.h>
#include <SD.h>
#include <SPI.h>
#include "tbeam_supreme_adapter.h"
static SPIClass sdSpiH(HSPI);
static SPIClass sdSpiF(FSPI);
static SPIClass* g_sdSpi = nullptr;
static const char* g_sdBusName = "none";
static uint32_t g_sdFreq = 0;
static XPowersLibInterface* g_pmu = nullptr;
static const char* kRootTestFile = "/Exercise_04_test.txt";
static const char* kNestedDir = "/test/testsub1/testsubsub1";
static const char* kNestedTestFile = "/test/testsub1/testsubsub1/Exercise_04_test.txt";
static const char* kPayload = "This is a test";
static bool initPmuForSdPower() {
return tbeam_supreme::initPmuForPeripherals(g_pmu, &Serial);
}
static const char* cardTypeToString(uint8_t type) {
switch (type) {
case CARD_MMC: return "MMC";
case CARD_SD: return "SDSC";
case CARD_SDHC: return "SDHC/SDXC";
default: return "UNKNOWN";
}
}
static bool tryMountWithBus(SPIClass& bus, const char* busName, uint32_t hz) {
SD.end();
bus.end();
delay(10);
// Keep inactive devices deselected on shared bus lines.
pinMode(tbeam_supreme::sdCs(), OUTPUT);
digitalWrite(tbeam_supreme::sdCs(), HIGH);
pinMode(tbeam_supreme::imuCs(), OUTPUT);
digitalWrite(tbeam_supreme::imuCs(), HIGH);
bus.begin(tbeam_supreme::sdSck(), tbeam_supreme::sdMiso(), tbeam_supreme::sdMosi(), tbeam_supreme::sdCs());
delay(2);
Serial.printf("SD: trying bus=%s freq=%lu Hz\r\n", busName, (unsigned long)hz);
if (!SD.begin(tbeam_supreme::sdCs(), bus, hz)) {
Serial.println("SD: mount failed (possible non-FAT format, power, or bus issue)");
return false;
}
uint8_t cardType = SD.cardType();
if (cardType == CARD_NONE) {
SD.end();
return false;
}
g_sdSpi = &bus;
g_sdBusName = busName;
g_sdFreq = hz;
return true;
}
static bool mountCard() {
const uint32_t freqs[] = {400000, 1000000, 4000000, 10000000};
for (uint8_t i = 0; i < (sizeof(freqs) / sizeof(freqs[0])); ++i) {
if (tryMountWithBus(sdSpiH, "HSPI", freqs[i])) {
Serial.println("SD: card detected and mounted");
return true;
}
}
for (uint8_t i = 0; i < (sizeof(freqs) / sizeof(freqs[0])); ++i) {
if (tryMountWithBus(sdSpiF, "FSPI", freqs[i])) {
Serial.println("SD: card detected and mounted");
return true;
}
}
Serial.println("SD: begin() failed on all bus/frequency attempts");
Serial.println(" likely card absent, bad format, pin mismatch, or hardware issue");
return false;
}
static void printCardInfo() {
uint8_t cardType = SD.cardType();
uint64_t cardSizeMB = SD.cardSize() / (1024ULL * 1024ULL);
uint64_t totalMB = SD.totalBytes() / (1024ULL * 1024ULL);
uint64_t usedMB = SD.usedBytes() / (1024ULL * 1024ULL);
Serial.printf("SD type: %s\r\n", cardTypeToString(cardType));
Serial.printf("SD size: %llu MB\r\n", cardSizeMB);
Serial.printf("FS total: %llu MB\r\n", totalMB);
Serial.printf("FS used : %llu MB\r\n", usedMB);
Serial.printf("SPI bus: %s @ %lu Hz\r\n", g_sdBusName, (unsigned long)g_sdFreq);
}
static bool ensureDirRecursive(const char* path) {
String full(path);
if (!full.startsWith("/")) {
full = "/" + full;
}
int start = 1;
while (start > 0 && start < (int)full.length()) {
int slash = full.indexOf('/', start);
String partial = (slash < 0) ? full : full.substring(0, slash);
if (!SD.exists(partial.c_str())) {
Serial.printf("Creating directory: %s\r\n", partial.c_str());
if (!SD.mkdir(partial.c_str())) {
Serial.printf("ERROR: mkdir failed for %s\r\n", partial.c_str());
return false;
}
}
if (slash < 0) {
break;
}
start = slash + 1;
}
return true;
}
static bool rewriteFile(const char* path, const char* payload) {
if (SD.exists(path)) {
Serial.printf("WARNING: %s exists ... erasing\r\n", path);
if (!SD.remove(path)) {
Serial.printf("ERROR: failed to erase %s\r\n", path);
return false;
}
}
File f = SD.open(path, FILE_WRITE);
if (!f) {
Serial.printf("ERROR: failed to create %s\r\n", path);
return false;
}
size_t wrote = f.println(payload);
f.close();
if (wrote == 0) {
Serial.printf("ERROR: write failed for %s\r\n", path);
return false;
}
Serial.printf("Wrote file: %s\r\n", path);
return true;
}
static void permissionsDemo(const char* path) {
Serial.println("Permissions demo:");
Serial.println(" SD/FAT does not support Unix permissions (chmod/chown).");
Serial.println(" Access control is by open mode (FILE_READ/FILE_WRITE).");
File r = SD.open(path, FILE_READ);
if (!r) {
Serial.printf(" Could not open %s as FILE_READ\r\n", path);
return;
}
Serial.printf(" FILE_READ open succeeded, size=%u bytes\r\n", (unsigned)r.size());
size_t writeInReadMode = r.print("attempt write while opened read-only");
if (writeInReadMode == 0) {
Serial.println(" As expected, write via FILE_READ handle was blocked.");
} else {
Serial.printf(" NOTE: write via FILE_READ returned %u (unexpected)\r\n", (unsigned)writeInReadMode);
}
r.close();
}
void setup() {
Serial.begin(115200);
Serial.println("Sleeping for 5 seconds to allow Serial Monitor connection...");
delay(5000); // Time to open Serial Monitor after reset
Serial.println("\r\n==================================================");
Serial.println("Exercise 04: SD card test loop");
Serial.println("==================================================");
Serial.printf("Pins: CS=%d SCK=%d MISO=%d MOSI=%d\r\n",
tbeam_supreme::sdCs(), tbeam_supreme::sdSck(),
tbeam_supreme::sdMiso(), tbeam_supreme::sdMosi());
Serial.printf("PMU I2C: SDA1=%d SCL1=%d\r\n",
tbeam_supreme::i2cSda(), tbeam_supreme::i2cScl());
Serial.println("Note: SD must be FAT16/FAT32 for Arduino SD library.\r\n");
initPmuForSdPower();
}
void loop() {
Serial.println("Sleeping 10 seconds");
delay(10000);
if (!mountCard()) {
Serial.println("SD step skipped this cycle.\r\n");
return;
}
printCardInfo();
if (!rewriteFile(kRootTestFile, kPayload)) {
SD.end();
Serial.println("Cycle ended due to root file error.\r\n");
return;
}
if (!ensureDirRecursive(kNestedDir)) {
SD.end();
Serial.println("Cycle ended due to directory creation error.\r\n");
return;
}
if (!rewriteFile(kNestedTestFile, kPayload)) {
SD.end();
Serial.println("Cycle ended due to nested file error.\r\n");
return;
}
permissionsDemo(kRootTestFile);
SD.end();
Serial.println("Cycle complete.\r\n");
}

View file

@ -0,0 +1,39 @@
## Exercise 05: SD Card Watcher
This exercise continuously watches SD card presence and prints state-change events.
Watcher behavior:
1. Initializes PMU and enables SD power rail (AXP2101 BLDO1).
2. Polls for card changes with debounced state transitions.
3. Emits events only on change:
- `EVENT: card inserted/mounted`
- `EVENT: card removed/unavailable`
- `EVENT: no card detected`
4. On mount event, prints card info and runs SD write workflow.
5. Every 15 seconds while mounted, runs a periodic write/permission check.
6. Uses fast preferred probe (`HSPI @ 400k`) and occasional full fallback scan.
Files used in this exercise:
- `/Exercise_05_test.txt`
- `/test/testsub1/testsubsub1/Exercise_05_test.txt`
## Build
```bash
source /home/jlpoole/rnsenv/bin/activate
pio run -e node_a
```
## Upload
```bash
source /home/jlpoole/rnsenv/bin/activate
pio run -e node_a -t upload --upload-port /dev/ttyACM0
```
## Monitor
```bash
screen /dev/ttyACM0 115200
```

View file

@ -0,0 +1,33 @@
; 20260213 ChatGPT
; $Id$
; $HeadURL$
[platformio]
default_envs = node_a
[env]
platform = espressif32
framework = arduino
board = esp32-s3-devkitc-1
monitor_speed = 115200
lib_deps =
lewisxhe/XPowersLib@0.3.3
Wire
; SD pins based on T-Beam S3 core pin mapping
build_flags =
-I ../../shared/boards
-I ../../external/microReticulum_Firmware
-D BOARD_MODEL=BOARD_TBEAM_S_V1
-D ARDUINO_USB_MODE=1
-D ARDUINO_USB_CDC_ON_BOOT=1
[env:node_a]
build_flags =
${env.build_flags}
-D NODE_LABEL=\"A\"
[env:node_b]
build_flags =
${env.build_flags}
-D NODE_LABEL=\"B\"

View file

@ -0,0 +1,485 @@
// 20260214 ChatGPT
// $Id$
// $HeadURL$
#include <Arduino.h>
#include <stdarg.h>
#include <FS.h>
#include <SD.h>
#include <SPI.h>
#include "driver/gpio.h" // gpio_get_level()
#include "tbeam_supreme_adapter.h"
// -------------------------
// Configuration toggles
// -------------------------
#define ENABLE_SD_RAIL_CYCLE 1 // Power-cycle AXP2101 BLDO1 (SD rail) at boot.
#define ENABLE_PIN_DUMPS 1 // Log SPI pin logic levels at key points (NON-INTRUSIVE).
#define STARTUP_SERIAL_DELAY_MS 5000
// -------------------------
// Globals
// -------------------------
static SPIClass sdSpiH(HSPI);
static SPIClass sdSpiF(FSPI);
static SPIClass* g_sdSpi = nullptr;
static const char* g_sdBusName = "none";
static uint32_t g_sdFreq = 0;
static XPowersLibInterface* g_pmu = nullptr;
static const char* kRootTestFile = "/Exercise_05_test.txt";
static const char* kNestedDir = "/test/testsub1/testsubsub1";
static const char* kNestedTestFile = "/test/testsub1/testsubsub1/Exercise_05_test.txt";
static const char* kPayload = "This is a test";
enum class WatchState : uint8_t {
UNKNOWN = 0,
ABSENT,
MOUNTED
};
static WatchState g_watchState = WatchState::UNKNOWN;
static uint8_t g_presentVotes = 0;
static uint8_t g_absentVotes = 0;
static uint32_t g_lastPollMs = 0;
static uint32_t g_lastFullScanMs = 0;
static uint32_t g_lastPeriodicActionMs = 0;
static const uint32_t kPollIntervalAbsentMs = 1000;
static const uint32_t kPollIntervalMountedMs = 2000;
static const uint32_t kFullScanIntervalMs = 10000;
static const uint32_t kPeriodicActionMs = 15000;
static const uint8_t kVotesToPresent = 2;
static const uint8_t kVotesToAbsent = 5; // More votes needed to declare absent to prevent false removes.
static const uint32_t kStartupWarmupMs = 1500; // Allow PMU and SD rail to stabilize.
static uint32_t g_logSeq = 0;
// -------------------------
// Logging helpers
// -------------------------
static void logf(const char* fmt, ...) {
char msg[192];
va_list args;
va_start(args, fmt);
vsnprintf(msg, sizeof(msg), fmt, args);
va_end(args);
Serial.printf("[%10lu][%06lu] %s\r\n",
(unsigned long)millis(),
(unsigned long)g_logSeq++,
msg);
}
static void dumpPmu(const char* tag, XPowersLibInterface* pmu) {
if (!pmu) {
logf("PMU(%s): pmu=null", tag);
return;
}
bool bldo1 = pmu->isPowerChannelEnable(XPOWERS_BLDO1);
int vbus = pmu->getVbusVoltage();
int batt = pmu->getBattVoltage();
logf("PMU(%s): BLDO1(SD)=%s VBUS=%dmV VBAT=%dmV",
tag, bldo1 ? "ON" : "OFF", vbus, batt);
}
// IMPORTANT: this function MUST NOT modify pin modes (regression cause).
static void dumpSdPins(const char* tag) {
#if ENABLE_PIN_DUMPS
const gpio_num_t CS = (gpio_num_t)tbeam_supreme::sdCs();
const gpio_num_t SCK = (gpio_num_t)tbeam_supreme::sdSck();
const gpio_num_t MISO = (gpio_num_t)tbeam_supreme::sdMiso();
const gpio_num_t MOSI = (gpio_num_t)tbeam_supreme::sdMosi();
int cs = gpio_get_level(CS);
int sck = gpio_get_level(SCK);
int miso = gpio_get_level(MISO);
int mosi = gpio_get_level(MOSI);
logf("PINS(%s): CS=%d SCK=%d MISO=%d MOSI=%d", tag, cs, sck, miso, mosi);
#else
(void)tag;
#endif
}
// -------------------------
// Power + bus conditioning
// -------------------------
static void forceSpiDeselected() {
pinMode(tbeam_supreme::sdCs(), OUTPUT);
digitalWrite(tbeam_supreme::sdCs(), HIGH);
pinMode(tbeam_supreme::imuCs(), OUTPUT);
digitalWrite(tbeam_supreme::imuCs(), HIGH);
}
static bool initPmuForSdPower() {
bool ok = tbeam_supreme::initPmuForPeripherals(g_pmu, &Serial);
if (!ok) {
logf("ERROR: PMU init failed");
return false;
}
logf("PMU adapter: AXP2101 ready, BLDO1(SD)=%s",
g_pmu && g_pmu->isPowerChannelEnable(XPOWERS_BLDO1) ? "ON" : "OFF");
return true;
}
static void cycleSdRail(XPowersLibInterface* pmu,
uint32_t off_ms = 250,
uint32_t on_settle_ms = 600) {
#if ENABLE_SD_RAIL_CYCLE
if (!pmu) {
logf("SD rail cycle skipped: pmu=null");
return;
}
dumpPmu("pre-sd-cycle", pmu);
// Ensure the card is NOT selected while power is unstable.
forceSpiDeselected();
dumpSdPins("pre-sd-cycle");
pmu->disablePowerOutput(XPOWERS_BLDO1);
delay(off_ms);
pmu->setPowerChannelVoltage(XPOWERS_BLDO1, 3300);
pmu->enablePowerOutput(XPOWERS_BLDO1);
delay(on_settle_ms);
dumpPmu("post-sd-cycle", pmu);
dumpSdPins("post-sd-cycle");
#else
(void)pmu; (void)off_ms; (void)on_settle_ms;
#endif
}
// -------------------------
// SD helpers
// -------------------------
static const char* cardTypeToString(uint8_t type) {
switch (type) {
case CARD_MMC: return "MMC";
case CARD_SD: return "SDSC";
case CARD_SDHC: return "SDHC/SDXC";
default: return "UNKNOWN";
}
}
static bool tryMountWithBus(SPIClass& bus, const char* busName, uint32_t hz, bool verbose) {
SD.end();
bus.end();
delay(10);
forceSpiDeselected();
bus.begin(tbeam_supreme::sdSck(), tbeam_supreme::sdMiso(), tbeam_supreme::sdMosi(), tbeam_supreme::sdCs());
// SD SPI "idle clocks" ritual: CS HIGH + >= 74 clocks with MOSI high (0xFF).
digitalWrite(tbeam_supreme::sdCs(), HIGH);
delay(2);
for (int i = 0; i < 10; i++) {
bus.transfer(0xFF); // 80 clocks total
}
delay(2);
dumpSdPins("after-idle-clocks");
if (verbose) {
logf("SD: trying bus=%s freq=%lu Hz", busName, (unsigned long)hz);
}
if (!SD.begin(tbeam_supreme::sdCs(), bus, hz)) {
if (verbose) logf("SD: mount failed (possible non-FAT format, power, or bus issue)");
return false;
}
uint8_t cardType = SD.cardType();
if (cardType == CARD_NONE) {
SD.end();
return false;
}
g_sdSpi = &bus;
g_sdBusName = busName;
g_sdFreq = hz;
return true;
}
static bool mountPreferred(bool verbose) {
return tryMountWithBus(sdSpiH, "HSPI", 400000, verbose);
}
static bool mountCardFullScan() {
const uint32_t freqs[] = {400000, 1000000, 4000000, 10000000};
for (uint8_t i = 0; i < (sizeof(freqs) / sizeof(freqs[0])); ++i) {
if (tryMountWithBus(sdSpiH, "HSPI", freqs[i], true)) {
logf("SD: card detected and mounted");
return true;
}
}
for (uint8_t i = 0; i < (sizeof(freqs) / sizeof(freqs[0])); ++i) {
if (tryMountWithBus(sdSpiF, "FSPI", freqs[i], true)) {
logf("SD: card detected and mounted");
return true;
}
}
logf("SD: begin() failed on all bus/frequency attempts");
logf(" likely card absent, bad format, pin mismatch, or hardware issue");
return false;
}
static void printCardInfo() {
uint8_t cardType = SD.cardType();
uint64_t cardSizeMB = SD.cardSize() / (1024ULL * 1024ULL);
uint64_t totalMB = SD.totalBytes() / (1024ULL * 1024ULL);
uint64_t usedMB = SD.usedBytes() / (1024ULL * 1024ULL);
logf("SD type: %s", cardTypeToString(cardType));
logf("SD size: %llu MB", cardSizeMB);
logf("FS total: %llu MB", totalMB);
logf("FS used : %llu MB", usedMB);
logf("SPI bus: %s @ %lu Hz", g_sdBusName, (unsigned long)g_sdFreq);
}
static bool ensureDirRecursive(const char* path) {
String full(path);
if (!full.startsWith("/")) full = "/" + full;
int start = 1;
while (start > 0 && start < (int)full.length()) {
int slash = full.indexOf('/', start);
String partial = (slash < 0) ? full : full.substring(0, slash);
if (!SD.exists(partial.c_str())) {
logf("Creating directory: %s", partial.c_str());
if (!SD.mkdir(partial.c_str())) {
logf("ERROR: mkdir failed for %s", partial.c_str());
return false;
}
}
if (slash < 0) break;
start = slash + 1;
}
return true;
}
static bool rewriteFile(const char* path, const char* payload) {
if (SD.exists(path)) {
logf("WARNING: %s exists ... erasing", path);
if (!SD.remove(path)) {
logf("ERROR: failed to erase %s", path);
return false;
}
}
File f = SD.open(path, FILE_WRITE);
if (!f) {
logf("ERROR: failed to create %s", path);
return false;
}
size_t wrote = f.println(payload);
f.close();
if (wrote == 0) {
logf("ERROR: write failed for %s", path);
return false;
}
logf("Wrote file: %s", path);
return true;
}
static void permissionsDemo(const char* path) {
logf("Permissions demo:");
logf(" SD/FAT does not support Unix permissions (chmod/chown).");
logf(" Access control is by open mode (FILE_READ/FILE_WRITE).");
File r = SD.open(path, FILE_READ);
if (!r) {
logf(" Could not open %s as FILE_READ", path);
return;
}
logf(" FILE_READ open succeeded, size=%u bytes", (unsigned)r.size());
size_t writeInReadMode = r.print("attempt write while opened read-only");
if (writeInReadMode == 0) {
logf(" As expected, write via FILE_READ handle was blocked.");
} else {
logf(" NOTE: write via FILE_READ returned %u (unexpected)", (unsigned)writeInReadMode);
}
r.close();
}
static bool verifyMountedCard() {
File root = SD.open("/", FILE_READ);
if (!root) return false;
root.close();
return true;
}
static void runCardWorkflow() {
printCardInfo();
if (!rewriteFile(kRootTestFile, kPayload)) {
logf("Watcher action: root file write failed");
return;
}
if (!ensureDirRecursive(kNestedDir)) {
logf("Watcher action: directory creation failed");
return;
}
if (!rewriteFile(kNestedTestFile, kPayload)) {
logf("Watcher action: nested file write failed");
return;
}
permissionsDemo(kRootTestFile);
}
// -------------------------
// Watcher state transitions
// -------------------------
static void setStateMounted() {
if (g_watchState != WatchState::MOUNTED) {
logf("EVENT: card inserted/mounted");
runCardWorkflow();
g_lastPeriodicActionMs = millis();
}
g_watchState = WatchState::MOUNTED;
}
static void setStateAbsent() {
if (g_watchState == WatchState::MOUNTED) {
logf("EVENT: card removed/unavailable");
} else if (g_watchState != WatchState::ABSENT) {
logf("EVENT: no card detected");
}
SD.end();
g_watchState = WatchState::ABSENT;
}
// -------------------------
// Arduino entry points
// -------------------------
void setup() {
Serial.begin(115200);
Serial.println("[WATCHER: startup]");
// De-select SPI devices immediately.
forceSpiDeselected();
dumpSdPins("very-early");
logf("Sleeping for %lu ms to allow Serial Monitor connection...", (unsigned long)STARTUP_SERIAL_DELAY_MS);
delay(STARTUP_SERIAL_DELAY_MS);
Serial.println();
Serial.println("==================================================");
Serial.println("Exercise 05: SD Card Watcher");
Serial.println("==================================================");
Serial.printf("Pins: CS=%d SCK=%d MISO=%d MOSI=%d\r\n",
tbeam_supreme::sdCs(), tbeam_supreme::sdSck(),
tbeam_supreme::sdMiso(), tbeam_supreme::sdMosi());
Serial.printf("PMU I2C: SDA1=%d SCL1=%d\r\n",
tbeam_supreme::i2cSda(), tbeam_supreme::i2cScl());
Serial.println("Note: SD must be FAT16/FAT32 for Arduino SD library.");
Serial.println();
initPmuForSdPower();
dumpPmu("post-pmu-init", g_pmu);
// Software equivalent of "remove/insert card".
cycleSdRail(g_pmu);
logf("Watcher: waiting %lu ms for SD rail/card stabilization", (unsigned long)kStartupWarmupMs);
delay(kStartupWarmupMs);
dumpSdPins("pre-warmup-mount");
bool warmMounted = false;
for (uint8_t i = 0; i < 3; ++i) {
if (mountPreferred(false)) {
warmMounted = true;
break;
}
delay(200);
}
if (warmMounted) {
logf("Watcher: startup warmup mount succeeded");
setStateMounted();
} else {
logf("Watcher: startup warmup did not mount card");
setStateAbsent();
}
}
void loop() {
const uint32_t now = millis();
const uint32_t pollInterval =
(g_watchState == WatchState::MOUNTED) ? kPollIntervalMountedMs : kPollIntervalAbsentMs;
if ((uint32_t)(now - g_lastPollMs) < pollInterval) {
delay(10);
return;
}
g_lastPollMs = now;
if (g_watchState == WatchState::MOUNTED) {
if (verifyMountedCard()) {
if ((uint32_t)(now - g_lastPeriodicActionMs) >= kPeriodicActionMs) {
logf("Watcher: periodic mounted check action");
runCardWorkflow();
g_lastPeriodicActionMs = now;
}
g_presentVotes = 0;
g_absentVotes = 0;
return;
}
// One immediate remount attempt prevents false removes on transient SPI errors.
if (mountPreferred(false) && verifyMountedCard()) {
g_presentVotes = 0;
g_absentVotes = 0;
return;
}
g_absentVotes++;
g_presentVotes = 0;
if (g_absentVotes >= kVotesToAbsent) {
setStateAbsent();
g_absentVotes = 0;
}
return;
}
// ABSENT/UNKNOWN state
bool mounted = mountPreferred(false);
if (!mounted && (uint32_t)(now - g_lastFullScanMs) >= kFullScanIntervalMs) {
g_lastFullScanMs = now;
logf("Watcher: preferred probe failed, running full scan");
mounted = mountCardFullScan();
}
if (mounted) {
g_presentVotes++;
g_absentVotes = 0;
if (g_presentVotes >= kVotesToPresent) {
setStateMounted();
g_presentVotes = 0;
}
} else {
g_absentVotes++;
g_presentVotes = 0;
if (g_absentVotes >= kVotesToAbsent) {
setStateAbsent();
g_absentVotes = 0;
}
}
}

View file

@ -0,0 +1,43 @@
## Exercise 06: RTC Check (PCF8563)
This exercise validates RTC read/write and power-off persistence on the T-Beam Supreme.
It:
- Initializes PMU + I2C bus used by RTC.
- Reads RTC at startup.
- Prints RTC every 10 seconds.
- Accepts serial commands:
- `show`
- `set YYYY-MM-DD HH:MM:SS`
- `help`
## Build
```bash
source /home/jlpoole/rnsenv/bin/activate
pio run -e node_a
```
## Upload
```bash
source /home/jlpoole/rnsenv/bin/activate
pio run -e node_a -t upload --upload-port /dev/ttyACM0
```
## Monitor
```bash
screen /dev/ttyACM0 115200
```
## Suggested Persistence Test
1. Set the RTC:
- `set 2026-02-14 17:30:00`
2. Confirm:
- `show`
3. Power off the unit for a few minutes.
4. Power on and run:
- `show`
5. Compare expected elapsed time vs RTC output.

View file

@ -0,0 +1,32 @@
; 20260214 ChatGPT
; $Id$
; $HeadURL$
[platformio]
default_envs = node_a
[env]
platform = espressif32
framework = arduino
board = esp32-s3-devkitc-1
monitor_speed = 115200
lib_deps =
lewisxhe/XPowersLib@0.3.3
build_flags =
-I ../../shared/boards
-I ../../external/microReticulum_Firmware
-D BOARD_MODEL=BOARD_TBEAM_S_V1
-D RTC_I2C_ADDR=0x51
-D ARDUINO_USB_MODE=1
-D ARDUINO_USB_CDC_ON_BOOT=1
[env:node_a]
build_flags =
${env.build_flags}
-D NODE_LABEL=\"A\"
[env:node_b]
build_flags =
${env.build_flags}
-D NODE_LABEL=\"B\"

View file

@ -0,0 +1,224 @@
// 20260214 ChatGPT
// $Id$
// $HeadURL$
#include <Arduino.h>
#include <Wire.h>
#include "tbeam_supreme_adapter.h"
#ifndef RTC_I2C_ADDR
#define RTC_I2C_ADDR 0x51
#endif
static XPowersLibInterface* g_pmu = nullptr;
static uint32_t g_logSeq = 0;
static uint32_t g_lastPrintMs = 0;
static String g_cmdBuf;
static bool g_lastWasCR = 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 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 void logf(const char* fmt, ...) {
char msg[196];
va_list args;
va_start(args, fmt);
vsnprintf(msg, sizeof(msg), fmt, args);
va_end(args);
Serial.printf("[%10lu][%06lu] %s\r\n", (unsigned long)millis(), (unsigned long)g_logSeq++, msg);
}
static bool initPmuForRtc() {
return tbeam_supreme::initPmuForPeripherals(g_pmu, &Serial);
}
static bool rtcRead(RtcDateTime& out, bool& lowVoltageFlag) {
Wire1.beginTransmission(RTC_I2C_ADDR);
Wire1.write(0x02); // seconds register
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 rtcWrite(const RtcDateTime& in) {
bool century = in.year < 2000;
uint8_t yy = (uint8_t)(in.year % 100);
uint8_t monthReg = toBcd(in.month) & 0x1FU;
if (century) {
monthReg |= 0x80U;
}
Wire1.beginTransmission(RTC_I2C_ADDR);
Wire1.write(0x02); // seconds register
Wire1.write(toBcd(in.second) & 0x7FU);
Wire1.write(toBcd(in.minute) & 0x7FU);
Wire1.write(toBcd(in.hour) & 0x3FU);
Wire1.write(toBcd(in.day) & 0x3FU);
Wire1.write(toBcd(in.weekday) & 0x07U);
Wire1.write(monthReg);
Wire1.write(toBcd(yy));
return Wire1.endTransmission() == 0;
}
static bool parseSetCommand(const String& line, RtcDateTime& dt) {
int y, mo, d, h, mi, s;
if (sscanf(line.c_str(), "set %d-%d-%d %d:%d:%d", &y, &mo, &d, &h, &mi, &s) != 6) {
return false;
}
if (y < 1900 || y > 2099) return false;
if (mo < 1 || mo > 12) return false;
if (d < 1 || d > 31) return false;
if (h < 0 || h > 23) return false;
if (mi < 0 || mi > 59) return false;
if (s < 0 || s > 59) return false;
dt.year = (uint16_t)y;
dt.month = (uint8_t)mo;
dt.day = (uint8_t)d;
dt.hour = (uint8_t)h;
dt.minute = (uint8_t)mi;
dt.second = (uint8_t)s;
dt.weekday = 0; // Not critical for this persistence check.
return true;
}
static void printRtcNow(const char* label) {
RtcDateTime now{};
bool lowV = false;
if (!rtcRead(now, lowV)) {
logf("%s: RTC read failed", label);
return;
}
logf("%s: %04u-%02u-%02u %02u:%02u:%02u weekday=%u%s",
label,
(unsigned)now.year, (unsigned)now.month, (unsigned)now.day,
(unsigned)now.hour, (unsigned)now.minute, (unsigned)now.second,
(unsigned)now.weekday,
lowV ? " [LOW_VOLTAGE_FLAG]" : "");
}
static void handleCommand(const String& raw) {
String line = raw;
line.trim();
if (line.length() == 0) {
return;
}
if (line == "help") {
logf("Commands:");
logf(" show");
logf(" set YYYY-MM-DD HH:MM:SS");
logf(" help");
return;
}
if (line == "show") {
printRtcNow("RTC");
return;
}
RtcDateTime dt{};
if (parseSetCommand(line, dt)) {
if (rtcWrite(dt)) {
logf("RTC set succeeded");
printRtcNow("RTC");
} else {
logf("RTC set failed");
}
return;
}
logf("Unknown command: %s", line.c_str());
}
void setup() {
Serial.begin(115200);
Serial.println("Sleeping for 5 seconds to allow Serial Monitor connection...");
delay(5000);
Serial.println("\r\n==================================================");
Serial.println("Exercise 06: RTC check (PCF8563)");
Serial.println("==================================================");
Serial.printf("RTC I2C: SDA1=%d SCL1=%d ADDR=0x%02X\r\n",
tbeam_supreme::i2cSda(), tbeam_supreme::i2cScl(), RTC_I2C_ADDR);
initPmuForRtc();
logf("Type 'help' for commands.");
logf("Power-off persistence test:");
logf(" 1) set time");
logf(" 2) power off for a few minutes");
logf(" 3) power on and run 'show' before any GPS sync");
printRtcNow("RTC startup");
}
void loop() {
while (Serial.available() > 0) {
char c = (char)Serial.read();
if (c == '\r' || c == '\n') {
// Handle CR, LF, and CRLF/LFCR cleanly as one line ending.
if ((c == '\n' && g_lastWasCR) || (c == '\r' && !g_lastWasCR && g_cmdBuf.length() == 0)) {
g_lastWasCR = (c == '\r');
continue;
}
handleCommand(g_cmdBuf);
g_cmdBuf = "";
g_lastWasCR = (c == '\r');
} else {
g_lastWasCR = false;
g_cmdBuf += c;
if (g_cmdBuf.length() > 120) {
g_cmdBuf = "";
logf("Input line too long, buffer cleared");
}
}
}
uint32_t nowMs = millis();
if ((uint32_t)(nowMs - g_lastPrintMs) >= 10000) {
g_lastPrintMs = nowMs;
printRtcNow("RTC periodic");
}
}

View file

@ -0,0 +1,51 @@
## Exercise 07: SD Startup Watcher
This exercise is derived from `Exercise 05` and keeps that original exercise intact.
The focus here is isolating reusable SD startup and hot-insert watcher logic into a library-style structure.
This exercise now has two parts:
1. A reusable SD startup/watcher library in `lib/startup_sd`.
2. A harness app in `src/main.cpp` that demonstrates how to use that library.
Watcher behavior:
1. Initializes PMU and enables SD power rail (AXP2101 BLDO1).
2. Polls for card changes with debounced state transitions.
3. Emits events only on change:
- `EVENT: card inserted/mounted`
- `EVENT: card removed/unavailable`
- `EVENT: no card detected`
4. On mount event, emits callback status (`SdEvent`) and runs SD write workflow.
5. Every 15 seconds while mounted, runs a periodic write/permission check.
6. Uses fast preferred probe (`HSPI @ 400k`) and occasional full fallback scan.
Status callback usage:
- `SdEvent::NO_CARD` -> show "Missing SD card / Please insert card to proceed"
- `SdEvent::CARD_MOUNTED` -> card ready
- `SdEvent::CARD_REMOVED` -> card removed, wait for insert
Files used in this exercise:
- `/Exercise_07_test.txt`
- `/test/testsub1/testsubsub1/Exercise_07_test.txt`
## Build
```bash
source /home/jlpoole/rnsenv/bin/activate
pio run -e node_a
```
## Upload
```bash
source /home/jlpoole/rnsenv/bin/activate
pio run -e node_a -t upload --upload-port /dev/ttyACM0
```
## Monitor
```bash
screen /dev/ttyACM0 115200
```

View file

@ -0,0 +1,346 @@
#include "StartupSdManager.h"
#include <stdarg.h>
#include "driver/gpio.h"
StartupSdManager::StartupSdManager(Print& serial) : serial_(serial) {}
bool StartupSdManager::begin(const SdWatcherConfig& cfg, SdStatusCallback callback) {
cfg_ = cfg;
callback_ = callback;
forceSpiDeselected();
dumpSdPins("very-early");
if (!initPmuForSdPower()) {
return false;
}
cycleSdRail();
delay(cfg_.startupWarmupMs);
bool warmMounted = false;
for (uint8_t i = 0; i < 3; ++i) {
if (mountPreferred(false)) {
warmMounted = true;
break;
}
delay(200);
}
if (warmMounted) {
setStateMounted();
} else {
setStateAbsent();
}
return true;
}
void StartupSdManager::update() {
const uint32_t now = millis();
const uint32_t pollInterval =
(watchState_ == SdWatchState::MOUNTED) ? cfg_.pollIntervalMountedMs : cfg_.pollIntervalAbsentMs;
if ((uint32_t)(now - lastPollMs_) < pollInterval) {
return;
}
lastPollMs_ = now;
if (watchState_ == SdWatchState::MOUNTED) {
if (verifyMountedCard()) {
presentVotes_ = 0;
absentVotes_ = 0;
return;
}
if (mountPreferred(false) && verifyMountedCard()) {
presentVotes_ = 0;
absentVotes_ = 0;
return;
}
absentVotes_++;
presentVotes_ = 0;
if (absentVotes_ >= cfg_.votesToAbsent) {
setStateAbsent();
absentVotes_ = 0;
}
return;
}
bool mounted = mountPreferred(false);
if (!mounted && (uint32_t)(now - lastFullScanMs_) >= cfg_.fullScanIntervalMs) {
lastFullScanMs_ = now;
logf("Watcher: preferred probe failed, running full scan");
mounted = mountCardFullScan();
}
if (mounted) {
presentVotes_++;
absentVotes_ = 0;
if (presentVotes_ >= cfg_.votesToPresent) {
setStateMounted();
presentVotes_ = 0;
}
} else {
absentVotes_++;
presentVotes_ = 0;
if (absentVotes_ >= cfg_.votesToAbsent) {
setStateAbsent();
absentVotes_ = 0;
}
}
}
bool StartupSdManager::consumeMountedEvent() {
bool out = mountedEventPending_;
mountedEventPending_ = false;
return out;
}
bool StartupSdManager::consumeRemovedEvent() {
bool out = removedEventPending_;
removedEventPending_ = false;
return out;
}
void StartupSdManager::logf(const char* fmt, ...) {
char msg[196];
va_list args;
va_start(args, fmt);
vsnprintf(msg, sizeof(msg), fmt, args);
va_end(args);
serial_.printf("[%10lu][%06lu] %s\r\n",
(unsigned long)millis(),
(unsigned long)logSeq_++,
msg);
}
void StartupSdManager::notify(SdEvent event, const char* message) {
if (callback_ != nullptr) {
callback_(event, message);
}
}
void StartupSdManager::forceSpiDeselected() {
pinMode(tbeam_supreme::sdCs(), OUTPUT);
digitalWrite(tbeam_supreme::sdCs(), HIGH);
pinMode(tbeam_supreme::imuCs(), OUTPUT);
digitalWrite(tbeam_supreme::imuCs(), HIGH);
}
void StartupSdManager::dumpSdPins(const char* tag) {
if (!cfg_.enablePinDumps) {
(void)tag;
return;
}
const gpio_num_t cs = (gpio_num_t)tbeam_supreme::sdCs();
const gpio_num_t sck = (gpio_num_t)tbeam_supreme::sdSck();
const gpio_num_t miso = (gpio_num_t)tbeam_supreme::sdMiso();
const gpio_num_t mosi = (gpio_num_t)tbeam_supreme::sdMosi();
logf("PINS(%s): CS=%d SCK=%d MISO=%d MOSI=%d",
tag, gpio_get_level(cs), gpio_get_level(sck), gpio_get_level(miso), gpio_get_level(mosi));
}
bool StartupSdManager::initPmuForSdPower() {
if (!tbeam_supreme::initPmuForPeripherals(pmu_, &serial_)) {
logf("ERROR: PMU init failed");
return false;
}
return true;
}
void StartupSdManager::cycleSdRail(uint32_t offMs, uint32_t onSettleMs) {
if (!cfg_.enableSdRailCycle) {
return;
}
if (!pmu_) {
logf("SD rail cycle skipped: pmu=null");
return;
}
forceSpiDeselected();
pmu_->disablePowerOutput(XPOWERS_BLDO1);
delay(offMs);
pmu_->setPowerChannelVoltage(XPOWERS_BLDO1, 3300);
pmu_->enablePowerOutput(XPOWERS_BLDO1);
delay(onSettleMs);
}
bool StartupSdManager::tryMountWithBus(SPIClass& bus, const char* busName, uint32_t hz, bool verbose) {
SD.end();
bus.end();
delay(10);
forceSpiDeselected();
bus.begin(tbeam_supreme::sdSck(), tbeam_supreme::sdMiso(), tbeam_supreme::sdMosi(), tbeam_supreme::sdCs());
digitalWrite(tbeam_supreme::sdCs(), HIGH);
delay(2);
for (int i = 0; i < 10; i++) {
bus.transfer(0xFF);
}
delay(2);
if (verbose) {
logf("SD: trying bus=%s freq=%lu Hz", busName, (unsigned long)hz);
}
if (!SD.begin(tbeam_supreme::sdCs(), bus, hz)) {
if (verbose) {
logf("SD: mount failed");
}
return false;
}
if (SD.cardType() == CARD_NONE) {
SD.end();
return false;
}
sdSpi_ = &bus;
sdBusName_ = busName;
sdFreq_ = hz;
return true;
}
bool StartupSdManager::mountPreferred(bool verbose) {
return tryMountWithBus(sdSpiH_, "HSPI", 400000, verbose);
}
bool StartupSdManager::mountCardFullScan() {
const uint32_t freqs[] = {400000, 1000000, 4000000, 10000000};
for (uint8_t i = 0; i < (sizeof(freqs) / sizeof(freqs[0])); ++i) {
if (tryMountWithBus(sdSpiH_, "HSPI", freqs[i], true)) {
logf("SD: card detected and mounted");
return true;
}
}
for (uint8_t i = 0; i < (sizeof(freqs) / sizeof(freqs[0])); ++i) {
if (tryMountWithBus(sdSpiF_, "FSPI", freqs[i], true)) {
logf("SD: card detected and mounted");
return true;
}
}
logf("SD: begin() failed on all bus/frequency attempts");
return false;
}
bool StartupSdManager::verifyMountedCard() {
File root = SD.open("/", FILE_READ);
if (!root) {
return false;
}
root.close();
return true;
}
const char* StartupSdManager::cardTypeToString(uint8_t type) {
switch (type) {
case CARD_MMC:
return "MMC";
case CARD_SD:
return "SDSC";
case CARD_SDHC:
return "SDHC/SDXC";
default:
return "UNKNOWN";
}
}
void StartupSdManager::printCardInfo() {
uint8_t cardType = SD.cardType();
uint64_t cardSizeMB = SD.cardSize() / (1024ULL * 1024ULL);
uint64_t totalMB = SD.totalBytes() / (1024ULL * 1024ULL);
uint64_t usedMB = SD.usedBytes() / (1024ULL * 1024ULL);
logf("SD type: %s", cardTypeToString(cardType));
logf("SD size: %llu MB", cardSizeMB);
logf("FS total: %llu MB", totalMB);
logf("FS used : %llu MB", usedMB);
logf("SPI bus: %s @ %lu Hz", sdBusName_, (unsigned long)sdFreq_);
}
bool StartupSdManager::ensureDirRecursive(const char* path) {
String full(path);
if (!full.startsWith("/")) {
full = "/" + full;
}
int start = 1;
while (start > 0 && start < (int)full.length()) {
int slash = full.indexOf('/', start);
String partial = (slash < 0) ? full : full.substring(0, slash);
if (!SD.exists(partial.c_str()) && !SD.mkdir(partial.c_str())) {
logf("ERROR: mkdir failed for %s", partial.c_str());
return false;
}
if (slash < 0) {
break;
}
start = slash + 1;
}
return true;
}
bool StartupSdManager::rewriteFile(const char* path, const char* payload) {
if (SD.exists(path) && !SD.remove(path)) {
logf("ERROR: failed to erase %s", path);
return false;
}
File f = SD.open(path, FILE_WRITE);
if (!f) {
logf("ERROR: failed to create %s", path);
return false;
}
size_t wrote = f.println(payload);
f.close();
if (wrote == 0) {
logf("ERROR: write failed for %s", path);
return false;
}
return true;
}
void StartupSdManager::permissionsDemo(const char* path) {
logf("Permissions demo: FAT has no Unix chmod/chown, use open mode only.");
File r = SD.open(path, FILE_READ);
if (!r) {
logf("Could not open %s as FILE_READ", path);
return;
}
size_t writeInReadMode = r.print("attempt write while opened read-only");
if (writeInReadMode == 0) {
logf("As expected, FILE_READ write was blocked.");
} else {
logf("NOTE: FILE_READ write returned %u (unexpected)", (unsigned)writeInReadMode);
}
r.close();
}
void StartupSdManager::setStateMounted() {
if (watchState_ != SdWatchState::MOUNTED) {
logf("EVENT: card inserted/mounted");
mountedEventPending_ = true;
notify(SdEvent::CARD_MOUNTED, "SD card mounted");
}
watchState_ = SdWatchState::MOUNTED;
}
void StartupSdManager::setStateAbsent() {
if (watchState_ == SdWatchState::MOUNTED) {
logf("EVENT: card removed/unavailable");
removedEventPending_ = true;
notify(SdEvent::CARD_REMOVED, "SD card removed");
} else if (watchState_ != SdWatchState::ABSENT) {
logf("EVENT: no card detected");
notify(SdEvent::NO_CARD, "Missing SD card / Please insert card to proceed");
}
SD.end();
watchState_ = SdWatchState::ABSENT;
}

View file

@ -0,0 +1,87 @@
#pragma once
#include <Arduino.h>
#include <SD.h>
#include <SPI.h>
#include <Wire.h>
#include "tbeam_supreme_adapter.h"
enum class SdWatchState : uint8_t {
UNKNOWN = 0,
ABSENT,
MOUNTED
};
enum class SdEvent : uint8_t {
NO_CARD,
CARD_MOUNTED,
CARD_REMOVED
};
using SdStatusCallback = void (*)(SdEvent event, const char* message);
struct SdWatcherConfig {
bool enableSdRailCycle = true;
bool enablePinDumps = true;
uint32_t startupWarmupMs = 1500;
uint32_t pollIntervalAbsentMs = 1000;
uint32_t pollIntervalMountedMs = 2000;
uint32_t fullScanIntervalMs = 10000;
uint8_t votesToPresent = 2;
uint8_t votesToAbsent = 5;
};
class StartupSdManager {
public:
explicit StartupSdManager(Print& serial = Serial);
bool begin(const SdWatcherConfig& cfg, SdStatusCallback callback = nullptr);
void update();
bool isMounted() const { return watchState_ == SdWatchState::MOUNTED; }
SdWatchState state() const { return watchState_; }
bool consumeMountedEvent();
bool consumeRemovedEvent();
void printCardInfo();
bool ensureDirRecursive(const char* path);
bool rewriteFile(const char* path, const char* payload);
void permissionsDemo(const char* path);
private:
void logf(const char* fmt, ...);
void notify(SdEvent event, const char* message);
void forceSpiDeselected();
void dumpSdPins(const char* tag);
bool initPmuForSdPower();
void cycleSdRail(uint32_t offMs = 250, uint32_t onSettleMs = 600);
bool tryMountWithBus(SPIClass& bus, const char* busName, uint32_t hz, bool verbose);
bool mountPreferred(bool verbose);
bool mountCardFullScan();
bool verifyMountedCard();
const char* cardTypeToString(uint8_t type);
void setStateMounted();
void setStateAbsent();
Print& serial_;
SdWatcherConfig cfg_{};
SdStatusCallback callback_ = nullptr;
SPIClass sdSpiH_{HSPI};
SPIClass sdSpiF_{FSPI};
SPIClass* sdSpi_ = nullptr;
const char* sdBusName_ = "none";
uint32_t sdFreq_ = 0;
XPowersLibInterface* pmu_ = nullptr;
SdWatchState watchState_ = SdWatchState::UNKNOWN;
uint8_t presentVotes_ = 0;
uint8_t absentVotes_ = 0;
uint32_t lastPollMs_ = 0;
uint32_t lastFullScanMs_ = 0;
uint32_t logSeq_ = 0;
bool mountedEventPending_ = false;
bool removedEventPending_ = false;
};

View file

@ -0,0 +1,12 @@
{
"name": "startup_sd",
"version": "0.1.0",
"dependencies": [
{
"name": "XPowersLib"
},
{
"name": "Wire"
}
]
}

View file

@ -0,0 +1,37 @@
; 20260213 ChatGPT
; $Id$
; $HeadURL$
[platformio]
default_envs = node_a
[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
; SD pins based on T-Beam S3 core pin mapping
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:node_a]
build_flags =
${env.build_flags}
-D NODE_LABEL=\"A\"
[env:node_b]
build_flags =
${env.build_flags}
-D NODE_LABEL=\"B\"

View file

@ -0,0 +1,135 @@
// 20260215 ChatGPT
// $Id$
// $HeadURL$
#include <Arduino.h>
#include <Wire.h>
#include <U8g2lib.h>
#include "StartupSdManager.h"
#include "tbeam_supreme_adapter.h"
#define STARTUP_SERIAL_DELAY_MS 5000
#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 const char* kRootTestFile = "/Exercise_07_test.txt";
static const char* kNestedDir = "/test/testsub1/testsubsub1";
static const char* kNestedTestFile = "/test/testsub1/testsubsub1/Exercise_07_test.txt";
static const char* kPayload = "This is a test";
static const uint32_t kPeriodicActionMs = 15000;
static U8G2_SH1106_128X64_NONAME_F_HW_I2C g_oled(U8G2_R0, /* reset=*/U8X8_PIN_NONE);
static StartupSdManager g_sd(Serial);
static uint32_t g_lastPeriodicActionMs = 0;
static void oledShow3(const char* l1, const char* l2 = nullptr, const char* l3 = nullptr) {
g_oled.clearBuffer();
g_oled.setFont(u8g2_font_6x10_tf);
if (l1) g_oled.drawUTF8(0, 16, l1);
if (l2) g_oled.drawUTF8(0, 32, l2);
if (l3) g_oled.drawUTF8(0, 48, l3);
g_oled.sendBuffer();
}
static void onSdStatus(SdEvent event, const char* message) {
Serial.printf("[SD-STATUS] %s\r\n", message);
if (event == SdEvent::NO_CARD) {
oledShow3("Missing SD card", "Please insert card", "to proceed");
} else if (event == SdEvent::CARD_MOUNTED) {
oledShow3("SD card ready", "Mounted OK");
} else if (event == SdEvent::CARD_REMOVED) {
oledShow3("SD card removed", "Please re-insert");
}
}
static void runCardWorkflow() {
g_sd.printCardInfo();
if (!g_sd.rewriteFile(kRootTestFile, kPayload)) {
Serial.println("Watcher action: root file write failed");
return;
}
if (!g_sd.ensureDirRecursive(kNestedDir)) {
Serial.println("Watcher action: directory creation failed");
return;
}
if (!g_sd.rewriteFile(kNestedTestFile, kPayload)) {
Serial.println("Watcher action: nested file write failed");
return;
}
g_sd.permissionsDemo(kRootTestFile);
}
void setup() {
Serial.begin(115200);
Wire.begin(OLED_SDA, OLED_SCL);
g_oled.setI2CAddress(OLED_ADDR << 1);
g_oled.begin();
oledShow3("Exercise 07", "SD startup watcher", "Booting...");
Serial.println("[WATCHER: startup]");
Serial.printf("Sleeping for %lu ms to allow Serial Monitor connection...\r\n",
(unsigned long)STARTUP_SERIAL_DELAY_MS);
delay(STARTUP_SERIAL_DELAY_MS);
Serial.println();
Serial.println("==================================================");
Serial.println("Exercise 07: SD Startup Watcher (Library Harness)");
Serial.println("==================================================");
Serial.printf("Pins: CS=%d SCK=%d MISO=%d MOSI=%d\r\n",
tbeam_supreme::sdCs(), tbeam_supreme::sdSck(),
tbeam_supreme::sdMiso(), tbeam_supreme::sdMosi());
Serial.printf("PMU I2C: SDA1=%d SCL1=%d\r\n",
tbeam_supreme::i2cSda(), tbeam_supreme::i2cScl());
Serial.println("Note: SD must be FAT16/FAT32 for Arduino SD library.\r\n");
SdWatcherConfig cfg;
cfg.enableSdRailCycle = true;
cfg.enablePinDumps = true;
cfg.startupWarmupMs = 1500;
cfg.pollIntervalAbsentMs = 1000;
cfg.pollIntervalMountedMs = 2000;
cfg.fullScanIntervalMs = 10000;
cfg.votesToPresent = 2;
cfg.votesToAbsent = 5;
if (!g_sd.begin(cfg, onSdStatus)) {
Serial.println("ERROR: SD watcher init failed");
}
if (g_sd.isMounted()) {
runCardWorkflow();
g_lastPeriodicActionMs = millis();
}
}
void loop() {
g_sd.update();
if (g_sd.consumeMountedEvent()) {
runCardWorkflow();
g_lastPeriodicActionMs = millis();
}
if (g_sd.consumeRemovedEvent()) {
Serial.println("SD removed, waiting for re-insert...");
}
const uint32_t now = millis();
if (g_sd.isMounted() && (uint32_t)(now - g_lastPeriodicActionMs) >= kPeriodicActionMs) {
Serial.println("Watcher: periodic mounted check action");
runCardWorkflow();
g_lastPeriodicActionMs = now;
}
delay(10);
}

92
exercises/README.md Normal file
View file

@ -0,0 +1,92 @@
## Buttons
There are three buttons
![T-Beam SUPREME](../img/20260212_182225_Thu.png)
![](../img/20260212_182300_Thu.png)
Listed in order from Antenna to USB port:
1) RESET
2) POWER
3) BOOT
![](../img/20260212_184521_Thu.png)
## RESET
The RESET button only needs to be depressed for less than 1 second. Doing so will cause the unit to reboot. If you have a "screen" monitor on the USB device, it will be disconnected. A good way to capture boot posting messages from within the unit is to have your command in a console ready to execute by depressing the RETURN key and you depress RETURN immediately after you depress the unit's RESET button, possibly allowing 1/2 second to allow kernel to recognize the new USB device, i.e. /dev/ttyACM0
## POWER
If your unit is powered OFF, simply depress the POWER button will start the unit. If the unit is powered ON, then depressing the POWER button and holding it down for 6 second will cause the unit to power down. The OLED display will go dark when the unit has successfully been powered down.
## BOOT
A) TODO: what happens when unit is powered ON and you depress BOOT?
B) TODO: what happens when unit is powered OFF and you depress BOOT?
C) To upload new binary image: depress the BOOT button and hold down, then while holding down the BOOT button disconnect the USB line and then reconnect the USB line and then lift up on the BOOT button. This sequence will cause the unit to await an upload of an image over USB to be stored in its FLASH memory. You can place the unit in this mode and then several minutes later perform the upload from your console, the unit will duly await an upload session. See further how to flash a new image in the section "Flashing Binary". Note: although the uploader may state it is trying to reset after successful installation of the new image, it seems that software attempt does not work and you have to manually depress the unit's RESET button to force a RESET. Remember, if you want to see the boot's posting, you should have your terminal ready to run "screen" so you capture the initial postings after you click RESET.
# Button Protocol For Uploading New Image
Pushing BOOT when the unit is currently running will not do anything. You have to shut the unit down first. So:
1) Depress POWER button for 6 seconds
2) Depress BOOT button and while depressed, click RESET button
3) Lift up BOOT button
The unit is now waiting for the serial console so commence an upload. You are not time restricted, so you can go through the above 3 steps and then 5 minutes later proceed with an upload in the command console.
# Exercises
These are progressve tests you can run to confirm how to access the unit's functionality and validate your workbench. Each exercise has it's own README.md
Exercise 00: 00_usb_radio_check
Exercise 01: ASCII ping-pong over LoRa (serial only). Using two units, each sends a message to the other and you monitor both units through two screen consoles.
Exercise 02: Add OLED display
Exercise 03: Add SD logging
Exercise 04: Replace ASCII payload with microR packets
Exercise 05: SD provisioning with identity.bin, peer list, beacon
Exercise 06: RTC check (PCF8563) read/set and persistence validation
Exercise 07: SD startup watcher library harness with hot-insert detection
Each exercise is self-contained:
its own platformio.ini
its own src/main.cpp
its own README.md with exact commands
To clean:
pio run -t clean
Nuclear option:
rm -rf .pio
Rebuild:
pio run -e node_a
pio run -e node_b
## Upload existing firmware (will not recompile if unchanged)
### Upload node A (set your port)
pio run -e node_a -t upload --upload-port /dev/ttyACM0
### Upload node B
pio run -e node_b -t upload --upload-port /dev/ttyACM1
To monitor both:
screen /dev/ttyACM0 115200
screen /dev/ttyACM1 115200
Attach antennas (transmitting without antenna will cause power feedback and likely destroy your circuits)
Keep boards at least ~30 cm apart so you dont desense the receivers.
shared board pin headers in one place:
shared/
boards/
tbeam-s3-core_pins.h
# Features
## SD Card
You can read and write to SD cards that are FAT formatted. (Can they be Linux formatted?)
## Real Time Clock ("RTC")
## Microphone

1
external/ArxContainer vendored Submodule

@ -0,0 +1 @@
Subproject commit d6affcd0bc83219b863c20abf7c269214db8db2a

1
external/ArxTypeTraits vendored Submodule

@ -0,0 +1 @@
Subproject commit 702de9cc59c7e047cdc169ae3547718b289d2c02

1
external/DebugLog vendored Submodule

@ -0,0 +1 @@
Subproject commit b581f7dde6c276c5df684e2328f406d9754d2f46

1
external/microReticulum_Firmware vendored Submodule

@ -0,0 +1 @@
Subproject commit 5dc607fc7227c46ccb19244e456782fbb7775eae

View file

@ -0,0 +1,27 @@
; platformio.ini
; 20260212 ChatGPT
; $Id$
; $HeadURL$
[platformio]
default_envs = tbeam_supreme
[env:tbeam_supreme]
platform = espressif32
framework = arduino
board = esp32-s3-devkitc-1 ; <-- change to your exact board if needed
monitor_speed = 115200
; Pull in microReticulum from your repo tree
build_flags =
-I ../../external/microReticulum/src
-I ../../external/microReticulum_Firmware
-D BOARD_MODEL=BOARD_TBEAM_S_V1
-D FIELDTEST_BEACON=1
lib_deps =
bblanchon/ArduinoJson@~7.4.2
hideakitai/MsgPack@~0.4.2
rweather/Crypto@^0.4.0
; SD stack usually comes with Arduino core
; Add your LoRa radio library here (RadioLib, SX126x-Arduino, etc.)

View file

@ -0,0 +1,117 @@
// firmware/fieldtest_beacon/src/main.cpp
// 20260212 ChatGPT
// $Id$
// $HeadURL$
#include <Arduino.h>
#include <SPI.h>
#include <SD.h>
#include "Boards.h"
// Include microReticulum headers from your external tree
#include "Identity.h"
#include "Reticulum.h"
#include "Destination.h"
#include "Transport.h"
// ---------- User-tunable ----------
static const uint32_t BEACON_INTERVAL_MS = 15000;
static const char* PATH_IDENTITY_BIN = "/provisioning/identity.bin";
static const char* PATH_LABEL_TXT = "/provisioning/label.txt";
static const char* LOG_PATH = "/logs/fieldtest.log";
// ---------- Globals ----------
static uint32_t g_iter = 0;
static uint32_t g_next_tx = 0;
static File g_log;
// Source board pin mapping from microReticulum_Firmware board definitions.
static const int SD_CS_PIN = SD_CS;
// Simple line logger (append-only)
static void log_line(const String& line) {
if (!g_log) return;
g_log.println(line);
g_log.flush();
}
// Read whole file into a buffer
static bool read_file(const char* path, std::vector<uint8_t>& out) {
File f = SD.open(path, FILE_READ);
if (!f) return false;
size_t n = f.size();
out.resize(n);
if (n > 0) f.read(out.data(), n);
f.close();
return true;
}
static String read_text_file(const char* path) {
File f = SD.open(path, FILE_READ);
if (!f) return String("");
String s = f.readString();
f.close();
s.trim();
return s;
}
void setup() {
Serial.begin(115200);
delay(250);
// ---- SD init ----
if (!SD.begin(SD_CS_PIN)) {
Serial.println("SD init failed");
return;
}
g_log = SD.open(LOG_PATH, FILE_APPEND);
if (!g_log) {
Serial.println("Failed to open log file");
return;
}
const String label = read_text_file(PATH_LABEL_TXT);
log_line("BOOT\tlabel=" + label);
std::vector<uint8_t> id_bytes;
if (!read_file(PATH_IDENTITY_BIN, id_bytes)) {
log_line(String("ERROR\tmissing_identity\tpath=") + PATH_IDENTITY_BIN);
return;
}
log_line(String("IDENTITY_OK\tbytes=") + id_bytes.size());
// ---- Load Identity into microReticulum ----
// TODO: adjust to the actual API in your microReticulum version
// Example intent:
// Identity ident = Identity::fromBytes(id_bytes.data(), id_bytes.size());
// Reticulum rns;
// rns.setIdentity(ident);
// rns.begin(...);
//
// Also: initialize your LoRa Interface and attach it to Reticulum here.
log_line("RNS_INIT_OK");
g_next_tx = millis() + 3000; // wait a moment before first TX
}
void loop() {
// TODO: run microReticulum polling / tick function, and radio interface polling
const uint32_t now = millis();
if ((int32_t)(now - g_next_tx) >= 0) {
g_next_tx = now + BEACON_INTERVAL_MS;
// Build payload: timestamp + iterator (GPS later)
const uint32_t epoch_guess = (uint32_t) (time(nullptr)); // placeholder; GPS later
const String payload = String("t=") + epoch_guess + " i=" + g_iter++;
// TODO: send to peer(s) (Destination derived from peers list)
// bool ok = destination.send(payload_bytes, len);
log_line(String("TX\t") + payload);
}
delay(5);
}

BIN
img/20260212_182225_Thu.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 77 KiB

BIN
img/20260212_182300_Thu.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 448 KiB

BIN
img/20260212_184521_Thu.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

View file

@ -0,0 +1,42 @@
#ifndef Pins_Arduino_h
#define Pins_Arduino_h
#include <stdint.h>
#define USB_VID 0x303a
#define USB_PID 0x1001
// Now declared in .platformio/packages/framework-arduinoespressif32/cores/esp32/Arduino.h
// #define NUM_ANALOG_INPUTS 20
// #define EXTERNAL_NUM_INTERRUPTS 46
// #define NUM_DIGITAL_PINS 48
// #define analogInputToDigitalPin(p) (((p) < 20) ? (analogChannelToDigitalPin(p)) : -1)
// #define digitalPinToInterrupt(p) (((p) < 48) ? (p) : -1)
// #define digitalPinHasPWM(p) (p < 46)
static const uint8_t TX = 43;
static const uint8_t RX = 44;
// The default Wire will be mapped to PMU and RTC
static const uint8_t SDA = 42;
static const uint8_t SCL = 41;
// Default SPI will be mapped to Radio
static const uint8_t SS = 10;
static const uint8_t MOSI = 11;
static const uint8_t MISO = 13;
static const uint8_t SCK = 12;
// Another SPI bus shares SD card and QMI8653 inertial measurement sensor
#define SPI_MOSI (35)
#define SPI_SCK (36)
#define SPI_MISO (37)
#define SPI_CS (47)
#define IMU_CS (34)
#define SDCARD_CS SPI_CS
#define IMU_INT (33)
// #define PMU_IRQ (40)
#define RTC_INT (14)
#endif /* Pins_Arduino_h */

View file

@ -0,0 +1,26 @@
; The 1.0 release of the LilyGo TBEAM-S3-Core board
[env:tbeam-s3-core]
custom_meshtastic_hw_model = 12
custom_meshtastic_hw_model_slug = LILYGO_TBEAM_S3_CORE
custom_meshtastic_architecture = esp32-s3
custom_meshtastic_actively_supported = true
custom_meshtastic_support_level = 1
custom_meshtastic_display_name = LILYGO T-Beam Supreme
custom_meshtastic_images = tbeam-s3-core.svg
custom_meshtastic_tags = LilyGo
custom_meshtastic_requires_dfu = true
custom_meshtastic_partition_scheme = 8MB
extends = esp32s3_base
board = tbeam-s3-core
board_build.partitions = default_8MB.csv
board_check = true
lib_deps =
${esp32s3_base.lib_deps}
# renovate: datasource=custom.pio depName=SensorLib packageName=lewisxhe/library/SensorLib
lewisxhe/SensorLib@0.3.4
build_flags =
${esp32s3_base.build_flags}
-I variants/esp32s3/tbeam-s3-core

View file

@ -0,0 +1,6 @@
2/12/2026 copied from meshastic/firmware/variants/esp32s3/tbeam-s3-core
since that appeared in the build tree of a successfully operating binary:
jlpoole@jp /usr/local/src/meshtastic/firmware $ cd /usr/local/src/meshtastic/firmware
ls -1 .pio/build 2>/dev/null | egrep -i 'tbeam|t-beam|s3' | sort
tbeam-s3-core
jlpoole@jp /usr/local/src/meshtastic/firmware $

View file

@ -0,0 +1,11 @@
#include "RadioLib.h"
static const uint32_t rfswitch_dio_pins[] = {RADIOLIB_LR11X0_DIO5, RADIOLIB_LR11X0_DIO6, RADIOLIB_NC, RADIOLIB_NC, RADIOLIB_NC};
static const Module::RfSwitchMode_t rfswitch_table[] = {
// mode DIO5 DIO6
{LR11x0::MODE_STBY, {LOW, LOW}}, {LR11x0::MODE_RX, {HIGH, LOW}},
{LR11x0::MODE_TX, {LOW, HIGH}}, {LR11x0::MODE_TX_HP, {LOW, HIGH}},
{LR11x0::MODE_TX_HF, {LOW, LOW}}, {LR11x0::MODE_GNSS, {LOW, LOW}},
{LR11x0::MODE_WIFI, {LOW, LOW}}, END_OF_MODE_TABLE,
};

View file

@ -0,0 +1,79 @@
// #define BUTTON_NEED_PULLUP // if set we need to turn on the internal CPU pullup during sleep
#define I2C_SDA1 42 // Used for PMU management and PCF8563
#define I2C_SCL1 41 // Used for PMU management and PCF8563
#define I2C_SDA 17 // For QMC6310 sensors and screens
#define I2C_SCL 18 // For QMC6310 sensors and screens
#define BUTTON_PIN 0 // The middle button GPIO on the T-Beam S3
// #define EXT_NOTIFY_OUT 13 // Default pin to use for Ext Notify Module.
#define LED_STATE_ON 0 // State when LED is lit
// TTGO uses a common pinout for their SX1262 vs RF95 modules - both can be enabled and we will probe at runtime for RF95 and if
// not found then probe for SX1262
#define USE_SX1262
#define USE_SX1268
#define USE_LR1121
#define LORA_DIO0 -1 // a No connect on the SX1262 module
#define LORA_RESET 5
#define LORA_DIO1 1 // SX1262 IRQ
#define LORA_DIO2 4 // SX1262 BUSY
#define LORA_DIO3 // Not connected on PCB, but internally on the TTGO SX1262, if DIO3 is high the TXCO is enabled
#ifdef USE_SX1262
#define SX126X_CS 10 // FIXME - we really should define LORA_CS instead
#define SX126X_DIO1 LORA_DIO1
#define SX126X_BUSY LORA_DIO2
#define SX126X_RESET LORA_RESET
// Not really an E22 but TTGO seems to be trying to clone that
#define SX126X_DIO2_AS_RF_SWITCH
#define SX126X_DIO3_TCXO_VOLTAGE 1.8
// Internally the TTGO module hooks the SX1262-DIO2 in to control the TX/RX switch (which is the default for the sx1262interface
// code)
#endif
// LR1121
#ifdef USE_LR1121
#define LR1121_IRQ_PIN 1
#define LR1121_NRESET_PIN LORA_RESET
#define LR1121_BUSY_PIN 4
#define LR1121_SPI_NSS_PIN 10
#define LR1121_SPI_SCK_PIN 12
#define LR1121_SPI_MOSI_PIN 11
#define LR1121_SPI_MISO_PIN 13
#define LR11X0_DIO3_TCXO_VOLTAGE 3.0
#define LR11X0_DIO_AS_RF_SWITCH
#endif
// Leave undefined to disable our PMU IRQ handler. DO NOT ENABLE THIS because the pmuirq can cause sperious interrupts
// and waking from light sleep
// #define PMU_IRQ 40
#define HAS_AXP2101
// PCF8563 RTC Module
#define PCF8563_RTC 0x51
// Specify the PMU as Wire1. In the t-beam-s3 core, PCF8563 and PMU share the bus
#define PMU_USE_WIRE1
#define RTC_USE_WIRE1
#define LORA_SCK 12
#define LORA_MISO 13
#define LORA_MOSI 11
#define LORA_CS 10
#define GPS_RX_PIN 9
#define GPS_TX_PIN 8
#define GPS_WAKEUP_PIN 7
#define GPS_1PPS_PIN 6
#define HAS_SDCARD // Have SPI interface SD card slot
#define SDCARD_USE_SPI1
// has 32768 Hz crystal
#define HAS_32768HZ 1
#define USE_SH1106

View file

@ -0,0 +1,71 @@
#pragma once
#include <Arduino.h>
#include <Wire.h>
#include <XPowersLib.h>
#include "Boards.h"
namespace tbeam_supreme {
inline int i2cSda() { return I2C_SDA; }
inline int i2cScl() { return I2C_SCL; }
inline int sdSck() { return SD_CLK; }
inline int sdMiso() { return SD_MISO; }
inline int sdMosi() { return SD_MOSI; }
inline int sdCs() { return SD_CS; }
inline int imuCs() { return IMU_CS; }
inline bool initPmuForPeripherals(XPowersLibInterface*& pmu, Print* out = nullptr) {
if (BOARD_MODEL != BOARD_TBEAM_S_V1) {
if (out) out->println("PMU adapter: BOARD_MODEL is not T-Beam Supreme");
return false;
}
Wire1.begin(i2cSda(), i2cScl());
if (!pmu) {
pmu = new XPowersAXP2101(Wire1);
}
if (!pmu->init()) {
if (out) out->println("PMU adapter: AXP2101 init failed");
return false;
}
// Match microReticulum_Firmware tbeam supreme rail setup.
pmu->setPowerChannelVoltage(XPOWERS_ALDO4, 3300);
pmu->enablePowerOutput(XPOWERS_ALDO4);
pmu->setPowerChannelVoltage(XPOWERS_ALDO3, 3300);
pmu->enablePowerOutput(XPOWERS_ALDO3);
pmu->setPowerChannelVoltage(XPOWERS_DCDC3, 3300);
pmu->enablePowerOutput(XPOWERS_DCDC3);
pmu->setPowerChannelVoltage(XPOWERS_ALDO2, 3300);
pmu->enablePowerOutput(XPOWERS_ALDO2);
pmu->setPowerChannelVoltage(XPOWERS_ALDO1, 3300);
pmu->enablePowerOutput(XPOWERS_ALDO1);
pmu->setPowerChannelVoltage(XPOWERS_BLDO1, 3300);
pmu->enablePowerOutput(XPOWERS_BLDO1);
pmu->disablePowerOutput(XPOWERS_DCDC2);
pmu->disablePowerOutput(XPOWERS_DCDC5);
pmu->disablePowerOutput(XPOWERS_DLDO1);
pmu->disablePowerOutput(XPOWERS_DLDO2);
pmu->disablePowerOutput(XPOWERS_VBACKUP);
pmu->setChargeTargetVoltage(XPOWERS_AXP2101_CHG_VOL_4V2);
pmu->setChargerConstantCurr(XPOWERS_AXP2101_CHG_CUR_500MA);
pmu->disableIRQ(XPOWERS_AXP2101_ALL_IRQ);
pmu->clearIrqStatus();
pmu->disableTSPinMeasure();
pmu->enableVbusVoltageMeasure();
pmu->enableBattVoltageMeasure();
if (out) {
out->printf("PMU adapter: AXP2101 ready, BLDO1(SD)=%s\r\n",
pmu->isPowerChannelEnable(XPOWERS_BLDO1) ? "ON" : "OFF");
}
return pmu->isPowerChannelEnable(XPOWERS_BLDO1);
}
} // namespace tbeam_supreme

1
tools/CMakeLists.txt Normal file
View file

@ -0,0 +1 @@
add_subdirectory(keygen)

View file

@ -0,0 +1,24 @@
add_executable(rns-provision main.cpp)
# If microReticulum exports an include directory / target, this may be unnecessary.
# Keep this here as a pragmatic fallback:
target_include_directories(rns-provision PRIVATE
${CMAKE_SOURCE_DIR}/external/microReticulum/src
)
# ArduinoJson is pulled by microReticulum headers; add it to this tool's include path.
target_include_directories(rns-provision PRIVATE
${CMAKE_SOURCE_DIR}/external/microReticulum/src
# ArduinoJson headers:
# - ArduinoJson.h lives at the repo root
# - ArduinoJson/... lives under src/
${CMAKE_BINARY_DIR}/_deps/arduinojson-src
${CMAKE_BINARY_DIR}/_deps/arduinojson-src/src
)
# Link against the microReticulum library target.
# If this target name is wrong in your submodule, change it here.
target_link_libraries(rns-provision PRIVATE ReticulumStatic)
install(TARGETS rns-provision RUNTIME DESTINATION bin)

376
tools/keygen/main.cpp Normal file
View file

@ -0,0 +1,376 @@
// rns-provision - generate microReticulum identity keypairs + provisioning bundles
//
// Examples:
// ./rns-provision --quantity 6 --format tsv
// ./rns-provision -q 6 -f json --public
// ./rns-provision -q 6 --outdir provisioning/20260212_1030 --bundle both --public
//
// Notes:
// - Writes sensitive material (private keys). Protect your output directories.
// - identity.bin is raw private key bytes as returned by microReticulum Identity.
//
// $Header$
// $Id$
#include <Identity.h>
#include <chrono>
#include <cstdint>
#include <cstdlib>
#include <cstring>
#include <exception>
#include <fstream>
#include <iomanip>
#include <iostream>
#include <sstream>
#include <stdexcept>
#include <string>
#include <vector>
#if __has_include(<filesystem>)
#include <filesystem>
namespace fs = std::filesystem;
#else
#error "This tool requires <filesystem> (C++17)."
#endif
#ifndef _WIN32
#include <sys/stat.h>
#endif
static constexpr const char* TOOL_NAME = "rns-provision";
static constexpr const char* TOOL_VERSION = "0.1.0";
static void usage(const char* argv0, bool full = true) {
std::cerr << TOOL_NAME << " " << TOOL_VERSION << "\n\n";
std::cerr
<< "Usage:\n"
<< " " << argv0 << " --quantity N [options]\n\n";
if (!full) return;
std::cerr
<< "Options:\n"
<< " -q, --quantity N Number of identities to generate (required)\n"
<< " -f, --format FMT Stdout format: tsv (default) or json\n"
<< " --public Include public_key in stdout and identity.json\n"
<< " --outdir DIR Write provisioning bundle to DIR\n"
<< " --bundle MODE none|json|bin|both (default: both if --outdir)\n"
<< " --prefix NAME Device directory prefix (default: device)\n"
<< " -h, --help Show this help message and exit\n"
<< "\n"
<< "Examples:\n"
<< " " << argv0 << " -q 6\n"
<< " " << argv0 << " -q 6 --outdir provisioning/20260212_1030 --bundle both\n"
<< "\n";
}
static bool is_flag(const std::string& a, const char* s) { return a == s; }
static std::string utc_now_iso8601() {
using namespace std::chrono;
auto now = system_clock::now();
std::time_t t = system_clock::to_time_t(now);
std::tm tm{};
#ifdef _WIN32
gmtime_s(&tm, &t);
#else
gmtime_r(&t, &tm);
#endif
std::ostringstream oss;
oss << std::put_time(&tm, "%Y-%m-%dT%H:%M:%SZ");
return oss.str();
}
static std::string json_escape(const std::string& s) {
std::ostringstream o;
for (unsigned char c : s) {
switch (c) {
case '\"': o << "\\\""; break;
case '\\': o << "\\\\"; break;
case '\b': o << "\\b"; break;
case '\f': o << "\\f"; break;
case '\n': o << "\\n"; break;
case '\r': o << "\\r"; break;
case '\t': o << "\\t"; break;
default:
if (c < 0x20) {
o << "\\u" << std::hex << std::setw(4) << std::setfill('0') << int(c);
} else {
o << c;
}
}
}
return o.str();
}
static std::string zpad_int(int value, int width) {
std::ostringstream oss;
oss << std::setw(width) << std::setfill('0') << value;
return oss.str();
}
static void ensure_dir(const fs::path& p) {
std::error_code ec;
if (fs::exists(p, ec)) {
if (!fs::is_directory(p, ec)) {
throw std::runtime_error("Path exists but is not a directory: " + p.string());
}
return;
}
if (!fs::create_directories(p, ec)) {
throw std::runtime_error("Failed to create directory: " + p.string() + " (" + ec.message() + ")");
}
}
static void write_text_file(const fs::path& p, const std::string& data) {
std::ofstream out(p, std::ios::out | std::ios::trunc);
if (!out) throw std::runtime_error("Failed to open for write: " + p.string());
out << data;
out.close();
if (!out) throw std::runtime_error("Failed writing file: " + p.string());
#ifndef _WIN32
// Best-effort: 0600 for sensitive text files (private keys)
::chmod(p.c_str(), 0600);
#endif
}
static void write_binary_file(const fs::path& p, const std::vector<uint8_t>& bytes) {
std::ofstream out(p, std::ios::out | std::ios::binary | std::ios::trunc);
if (!out) throw std::runtime_error("Failed to open for write: " + p.string());
out.write(reinterpret_cast<const char*>(bytes.data()), static_cast<std::streamsize>(bytes.size()));
out.close();
if (!out) throw std::runtime_error("Failed writing file: " + p.string());
#ifndef _WIN32
::chmod(p.c_str(), 0600);
#endif
}
enum class BundleMode { NONE, JSON, BIN, BOTH };
static BundleMode parse_bundle_mode(const std::string& s) {
if (s == "none") return BundleMode::NONE;
if (s == "json") return BundleMode::JSON;
if (s == "bin") return BundleMode::BIN;
if (s == "both") return BundleMode::BOTH;
throw std::runtime_error("Invalid --bundle value (must be none|json|bin|both)");
}
// Convert microReticulum Bytes-ish type to std::vector<uint8_t>.
// This assumes the returned object supports .size() and operator[].
// If your microReticulum Bytes type differs, well adjust here.
template <typename BytesT>
static std::vector<uint8_t> to_u8vec(const BytesT& b) {
std::vector<uint8_t> v;
v.reserve(static_cast<size_t>(b.size()));
for (size_t i = 0; i < static_cast<size_t>(b.size()); i++) {
v.push_back(static_cast<uint8_t>(b[i]));
}
return v;
}
int main(int argc, char** argv) {
try {
int quantity = -1;
std::string format = "tsv";
bool include_public = false;
bool do_outdir = false;
fs::path outdir;
std::string prefix = "device";
BundleMode bundle_mode = BundleMode::NONE;
bool bundle_mode_explicit = false;
for (int i = 1; i < argc; i++) {
std::string a(argv[i]);
if (is_flag(a, "-h") || is_flag(a, "--help")) {
usage(argv[0], true);
return 0;
} else if (is_flag(a, "-q") || is_flag(a, "--quantity")) {
if (i + 1 >= argc) throw std::runtime_error("Missing value for --quantity");
quantity = std::stoi(argv[++i]);
} else if (is_flag(a, "-f") || is_flag(a, "--format")) {
if (i + 1 >= argc) throw std::runtime_error("Missing value for --format");
format = argv[++i];
} else if (is_flag(a, "--public")) {
include_public = true;
} else if (is_flag(a, "--outdir")) {
if (i + 1 >= argc) throw std::runtime_error("Missing value for --outdir");
outdir = fs::path(argv[++i]);
do_outdir = true;
} else if (is_flag(a, "--bundle")) {
if (i + 1 >= argc) throw std::runtime_error("Missing value for --bundle");
bundle_mode = parse_bundle_mode(argv[++i]);
bundle_mode_explicit = true;
} else if (is_flag(a, "--prefix")) {
if (i + 1 >= argc) throw std::runtime_error("Missing value for --prefix");
prefix = argv[++i];
} else if (is_flag(a, "--version")) {
std::cout << TOOL_NAME << " " << TOOL_VERSION << "\n";
return 0;
} else {
throw std::runtime_error("Unknown argument: " + a);
}
}
if (quantity <= 0) {
usage(argv[0], true);
return 2;
}
if (!(format == "tsv" || format == "json")) {
throw std::runtime_error("Invalid --format (must be tsv or json)");
}
// Default bundle behavior: if user set --outdir but not --bundle, write both.
if (do_outdir && !bundle_mode_explicit) {
bundle_mode = BundleMode::BOTH;
}
if (do_outdir) {
ensure_dir(outdir);
}
struct DeviceRec {
int n;
std::string device_name;
std::string id_hex;
std::string priv_hex;
std::string pub_hex;
std::vector<uint8_t> priv_bin;
fs::path device_dir;
};
std::vector<DeviceRec> devices;
devices.reserve(static_cast<size_t>(quantity));
for (int n = 1; n <= quantity; n++) {
RNS::Identity ident(true);
DeviceRec rec;
rec.n = n;
rec.device_name = prefix + "_" + zpad_int(n, 3);
// These methods matched your earlier build. If microReticulum changes, we adjust here.
rec.id_hex = ident.hash().toHex();
rec.priv_hex = ident.get_private_key().toHex();
if (include_public) rec.pub_hex = ident.get_public_key().toHex();
// Write binary blob for the private key
rec.priv_bin = to_u8vec(ident.get_private_key());
if (do_outdir) {
rec.device_dir = outdir / rec.device_name;
ensure_dir(rec.device_dir);
}
devices.push_back(std::move(rec));
}
// ---- stdout ----
if (format == "tsv") {
std::cout << "n\tdevice\tid_hex\tprivate_key_hex";
if (include_public) std::cout << "\tpublic_key_hex";
std::cout << "\n";
for (const auto& d : devices) {
std::cout << d.n << "\t" << d.device_name << "\t" << d.id_hex << "\t" << d.priv_hex;
if (include_public) std::cout << "\t" << d.pub_hex;
std::cout << "\n";
}
} else {
std::cout << "[\n";
for (size_t i = 0; i < devices.size(); i++) {
const auto& d = devices[i];
std::cout << " {\n";
std::cout << " \"n\": " << d.n << ",\n";
std::cout << " \"device\": \"" << json_escape(d.device_name) << "\",\n";
std::cout << " \"id\": \"" << d.id_hex << "\",\n";
std::cout << " \"private_key\": \"" << d.priv_hex << "\"";
if (include_public) {
std::cout << ",\n \"public_key\": \"" << d.pub_hex << "\"\n";
} else {
std::cout << "\n";
}
std::cout << " }" << (i + 1 == devices.size() ? "\n" : ",\n");
}
std::cout << "]\n";
}
// ---- bundle output ----
if (do_outdir && bundle_mode != BundleMode::NONE) {
const std::string created_utc = utc_now_iso8601();
// manifest.json
{
std::ostringstream m;
m << "{\n";
m << " \"schema_version\": 1,\n";
m << " \"created_utc\": \"" << created_utc << "\",\n";
m << " \"quantity\": " << quantity << ",\n";
m << " \"prefix\": \"" << json_escape(prefix) << "\",\n";
m << " \"devices\": [\n";
for (size_t i = 0; i < devices.size(); i++) {
const auto& d = devices[i];
m << " {\n";
m << " \"n\": " << d.n << ",\n";
m << " \"device\": \"" << json_escape(d.device_name) << "\",\n";
m << " \"id\": \"" << d.id_hex << "\"";
if (bundle_mode == BundleMode::JSON || bundle_mode == BundleMode::BOTH) {
m << ",\n \"identity_json\": \"" << json_escape((d.device_name + "/identity.json")) << "\"";
}
if (bundle_mode == BundleMode::BIN || bundle_mode == BundleMode::BOTH) {
m << ",\n \"identity_bin\": \"" << json_escape((d.device_name + "/identity.bin")) << "\"";
}
m << "\n }" << (i + 1 == devices.size() ? "\n" : ",\n");
}
m << " ]\n";
m << "}\n";
write_text_file(outdir / "manifest.json", m.str());
}
for (const auto& d : devices) {
// label.txt
{
std::ostringstream l;
l << d.device_name << "\n";
l << "id: " << d.id_hex << "\n";
write_text_file(d.device_dir / "label.txt", l.str());
}
// identity.json
if (bundle_mode == BundleMode::JSON || bundle_mode == BundleMode::BOTH) {
std::ostringstream j;
j << "{\n";
j << " \"schema_version\": 1,\n";
j << " \"created_utc\": \"" << created_utc << "\",\n";
j << " \"device\": \"" << json_escape(d.device_name) << "\",\n";
j << " \"id\": \"" << d.id_hex << "\",\n";
j << " \"private_key\": \"" << d.priv_hex << "\"";
if (include_public) {
j << ",\n \"public_key\": \"" << d.pub_hex << "\"\n";
} else {
j << "\n";
}
j << "}\n";
write_text_file(d.device_dir / "identity.json", j.str());
}
// identity.bin
if (bundle_mode == BundleMode::BIN || bundle_mode == BundleMode::BOTH) {
write_binary_file(d.device_dir / "identity.bin", d.priv_bin);
}
}
std::cerr << "Wrote provisioning bundle to: " << outdir.string() << "\n";
}
return 0;
} catch (const std::exception& e) {
std::cerr << "Error: " << e.what() << "\n";
return 1;
}
}