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:
parent
c7646e169e
commit
41c1fe6819
16 changed files with 2016 additions and 71 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -17,3 +17,4 @@
|
|||
/hold/
|
||||
.platformio_local/
|
||||
|
||||
.codex
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
|
@ -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
|
||||
|
|
@ -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();
|
||||
}
|
||||
|
|
|
|||
32
exercises/16_PSRAM/README.md
Normal file
32
exercises/16_PSRAM/README.md
Normal 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
|
||||
54
exercises/16_PSRAM/platformio.ini
Normal file
54
exercises/16_PSRAM/platformio.ini
Normal 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\"
|
||||
12
exercises/16_PSRAM/scripts/set_build_epoch.py
Normal file
12
exercises/16_PSRAM/scripts/set_build_epoch.py
Normal 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),
|
||||
]
|
||||
)
|
||||
342
exercises/16_PSRAM/src/main.cpp
Normal file
342
exercises/16_PSRAM/src/main.cpp
Normal 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();
|
||||
}
|
||||
32
exercises/17_Flash/README.md
Normal file
32
exercises/17_Flash/README.md
Normal 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.
|
||||
50
exercises/17_Flash/platformio.ini
Normal file
50
exercises/17_Flash/platformio.ini
Normal 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\"
|
||||
11
exercises/17_Flash/read_partition_bin.py
Normal file
11
exercises/17_Flash/read_partition_bin.py
Normal 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")
|
||||
12
exercises/17_Flash/scripts/set_build_epoch.py
Normal file
12
exercises/17_Flash/scripts/set_build_epoch.py
Normal 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),
|
||||
]
|
||||
)
|
||||
26
exercises/17_Flash/show_partition_table.py
Normal file
26
exercises/17_Flash/show_partition_table.py
Normal 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
|
||||
608
exercises/17_Flash/src/main.cpp
Normal file
608
exercises/17_Flash/src/main.cpp
Normal 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();
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue