From 7670f179651c2c37c0c0ab11b61f2516899d39f3 Mon Sep 17 00:00:00 2001 From: David Konsumer Date: Thu, 1 Oct 2026 14:51:00 -0700 Subject: [PATCH 1/2] Serial push reuses slot, PIO upload script --- src/serial_console.cpp | 19 +++++++++++++++---- tools/pio_launcher_upload.py | 27 +++++++++++++++++++++++++++ 2 files changed, 42 insertions(+), 4 deletions(-) create mode 100644 tools/pio_launcher_upload.py diff --git a/src/serial_console.cpp b/src/serial_console.cpp index ff75e25f..90ec5f60 100644 --- a/src/serial_console.cpp +++ b/src/serial_console.cpp @@ -413,11 +413,22 @@ static void handleFlashCommand(const String &name, uint32_t size) { return; } - String label = launcherInstallNextAppLabel(table, name); - std::vector dataPartitions; // app image only, no data partitions + // Host pushes can't answer the on-device "which partition to replace?" menu, so + // pick for them: overwrite the slot a previous push of this name used (growing + // it if the image got bigger), else take free space, else fail instead of hanging. LauncherPartitionEntry appEntry; - if (!launcherSelectInstallLayout(table, size, label, dataPartitions, appEntry, error)) { - launcherConsolePrintf("ERR %s\n", error.c_str()); + String label = launcherPartitionSanitizedAppLabelBase(name); + const LauncherPartitionEntry *old = launcherPartitionFindByLabel(table, label.c_str()); + if (old && launcherPartitionIsReplaceableApp(*old)) { + if (old->size >= size) appEntry = *old; + else launcherPartitionRemoveEntryByOffset(table, old->offset); + } else { + label = launcherInstallNextAppLabel(table, name); + } + if (appEntry.size == 0 && + (!launcherPartitionCreateOtaApp(table, size, label.c_str(), &appEntry, &error) || + !launcherPartitionValidate(table, &error))) { + launcherConsolePrintf("ERR no space for app: %s\n", error.c_str()); return; } diff --git a/tools/pio_launcher_upload.py b/tools/pio_launcher_upload.py new file mode 100644 index 00000000..cee1b70f --- /dev/null +++ b/tools/pio_launcher_upload.py @@ -0,0 +1,27 @@ +"""PlatformIO extra script: `pio run -t upload` pushes the build into a running +Launcher over serial, instead of flashing the whole chip. +Launcher stays intact; the pushed app boots after the Launcher startup delay. +Re-pushing replaces the same slot; if flash is full it fails rather than prompting. + +In your project's platformio.ini: + + extra_scripts = post:/path/to/Launcher/tools/pio_launcher_upload.py + upload_port = /dev/ttyACM0 ; optional, else PlatformIO auto-detects + +Needs `pyserial` and `esptool` in PlatformIO's Python (pip install pyserial esptool). +""" + +import os + +Import("env") # noqa: F821 (provided by PlatformIO/SCons) + +flasher = os.path.join(os.path.dirname(os.path.abspath(__file__)), "serial_flasher.py") +app_name = os.path.basename(env.subst("$PROJECT_DIR")) # noqa: F821 + + +if not env.subst("$UPLOAD_PORT"): # noqa: F821 + env.AutodetectUploadPort() # noqa: F821 + +env.Replace( # noqa: F821 + UPLOADCMD=f'"$PYTHONEXE" "{flasher}" -p "$UPLOAD_PORT" -n "{app_name}" -f $SOURCE' +) From e656b492e59fb0290953afba68f7d942479847b9 Mon Sep 17 00:00:00 2001 From: David Konsumer Date: Thu, 1 Oct 2026 18:40:23 -0700 Subject: [PATCH 2/2] Serial push: separate flash firmware / overwrite, drop PIO script - serial_console: keep "flash firmware " on the original launcherSelectInstallLayout path; add "flash overwrite " which reuses the slot a name already owns (grow/move it) instead of prompting, so a host on the other end of a serial link never hangs. Both share one streaming body; no extra flash cost beyond the branch. - serial_flasher.py: opt-in --overwrite (sends "flash overwrite" and uses windowed ACK streaming). Default stays byte-for-byte the main behavior ("flash firmware" + one-chunk-per-ack). Also survive USB-CDC re-enumeration on reset. - remove tools/pio_launcher_upload.py: serial_flasher covers the push. Verified on Cardputer ADV: --overwrite creates a slot then reuses it on re-push (single alpha0); default reproduces main's behavior. --- src/serial_console.cpp | 53 +++++++---- tools/pio_launcher_upload.py | 27 ------ tools/serial_flasher.py | 167 ++++++++++++++++++++++++++--------- 3 files changed, 160 insertions(+), 87 deletions(-) delete mode 100644 tools/pio_launcher_upload.py diff --git a/src/serial_console.cpp b/src/serial_console.cpp index 90ec5f60..06ecc45d 100644 --- a/src/serial_console.cpp +++ b/src/serial_console.cpp @@ -36,6 +36,8 @@ // flash firmware reserve space, reply "READY ", then // read raw bytes from Serial and // install/boot them as a new app +// flash overwrite same, but reuse the slot already +// uses (grow/move it) instead of prompting // wifi auto connect to the first scanned network with // saved credentials, else print the scan // wifi scan scan and list nearby networks @@ -400,7 +402,7 @@ static void handlePartitionCommand(const std::vector &tokens) { launcherConsolePrintln("ERR unknown partition subcommand"); } -static void handleFlashCommand(const String &name, uint32_t size) { +static void handleFlashCommand(const String &name, uint32_t size, bool overwrite) { if (size == 0) { launcherConsolePrintln("ERR invalid size"); return; @@ -413,23 +415,33 @@ static void handleFlashCommand(const String &name, uint32_t size) { return; } - // Host pushes can't answer the on-device "which partition to replace?" menu, so - // pick for them: overwrite the slot a previous push of this name used (growing - // it if the image got bigger), else take free space, else fail instead of hanging. LauncherPartitionEntry appEntry; - String label = launcherPartitionSanitizedAppLabelBase(name); - const LauncherPartitionEntry *old = launcherPartitionFindByLabel(table, label.c_str()); - if (old && launcherPartitionIsReplaceableApp(*old)) { - if (old->size >= size) appEntry = *old; - else launcherPartitionRemoveEntryByOffset(table, old->offset); + if (overwrite) { + // Reuse the slot a previous push of this name used, growing it into adjacent + // free space when the image got bigger. A host on the other end of a serial + // link can't answer the on-device "which partition to replace?" menu, so this + // falls back to free space and fails instead of prompting. + String label = launcherPartitionSanitizedAppLabelBase(name); + const LauncherPartitionEntry *old = launcherPartitionFindByLabel(table, label.c_str()); + if (old && launcherPartitionIsReplaceableApp(*old)) { + if (old->size >= size) appEntry = *old; + else launcherPartitionRemoveEntryByOffset(table, old->offset); + } else { + label = launcherInstallNextAppLabel(table, name); + } + if (appEntry.size == 0 && + (!launcherPartitionCreateOtaApp(table, size, label.c_str(), &appEntry, &error) || + !launcherPartitionValidate(table, &error))) { + launcherConsolePrintf("ERR no space for app: %s\n", error.c_str()); + return; + } } else { - label = launcherInstallNextAppLabel(table, name); - } - if (appEntry.size == 0 && - (!launcherPartitionCreateOtaApp(table, size, label.c_str(), &appEntry, &error) || - !launcherPartitionValidate(table, &error))) { - launcherConsolePrintf("ERR no space for app: %s\n", error.c_str()); - return; + String label = launcherInstallNextAppLabel(table, name); + std::vector dataPartitions; // app image only, no data partitions + if (!launcherSelectInstallLayout(table, size, label, dataPartitions, appEntry, error)) { + launcherConsolePrintf("ERR %s\n", error.c_str()); + return; + } } // Tell the host it's safe to start streaming the raw firmware bytes now. @@ -899,6 +911,7 @@ static void printHelp() { launcherConsolePrintln(" partition edit