After various memory exercises, then back to GPS to get UBlox working. UBlox is working. TODO: start libraries, downplay SD Card as we can use memory for interim logging

This commit is contained in:
John Poole 2026-04-04 11:52:41 -07:00
commit 41c1fe6819
16 changed files with 2016 additions and 71 deletions

1
.gitignore vendored
View file

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

View file

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

View file

@ -40,3 +40,9 @@ build_flags =
build_flags =
${env.build_flags}
-D NODE_LABEL=\"B\"
[env:node_c]
build_flags =
${env.build_flags}
-D NODE_LABEL=\"C\"
-D GPS_UBLOX

File diff suppressed because it is too large Load diff

View file

@ -39,3 +39,9 @@ build_flags =
build_flags =
${env.build_flags}
-D NODE_LABEL=\"B\"
[env:node_c]
build_flags =
${env.build_flags}
-D NODE_LABEL=\"C\"
-D GPS_UBLOX

View file

@ -4,6 +4,7 @@
#include <Arduino.h>
#include <Wire.h>
#include <U8g2lib.h>
#include <time.h>
#ifndef NODE_LABEL
#define NODE_LABEL "RAM"
@ -21,10 +22,11 @@
static U8G2_SH1106_128X64_NONAME_F_HW_I2C g_oled(U8G2_R0, U8X8_PIN_NONE);
static const char *kTmpPath = "/tmp/volatile.txt";
static const size_t kTmpFileCapacity = 4096;
static const char *kTmpPath = "/tmp/AMY_output.log";
static const size_t kTmpFileCapacity = 32768;
static char g_tmpFileBuffer[kTmpFileCapacity];
static size_t g_tmpFileSize = 0;
static unsigned g_tmpLineNumber = 0;
static void oledShowLines(const char *l1,
const char *l2 = nullptr,
@ -47,6 +49,51 @@ static size_t getAvailableRamBytes()
return ESP.getFreeHeap();
}
static void getTimestamp(char *out, size_t outSize)
{
const time_t now = time(nullptr);
if (now > 1700000000) {
struct tm tmNow;
localtime_r(&now, &tmNow);
snprintf(out, outSize, "%04d-%02d-%02d %02d:%02d:%02d",
tmNow.tm_year + 1900,
tmNow.tm_mon + 1,
tmNow.tm_mday,
tmNow.tm_hour,
tmNow.tm_min,
tmNow.tm_sec);
return;
}
const uint32_t sec = millis() / 1000;
const uint32_t hh = sec / 3600;
const uint32_t mm = (sec % 3600) / 60;
const uint32_t ss = sec % 60;
snprintf(out, outSize, "uptime %02u:%02u:%02u", (unsigned)hh, (unsigned)mm, (unsigned)ss);
}
static void appendTimestampLine()
{
char timestamp[32];
getTimestamp(timestamp, sizeof(timestamp));
char line[96];
const int written = snprintf(line, sizeof(line), "%u, %s\r\n", g_tmpLineNumber + 1, timestamp);
if (written <= 0) {
return;
}
const size_t lineLen = (size_t)written;
if (g_tmpFileSize + lineLen > kTmpFileCapacity - 1) {
Serial.println("Warning: /tmp log full, stopping writes");
return;
}
memcpy(g_tmpFileBuffer + g_tmpFileSize, line, lineLen);
g_tmpFileSize += lineLen;
g_tmpLineNumber++;
}
static void printRamStatus()
{
const size_t freeBytes = getAvailableRamBytes();
@ -65,7 +112,7 @@ static void printRamStatus()
char line4[32];
snprintf(line4, sizeof(line4), "Total: %u KB", (unsigned)(totalBytes / 1024U));
char line5[32];
snprintf(line5, sizeof(line5), "Max alloc: %u KB", (unsigned)(maxAlloc / 1024U));
snprintf(line5, sizeof(line5), "Lines: %u", (unsigned)g_tmpLineNumber);
oledShowLines(line1, line2, line3, line4, line5);
}
@ -85,6 +132,7 @@ static void printTmpFileStat()
{
Serial.printf("Path: %s\r\n", kTmpPath);
Serial.printf("Size: %u bytes\r\n", (unsigned)g_tmpFileSize);
Serial.printf("Lines: %u\r\n", (unsigned)g_tmpLineNumber);
Serial.printf("Capacity: %u bytes\r\n", (unsigned)kTmpFileCapacity);
}
@ -219,5 +267,6 @@ void loop()
}
lastMs = now;
appendTimestampLine();
printRamStatus();
}

