Serial sideload for Lua apps: fput/fadd/fend CLI + scripts/sideload_app.py

The ThinkNode M9's microSD is soldered on, so the SDK's "drop it on the card"
route does not exist there and the Store can only fetch from its own host.
Three console commands write into the same /apps (or /lang) the Store uses:

  fput /apps/<name>                 open (truncate); names [A-Za-z0-9._-]
  fadd <off> <len> <sum> <base64>   append a chunk, every field verified
  fend                              close

Same physical-access trust level as the existing "rm"/"erase", narrower scope
(two directories). DataStore gains a root-aware mkdirRooted(); the CLI line
buffer grows from 80 to 200 bytes.

Why the chunks are self-checking and short: the UART interrupt is not
IRAM-resident, so while the loop is inside a flash-cache pause only the
128-byte hardware FIFO buffers console input and the middle of a longer line
is lost -- observed on the M9 as a 197-char line echoed back as 120. Offset,
decoded length and byte sum reject a damaged line; the host re-sends. The
host script also waits for "[BOOT] ui ready" because the CH34x bridge resets
the board whenever the port is opened and the console is not serviced before
the UI is up.

Verified on the M9: gpscompass.lua (19055 B) + .json pushed, sizes confirmed
by the device and by `ls /apps` after a reboot. M9 and V4-R8 envs compile.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Christopher Van HooseandClaude Fable 5 committed 2026-08-22 01:15:56 -04:00
1 parent b273f20e26
commit 0771e15452
7 files changed
+296 -7

No files matched your search

+5
View File
@@ -225,6 +225,11 @@ bool DataStore::fileExists(FILESYSTEM* fs, const char* filename) {
return fs->exists(_rp(filename));
}
bool DataStore::mkdirRooted(FILESYSTEM* fs, const char* dir) {
const char* p = _rp(dir);
return fs->exists(p) || fs->mkdir(p);
}
bool DataStore::removeRooted(FILESYSTEM* fs, const char* filename) {
return fs->remove(_rp(filename));
}