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

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();
}