View file

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

View file

@ -0,0 +1,54 @@
; 20260403 ChatGPT
; Exercise 16_PSRAM
[platformio]
default_envs = amy
[env]
platform = espressif32
framework = arduino
board = freenove_esp32_s3_wroom
monitor_speed = 115200
extra_scripts = pre:scripts/set_build_epoch.py
lib_deps =
Wire
olikraus/U8g2@^2.36.4
build_flags =
-I ../../shared/boards
-I ../../external/microReticulum_Firmware
-D BOARD_MODEL=BOARD_TBEAM_S_V1
-D OLED_SDA=17
-D OLED_SCL=18
-D OLED_ADDR=0x3C
-D ARDUINO_USB_MODE=1
-D ARDUINO_USB_CDC_ON_BOOT=1
board_build.flash_mode = qio
board_build.psram = 1
board_build.psram_type = spi
board_build.arduino.memory_type = qio_qspi
[env:amy]
extends = env
build_flags =
${env.build_flags}
-D NODE_LABEL=\"AMY\"
[env:bob]
extends = env
build_flags =
${env.build_flags}
-D NODE_LABEL=\"BOB\"
[env:cy]
extends = env
build_flags =
${env.build_flags}
-D NODE_LABEL=\"CY\"
[env:dan]
extends = env
build_flags =
${env.build_flags}
-D NODE_LABEL=\"DAN\"

View file

@ -0,0 +1,12 @@
import time
Import("env")
epoch = int(time.time())
utc_tag = time.strftime("%Y%m%d_%H%M%S_z", time.gmtime(epoch))
env.Append(
CPPDEFINES=[
("FW_BUILD_EPOCH", str(epoch)),
("FW_BUILD_UTC", '\\"%s\\"' % utc_tag),
]
)

View file

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

View file

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

View file

@ -0,0 +1,50 @@
; 20260403 ChatGPT
; Exercise 17_Flash
[platformio]
default_envs = amy
[env]
platform = espressif32
framework = arduino
board = esp32-s3-devkitc-1
monitor_speed = 115200
extra_scripts = pre:scripts/set_build_epoch.py
lib_deps =
Wire
olikraus/U8g2@^2.36.4
lewisxhe/XPowersLib@0.3.3
build_flags =
-I ../../shared/boards
-I ../../external/microReticulum_Firmware
-D BOARD_MODEL=BOARD_TBEAM_S_V1
-D OLED_SDA=17
-D OLED_SCL=18
-D OLED_ADDR=0x3C
-D ARDUINO_USB_MODE=1
-D ARDUINO_USB_CDC_ON_BOOT=1
[env:amy]
extends = env
build_flags =
${env.build_flags}
-D NODE_LABEL=\"AMY\"
[env:bob]
extends = env
build_flags =
${env.build_flags}
-D NODE_LABEL=\"BOB\"
[env:cy]
extends = env
build_flags =
${env.build_flags}
-D NODE_LABEL=\"CY\"
[env:dan]
extends = env
build_flags =
${env.build_flags}
-D NODE_LABEL=\"DAN\"

View file

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

View file

@ -0,0 +1,12 @@
import time
Import("env")
epoch = int(time.time())
utc_tag = time.strftime("%Y%m%d_%H%M%S_z", time.gmtime(epoch))
env.Append(
CPPDEFINES=[
("FW_BUILD_EPOCH", str(epoch)),
("FW_BUILD_UTC", '\"%s\"' % utc_tag),
]
)

View file

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

View file

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