Skip to content

Epic/06 final touches#6

Merged
diegoparrilla merged 21 commits intomainfrom
epic/06-final-touches
May 4, 2026
Merged

Epic/06 final touches#6
diegoparrilla merged 21 commits intomainfrom
epic/06-final-touches

Conversation

@diegoparrilla
Copy link
Copy Markdown
Contributor

No description provided.

The bundled `httpc/` HTTPS-capable HTTP client was scaffolding
for a never-shipped firmware-download feature. Its only consumer
was `rp/src/download.c` — and download.c's public API
(`download_start`, `download_poll`, `download_finish`,
`download_confirm`, `download_getStatus`, `download_setStatus`,
`download_getFilepath`, `download_setFilepath`,
`download_getUrlComponents`) had zero callers anywhere in the
active source. Both retired together.

  - rp/src/CMakeLists.txt: drop add_subdirectory(httpc), the
    httpc link line, the download.c source entry, and the
    obsolete -DAPP_DOWNLOAD_HTTPS=0 define (also unreferenced).
  - Delete rp/src/httpc/, rp/src/download.c, rp/src/include/download.h.

Knock-on benefit: the httpc dependency pulled in mbedtls headers
and the lwip-http stack, both of which now don't compile, so the
RP firmware build is meaningfully faster.

Note: the "download" concept in http_server.c
(stream_download_drive, HC_STREAM_DOWNLOAD) is the SD-card
GET /api/v1/files/<path> response path — completely separate
from the deleted firmware-download module despite the name
collision.

Net diff: -882 lines, +1 line.
Power-on countdown auto-fire now lands the user in the same state
as pressing [U] instead of [F]. Runner is a strict superset of the
GEMDRIVE-only path the [F]/[E] keys produce — it installs GEMDRIVE
the same way and additionally exposes the Runner control surface
to the workstation (`runner status`, `runner run`, `runner adv …`,
the upcoming `runner load` / `runner exec`). For an unattended
boot, Runner mode is the more useful default.

  - emul.c:1527 cmdFirmware(NULL) → cmdRunner(NULL).
  - emul.c:1525-1526 comment refreshed: "[F]" → "[U]" + the why.
  - emul.c:403 section header: "auto-launches GEMDRIVE" →
    "auto-launches Runner mode" with the Epic 06 / S2 marker.

`showCounter`'s "Boot will continue in N seconds..." UI text is
neutral about which mode it lands in; no UI string change needed.

Verified on hardware: power on, leave alone, 20 seconds later the
device commits firmware mode AND `runner status` reports
`active: true`.
The advanced-runner hook vector setting was visually nested under
GEMDRIVE's sub-options ("F[o]lder / [D]rive / [R]eloc / Mem[t]op
/ Adv [V]ector"), which made the menu read as one wall of GEMDRIVE
config instead of three distinct config groups. Move it onto its
own top-level row, modelled on the existing "API Endpoint" block
(header line + indented `Label : value`).

Resulting layout:

  GEMDRIVE
    F[o]lder    : ...
    [D]rive     : C:
    [R]eloc addr: ...
    Mem[t]op    : ...

  Adv [V]ector
    Hook        : etv_timer ($400)

  API Endpoint
    URL         : http://sidecart.local/
    IP address  : 192.168.1.42

The [V] keypress and ACONFIG_PARAM_ADV_HOOK_VECTOR setting are
untouched — pure render change. The two display strings ("vbl
($70)" / "etv_timer ($400)") are now factored into a
const char *advHookDisplay so the body is three linear
term_printString calls instead of the old snprintf dance.
Adds a dedicated "USB CDC (Debug serial)" block on the setup
menu, modelled on the same header / indented-value layout as the
"API Endpoint" and "Adv [V]ector" blocks. The status field
("connected" / "disconnected") is live-refreshed: a small helper
polled from the main loop watches `usbcdc_getStats(..., &attached)`
and on a transition overdraws just the status line at fixed
row MENU_USBCDC_ROW + 1, leaving the rest of the menu untouched.

Implementation notes:

  - Both display strings ("connected   " / "disconnected") are
    12 chars, so the overdraw fully covers the previous value
    with no stale tail bytes — no clear-to-eol needed.
  - Live-refresh runs every main-loop iteration (~100 Hz) but
    early-exits on no state change, so the steady-state cost is
    one bool comparison.
  - After the overdraw the helper restores the visible cursor
    to "Select an option: " on the bottom row (col 18), so a
    plug/unplug event doesn't strand the cursor over the USB
    line and break the "type a single key here" UX.
  - Gated on menuScreenActive — once the user has left the
    setup menu the helper short-circuits and never touches the
    framebuffer.

Layout after this:

    ...
    Adv [V]ector
      Hook        : etv_timer ($400)

    API Endpoint
      URL         : http://sidecart.local/
      IP address  : 192.168.1.42

    USB CDC (Debug serial)
      Status      : connected

    [E]xit (launch)  r[U]nner  [X] Booster
    Select an option: ▌

Verified on hardware: plug a USB cable mid-menu → status flips
to "connected" within one main-loop tick; unplug → flips back to
"disconnected"; cursor stays parked after "Select an option:"
through every transition.
Splits the existing `runner run` (Pexec(0) load-and-go) into a
three-verb lifecycle so a workstation can drive ST programs the
way an interactive debugger would: load once, exec many times,
unload when done.

Wire-protocol additions (m68k ↔ RP):

  RUNNER_CMD_LOAD        ($0506)  Pexec(3) load-only
  RUNNER_CMD_EXEC        ($0507)  Pexec(4) just-go
  RUNNER_CMD_UNLOAD      ($0508)  GEMDOS Mfree(basepage)
  RUNNER_CMD_DONE_LOAD   ($0587)  i32: >0 basepage, <0 -GEMDOS errno
  RUNNER_CMD_DONE_EXEC   ($0588)  i32: program exit code
  RUNNER_CMD_DONE_UNLOAD ($0589)  i32: 0 OK, <0 -GEMDOS errno

  RUNNER_BASEPAGE        APP_FREE + 0x1994  4 B input slot for EXEC

HTTP API additions:

  POST /api/v1/runner/load   sync, body {remote, cmdline}, 200/422/409
  POST /api/v1/runner/exec   fire-and-forget, empty body, 202/409/503
  POST /api/v1/runner/unload sync, empty body, 200/422/409

  GET  /api/v1/runner adds   loaded_basepage, last_load_errno fields

CLI:

  python3 cli/sidecart.py runner load  /HELLODBG.TOS [cmdline...]
  python3 cli/sidecart.py runner exec
  python3 cli/sidecart.py runner unload
  python3 cli/sidecart.py runner status   # now prints "loaded : ..."

Notable details that bit during bring-up:

  - RUNNER_BASEPAGE lives in the cartridge ROM region — m68k can
    only READ it. Both runner_load and runner_exec must NOT try
    to write it ('move.l ..., RUNNER_BASEPAGE' would be a bus
    error — 2 bombs); the RP owns every cartridge-region write
    via the existing byte-pair-swapped runner_write_u32 helper.

  - GEMDOS Pexec(4) takes the basepage in the CMDLINE slot
    (14(a0)), NOT the fname slot (10(a0)). The latter (which I
    had at first) makes TOS see a NULL basepage and bus-error.
    GEMDRIVE's mode 0 → mode 6 rewriter at gemdrive.s:1023 uses
    the same convention; it's the canonical evidence.

  - Pexec(4) does NOT free the basepage (PE_GO semantics, vs.
    Pexec(6) PE_GO_AND_FREE). Re-exec on the same loaded program
    is therefore valid; emul_recordRunnerExecDone preserves
    pendingBasepage. Explicit `runner unload` calls Mfree.

  - runner_load / runner_exec / runner_unload all spin on a
    `wait_clear_*` loop after send_sync until the RP has cleared
    the cartridge sentinel; without that the m68k re-dispatches
    the same command (Pexec is fast enough that the m68k laps
    the chandler-callback's clear), which corrupts state.

  - Cartridge code budget: 10132 / 10240 bytes. Tight — future
    cartridge-side additions in this epic will need to trim the
    Cconws traces.

Plus a doc note in README.md "Debug traces" flagging that
powering the Pico via USB before the ST is on causes the
20 s setup-menu countdown to elapse against an absent ST,
auto-firing Runner mode (Epic 06 / S2) before the operator can
press [U]/[E]/[F]. Workaround documented.

Verified end-to-end on hardware:
  load /HELLODBG.TOS  → basepage cached
  status              → loaded: basepage 0x...
  exec                → program runs, exit code reported
  exec                → program runs again (re-exec works)
  unload              → Mfree succeeds, basepage cleared
  exec                → 409 no_program_loaded
…ntdown bar

Layered on top of the existing term-based menu without rewriting
the rendering pipeline. All u8g2 work happens AFTER the term
prints, so the term renderer never has a chance to clobber the
graphical overlays.

Changes in rp/src/emul.c:

  drawMenuDividers()
    Three 1-px horizontal rules at y=60 / 84 / 116 — the gap
    pixels between term rows so they don't slice any character
    cell. Visually separates GEMDRIVE / Adv [V]ector / API
    Endpoint / USB CDC config groups.

  drawIconCell(x, y_top, glyph, visible)
    8×9 erase + conditional u8g2_DrawGlyph from
    u8g2_font_open_iconic_embedded_1x_t. Erase is one pixel
    taller than the nominal 8×8 cell because some glyphs in
    this font (notably 0x4D lightbulb) carry an ascender margin
    and their topmost pixel lands at y_top - 1; an 8-tall erase
    leaves that row painted on hide→show transitions.

  drawMenuStatusIcons()
    Right-aligned 8×8 icons at the four section headers:
      0x42 cog        — Adv [V]ector
      0x4C hard-drive — GEMDRIVE
      0x4D lightbulb  — USB CDC (only when tud_cdc_connected())
      0x50 wifi       — API Endpoint (only when DHCP has IP)

    Three earlier candidates for USB CDC (flash 0x43, pulse
    0x46, bluetooth 0x4A) all proved unreadable at 8×8 in
    hardware testing — lightbulb's silhouette won.

  drawCountdownBar(sec_left, sec_total)
    Replaces "Boot will continue in N seconds..." plain text
    with a filled white box at the bottom strip that shrinks
    once per second. The status message ("Booting in N s — any
    key halts") is drawn TWICE with different u8g2 clip windows
    — once in black against the filled left half, once in white
    against the empty right half — so it reads cleanly across
    the bar boundary. (XOR mode would be the obvious one-pass
    approach but u8g2's default font mode XORs the full glyph
    bounding box on the white background, leaving solid black
    blocks where letters should be.)

  refreshUsbCdcLine()
    Now also flips the lightbulb icon in lock-step with the
    "connected"/"disconnected" status text so the USB icon is
    live, not just a snapshot.

  Halt-state message updated:
    "Press [E] or [X] to continue." → "Press [E], [U] or [X] to continue."
    (replaces both occurrences — refreshSetupInfoLine and the
    main-loop transition handler.)

Visual delta: section dividers, four status icons in the right
column, animated white→black countdown bar with always-readable
overlay text. Existing menu functionality (every keypress, the
USB CDC live-refresh, the auto-fire at countdown-zero, the
"Countdown stopped." text) all unchanged.

Tier 2 (logo XBM, two-column layout, change-highlight, nav
pills) and Tier 3 (term-pipeline rewrite, modal folder picker,
sparklines) are parked on Epic 06's backlog with their
prerequisites; pull back only if the menu still feels flat
after this Tier 1 lands.
Documents the three new runner verbs landed in S5+S6+S7 + the
menu polish in S8, so a reader who has only the README and
docs/api.md can drive the full lifecycle on hardware.

  docs/api.md
    New "Pexec lifecycle: load / exec / unload" subsection
    under Runner mode, between /runner/run and /runner/cd.
    Covers:
      - The verb → GEMDOS Pexec mode mapping table
      - RP-side single-slot state (loaded_basepage, last_load_errno
        fields surfaced in /api/v1/runner)
      - Strict-refuse semantics (409 program_already_loaded)
      - Per-endpoint sections for /load, /exec, /unload with curl
        + sidecart examples + response envelopes + error code
        listings
      - End-to-end lifecycle example (load → status → exec → exec
        → unload → status → exec → 409)

  README.md
    New "Pexec lifecycle (load / exec / unload)" subsection
    under Runner mode with the four-line CLI snippet and a
    pointer at the api.md anchor for full details.

The S8 menu polish (icons, dividers, animated countdown bar)
ships entirely on the device — nothing for an end-user to learn
or do, so no README/api.md change there beyond the existing
power-up note from earlier in the epic.
Three small fixes the first S9 pass missed:

  - docs/api.md GET /api/v1/runner — response shape now lists
    the new loaded_basepage and last_load_errno fields, and the
    last_command enum gains PEXEC_LOAD / PEXEC_EXEC /
    PEXEC_UNLOAD with a one-paragraph note that links to the
    Pexec lifecycle subsection.
  - docs/api.md POST /api/v1/runner/exec — adds the 202
    response-body example ({ ok, accepted }) so the section
    matches the shape of /load and /unload.
  - README.md Power-up note — drops the "(per Epic 06 / S2)"
    internal-history citation; users following the doc don't
    need an epic-and-story tag to understand the auto-Runner
    behaviour.

No code change. CLI tests still 105/105.
Replaces the prior README — which opened with the upstream
template's title and was effectively a developer-internals
sketch — with an end-user / app-consumer-focused README in the
shape of the sister md-drives-emulator project.

New top-level structure:

  Title + tagline
  Highlights (bullet list of features)
  Read-before-installing call-out (no auth, template doc link)
  🚀 Installation         5-step Booster-app deploy flow
  🕹️ Usage                boot countdown, [U]/[E]/[F]/[X] table
  ⚙️ Setup menu screen    ASCII screenshot + per-section table
  Runner mode             (existing CLI snippet kept)
  Pexec lifecycle         (existing kept)
  Advanced Runner         (existing kept)
  Remote HTTP API         enriched with an end-to-end
                          edit-build-test workflow example
  Debug traces            (existing kept verbatim)
  Project internals       absorbs the prior "Shared 64 KB
                          region layout" + "Cartridge code
                          layout" sections at the bottom +
                          adds a small "Building from source"
                          subsection pointing at programming.md
                          / the SidecarTridge programming docs
  License                 (existing kept)

No content from the prior README was lost — the technical
internals just moved to the bottom under a "for contributors
and ST programmers writing apps that talk to md-devops
directly" framing, so a fresh-clone reader gets onboarding
context first and developer-internals after they've decided
they care.

Pure doc change. CLI tests untouched (still 105/105).
Both surfaces — CLI and HTTP API — used to expose the
file/folder management verbs at the top level alongside
unrelated families (ping / runner / debug). The result was
incoherent nesting: `runner run` and `ls` sat at the same depth
even though one is a Runner-mode operation and the other is a
GEMDRIVE drive-management operation.

Hard cutover, no backward-compatible aliases. Now everything
GEMDRIVE-related lives under a `gemdrive` prefix on both
surfaces:

  CLI                                   HTTP
  ------------                          ----
  sidecart gemdrive volume              GET    /api/v1/gemdrive/volume
  sidecart gemdrive ls [PATH]           GET    /api/v1/gemdrive/files?path=...
  sidecart gemdrive mkdir REMOTE        POST   /api/v1/gemdrive/folders/<rel>
  sidecart gemdrive rmdir REMOTE        DELETE /api/v1/gemdrive/folders/<rel>
  sidecart gemdrive mvdir FROM TO       POST   /api/v1/gemdrive/folders/<from>/rename
  sidecart gemdrive rm REMOTE           DELETE /api/v1/gemdrive/files/<rel>
  sidecart gemdrive mv FROM TO          POST   /api/v1/gemdrive/files/<from>/rename
  sidecart gemdrive get REMOTE [LOCAL]  GET    /api/v1/gemdrive/files/<rel>
  sidecart gemdrive put LOCAL [REMOTE]  PUT    /api/v1/gemdrive/files/<rel>

  ping / runner / debug families unchanged (still top-level
  `ping` / `runner …` / `debug …` on the CLI, still
  /api/v1/{ping,runner,debug}* on the HTTP side).

Mechanical changes:

  cli/sidecart.py
    - argparse: new gemdrive subparser with the nine verbs as
      its subcommands; main() routes args.cmd == "gemdrive"
      to a gd_handlers dict by args.gemdrive_cmd.
    - URL builders (cmd_volume's literal, folders_url, files_url)
      retargeted to the new /api/v1/gemdrive/* paths.
    - Module docstring (Subcommands + Examples sections)
      refreshed.

  cli/test_sidecart.py
    - 31 _run_cli argv lists rewritten to inject "gemdrive"
      before the verb (Python regex pass).
    - 11 last_path assertions retargeted to /api/v1/gemdrive/*.

  rp/src/http_server.c
    - Static route table entries (/volume, /files exact-match
      listing) → /gemdrive/volume, /gemdrive/files.
    - Two prefix-dispatcher arms (folders_prefix, files_prefix
      including the upload-streaming special case) → the new
      /api/v1/gemdrive/* prefixes.
    - Location: response headers + cross-reference error
      messages refreshed.

  docs/api.md, README.md
    - Every curl URL and every sidecart CLI snippet refreshed
      to the new shapes.

Verified: full RP firmware build green; all 105 CLI tests green.
Two new chapters added before the existing Runner mode section,
in the order a fresh user actually needs them:

  ## 🛜 First-contact: ping
    Why ping is the first thing to test, what success looks like
    in human + JSON + curl, and a troubleshooting list for the
    common failure modes (mDNS not resolving, wrong host, wrong
    network segment) with concrete --host / SIDECART_HOST escape
    hatches.

  ## 💾 GEMDRIVE commands — manage files and folders remotely
    Per-verb section for every gemdrive subcommand
    (volume / ls / get / put / rm / mv / mkdir / rmdir / mvdir)
    with output examples and the constraints that bite (8.3
    name limits, 4 MB upload cap, no-implicit-overwrite on mv,
    refuses non-empty rmdir, no descendant-cycle on mvdir).
    Plus a cross-cutting edit-build-test recipe.

The prior "Remote HTTP Management API" section was duplicating
the CLI examples that now live in the GEMDRIVE chapter; rewrote
it as a brief "under the hood" pointer that maps each CLI
prefix to its HTTP family + links to docs/api.md for the full
reference. Net diff: more total content, less duplication.

Local-only sweeps over the (gitignored) epic docs to keep the
historical-design notes coherent with the shipped state are
done in the working tree but not part of this commit:
  - docs/epics/06-final-touches.md: S9 marked shipped, S10 row
    added.
  - docs/epics/02-http-api.md: 48 URL + 22 CLI rewrites for
    the gemdrive cutover.
  - docs/epics/03-runner.md: 1 CLI rewrite.
The HTTP API is the device's single control surface — every
remote operation flows through it — so the README now explains
that fact upfront, before launching into the chapters that use
it.

Reordered:
  ⚙️ Setup menu screen
  🌐 Remote HTTP Management API + sidecart.py CLI    ← new spot
  🛜 First-contact: ping
  💾 GEMDRIVE commands
  Runner mode
  Debug traces
  Project internals
  License

The promoted chapter now covers in detail what was previously a
brief "under the hood" footnote at the bottom:

  - The HTTP API itself: hostname / IP, the family table
    (ping / gemdrive / runner / debug → endpoints), and the
    framing that the CLI is one of many possible clients.

  - cli/sidecart.py specifics:
      * Requirements (Python 3.10+, stdlib-only — no pip).
      * "Installation" (clone the repo and call it; or symlink
        onto $PATH for shorter invocations).
      * Host configuration: --host flag + $SIDECART_HOST env
        var with precedence rules.
      * Global flags table (--host / --json / -q / -h).
      * Exit code map (0/1/2/3/4/5/6/7/8) so shell scripts can
        branch on category.
      * Direct curl equivalents for two example endpoints, to
        make the "thin wrapper" framing concrete.

  - No-auth warning kept as a callout block.

The previously-existing "Remote HTTP Management API — under the
hood" section near the bottom of the file was the same content
in summary form; deleted as redundant. Net diff: more content
total, less duplication, better narrative flow.

CLI tests: still 105/105.
The Runner mode and Debug traces chapters were the only places
in the README still rolling all subcommands into a single
batch-call code block. Now they match the GEMDRIVE chapter's
shape: one `### subcommand …` heading per verb, a one-or-two-
line description, an example invocation with realistic output,
and any caveats (HTTP status, sync vs fire-and-forget, blast
radius).

Runner mode now has dedicated subsections for:
  status, reset, cd, res, meminfo, run, load/exec/unload, and
  the four Advanced Runner verbs (adv status / meminfo / jump /
  load) under their own `### Advanced Runner` umbrella.

Each carries:
  - The exact `$ sidecart …` invocation, copy-pasteable.
  - A representative output block (so the reader knows what
    success looks like, and what fields they'll see).
  - Whether the call is synchronous or fire-and-forget, and the
    relevant timeout / HTTP code (10 s + 200 for `runner load`,
    202 for fire-and-forget verbs, 5 s for `runner unload`,
    etc.).
  - For `runner adv jump` / `runner adv load`: the shell-quoting
    gotcha for `$hex` arguments and the address constraints
    (even, 24-bit, ≥ 0x800, below phystop).
  - Cross-references between related verbs (e.g. `runner
    meminfo` pointing at `runner adv meminfo` as the wedged-ST
    fallback; `runner adv jump` paired with `runner adv load`
    for the load-and-jump idiom).

Also adds a short "Foreground vs Advanced" framing paragraph at
the top of Runner mode so readers understand why the same kind
of operation (meminfo) appears twice.

Debug traces now has dedicated subsections for:
  - `debug status` — what each diagnostic field means and when
    a non-zero `bytes_dropped` / `usbcdc_dropped` matters.
  - `debug tail` — long-poll behaviour, when to prefer it over
    USB, plus the `tee` capture idiom.
  - `USB CDC` — same byte stream, no network, with terminal
    examples; called out as a peer surface to `debug tail`
    (both can run at once, ring fans out internally).
  - End-to-end smoke — the existing HELLODBG.TOS recipe, kept.

The m68k ABI explanation, power-up ordering callout, C / asm
emit examples, and dump-string snippets are all preserved
verbatim — only the trailing transports + smoke block was
restructured.
Previous wording claimed `runner reset` returned the ST to the
setup menu, with the user having to press [U] or wait for the
countdown again. That's wrong: the firmware-mode commit is held
on the Pico (not the ST), so a soft ST reset just lands back in
whatever mode was active. The setup menu only reappears on a
full power-cycle of the ST combined with a hard reset of the
Pico.
…code

Three-way audit of rp/src/http_server.c (source of truth) against
docs/api.md and README.md surfaced a set of stale claims that no
longer match the firmware. Fixing them all in one pass.

docs/api.md:

  Runner mode framing (was: "every Runner endpoint returns 409
  runner_inactive ... only run/cd/res/meminfo gate on busy")
  rewritten as three behavioural buckets matching the actual
  handler logic:
    - Status reads (`/api/v1/runner`, `/api/v1/runner/adv`):
      always 200, never error on inactive Runner — they just
      report `active: false` (handlers at http_server.c:1224 and
      :2512 have no `runner_inactive` gate).
    - Foreground commands (run/cd/res/meminfo + load/exec/unload):
      every one of these gates on busy. The previous "four
      foreground gating endpoints" claim missed load/exec/unload
      (handlers at :1580, :1747, :1800 all return 503 busy).
    - VBL-driven commands (`reset` + `adv/*`): ride the VBL ISR,
      no busy gate, only `runner_inactive` + (for jump/load)
      `wrong_hook`. The `busy` code-vocabulary entry was
      rewritten to match.

  `POST /api/v1/runner/reset` description rewritten:
    - Was: "Fires `RUNNER_CMD_RESET`". Truth (http_server.c:2753):
      it fires `RUNNER_ADV_CMD_RESET` — Epic 04/S5 rewired this
      to the VBL ISR specifically so it could escape wedged
      programs. The handler comment at :2731-2740 spells this out.
    - Added the firmware-mode-lives-on-Pico fact: a soft ST reset
      re-enters Runner mode automatically; only a power-cycle
      plus hard Pico reset returns to the setup menu.

  504 gateway_timeout description (was: "Runner endpoint waited
  > 1 s") corrected: the actual deadlines are 10 s for `runner
  load`, 5 s for `runner unload`, 1 s for meminfo / adv-meminfo /
  per-chunk adv-load (constants at http_server.c:1573, 1793,
  2080, 2204).

  `runner run` / `runner load` example removed the misleading
  `--` separator and `cmdline = "-v --file foo"` annotation.
  argparse REMAINDER preserves `--` verbatim, so the previous
  example would actually have produced `cmdline = "-- -v --file
  foo"`. The CLI's own help string says no `--` is needed
  (sidecart.py:1287). Added a one-line note that `--`, if
  passed, lands in `cmdline` literally.

README.md:

  Same three-bucket reframing of the Runner-mode surface (the
  previous two-bucket "Foreground / Advanced" split miscategorised
  `reset`, which rides the VBL ISR even though its URL doesn't
  carry `/adv/`).

  The `runner reset` subsection now states up-front that it
  rides the VBL ISR via `RUNNER_ADV_CMD_RESET` — that's the
  reason it can escape wedged programs and the reason it doesn't
  gate on busy. Auto-re-enter-Runner-mode behaviour kept from
  the prior fix.

CLI: no changes — sidecart.py was already consistent with the
RP source of truth (105/105 tests still passing).
Continues the docs/api.md vs RP-code vs CLI consistency sweep.
Four more drift fixes; all cite the canonical handler in
rp/src/http_server.c.

docs/api.md:

  fs_type list expanded to include `UNKNOWN`. Truth: the
  fat_type_name() table at http_server.c:1156-1162 falls through
  to "UNKNOWN" when the FatFs FS code isn't one of FAT12 / 16 /
  32 / EXFAT — possible on rare card formats. Docs previously
  promised one of four values.

  Error code vocabulary list (Conventions section) now includes
  the four codes that response handlers actually emit but were
  missing from the doc:
    - `pexec_failed` — runner load Pexec(3) error envelope
      (http_server.c:1712).
    - `mfree_failed` — runner unload Mfree error envelope
      (:1839).
    - `program_already_loaded` — strict-refuse on second load
      (:1589).
    - `no_program_loaded` — exec/unload with no prior load
      (:1756, :1809).
  These are documented in the per-endpoint sections already;
  the vocabulary list is the canonical "what codes can clients
  switch on" reference and was incomplete.

  Upload errors list now mentions `409 conflict "Cannot PUT
  root"` — handle_file_upload_init at :3742 returns this, but
  the doc only listed file-exists and is_directory.

README.md:

  `gemdrive mv` description said "Both endpoints must stay in
  the same drive (the API can't move across drives)". Misleading
  — the API jails everything under one GEMDRIVE root and has no
  drive-letter concept at all. Rewrote to "Both source and
  destination paths are jailed under GEMDRIVE_FOLDER like every
  other API path — the API has no notion of multiple drives,
  only the one emulated GEMDRIVE root."

CLI: still consistent (105/105 tests pass).
The README is for end users / developers integrating with the
firmware, not for contributors who track our internal Epic-and-
stories planning. Two stray references slipped through:

  - "HTTP-management story stitched on top" in the Highlights
    intro — generic English usage of "story" but easy to read
    as an internal-jargon leak. Rewrote as "HTTP-management
    surface stitched on top".
  - "Epic 04 rewired it from the foreground poll loop" in the
    runner reset framing. Replaced with a behaviour-focused
    "it was deliberately wired to the VBL ISR rather than the
    foreground poll loop so it can escape wedged programs" —
    same fact, no leak of our development process.

A grep for `[Ee]pic|[Ss]tory|\bS[0-9]+\b` in README.md is now
clean.
Until now the README mentioned the two Advanced Runner hook
vectors only in passing — the menu table called out
\`vbl ($70)\` as "faster" and \`etv_timer ($400)\` as "more
compatible" without saying *why*, and the Advanced Runner
chapter just said "adv jump and adv load require \$70". A
reader had no way to make an informed choice between them.

Setup menu chapter:

  Added a "Picking a hook vector" subsection right under the
  menu-elements table. It walks through:

    - **\`vbl (\$70)\`**: hardware VBL exception vector. Standard
      m68k trap frame on the supervisor stack, so saved PC and
      SR are at known offsets — \`runner adv jump\` and
      \`runner adv load\` can rewrite them. Sits ahead of TOS'
      VBL chain (works against programs that bypass TOS).
      Vulnerable to programs that overwrite \`\$70.l\` directly.

    - **\`etv_timer (\$400)\`**: TOS' documented extension hook,
      called from inside TOS' VBL chain after TOS' own
      bookkeeping. More polite about cooperating with TOS.
      Survives programs that rewrite \`\$70.l\`. Trap-frame
      layout past the MFP scratch is not stable across TOS
      versions — so jump/load refuse with 409 wrong_hook here.
      Vulnerable to programs that hijack \`etv_timer\` itself.

    - Which verbs work on which vector (status/meminfo/reset
      work on either; jump/load need \$70).

    - Rule of thumb for picking: keep \`etv_timer\` as the
      polite default, switch to \`vbl\` when you need
      jump/load, when debugging code that bypasses TOS, or
      when something has hijacked \`etv_timer\`.

  The menu table's "Adv [V]ector" row now points at the new
  subsection instead of the half-truth "(faster) / (more
  compatible)" parenthetical.

Advanced Runner chapter intro:

  Replaced the brief "adv jump and adv load require the VBL
  hook" line with a verb × vector compatibility table, and a
  one-paragraph technical reason why jump/load can't safely
  run from \`etv_timer\` (trap-frame layout past MFP scratch,
  TOS-version-fragile). Cross-references the new setup-menu
  subsection for the full trade-off.

CLI tests still passing (no CLI changes — docs only).
The Advanced Runner's hook-vector setting now defaults to
\`vbl\` (\$70) rather than \`etv_timer\` (\$400). Rationale:

  - The full Advanced Runner feature set (\`runner adv jump\`
    and \`runner adv load\`) only works on \$70, because we
    need the standard m68k exception trap-frame layout to
    rewrite the saved PC. \`etv_timer\` runs from inside TOS'
    VBL chain past the MFP scratch and the layout there is
    not stable across TOS versions.
  - Defaulting to \`vbl\` means a fresh-flashed device gives
    the user every Advanced Runner verb out of the box, no
    setup-menu detour. \`etv_timer\` remains available as an
    opt-in for callers who deliberately want to ride
    downstream of TOS' VBL chain (e.g. to cooperate with a
    program that has installed its own raw \$70 handler).

Code changes:

  - rp/src/aconfig.c — defaultEntries[] for
    ACONFIG_PARAM_ADV_HOOK_VECTOR flipped to "vbl". Comment
    rewritten to explain the reasoning.
  - rp/src/gemdrive.c — HELLO publish path: aconfigString()
    fallback flipped to "vbl"; the strcmp branching reordered
    so anything-other-than-"etv_timer" lands on \$70 (was: only
    "vbl" landed on \$70, anything else on \$400). Net effect:
    a corrupted/garbage setting now falls back to vbl rather
    than etv_timer.
  - rp/src/emul.c — menu display fallback flipped to "vbl";
    display logic reordered so anything-other-than-"etv_timer"
    renders as "vbl (\$70)". Same flip in cmdAdvHookVector
    (the \`[V]\` toggle) so an empty/missing value cycles to
    "etv_timer" first, then back to "vbl".

README:

  - Removed the "Rule of thumb" paragraph that recommended
    keeping etv_timer as the default — it no longer applies
    and the user explicitly didn't want it.
  - Setup-menu screenshot updated: the \`Hook\` line now shows
    \`vbl (\$70)\`, matching what a fresh boot will display.
  - "Picking a hook vector" subsection rewritten to state
    \`vbl (\$70)\` as the default up-front and frame
    \`etv_timer (\$400)\` as the opt-in alternative; the
    pros/cons of each are unchanged.

docs/api.md needed no changes — every existing reference is
neutral about which is default (e.g. "if you flipped
ADV_HOOK_VECTOR to etv_timer") and reads correctly under the
new default.
Updates the three tracked version.txt files (root,
rp/version.txt, target/version.txt) and the regenerated m68k
target_firmware.h (one cell drift from the version string
baked into BOOT.BIN at build time).
Three small touch-ups before tagging:

  - README title: rewritten as "SidecarTridge Multi-device
    DevOps microfirmware" and the lede paragraph reflowed into
    one sentence — drops the dev-jargon "md-devops" name from
    the title and matches the brand framing in the rest of the
    repo.
  - desc/app.json: filled with the actual app's name
    ("DevOps toolkit"), feature-summary description with the
    docs.sidecartridge.com "Learn more" link in the same shape
    md-drives-emulator uses, and the four catalog tags
    (Development / GEMDRIVE / Debug / CLI). The three
    placeholder fields the build pipeline owns
    (<APP_UUID> / <BINARY_MD5_HASH> / <APP_VERSION>) are kept
    verbatim.
  - rp/src/include/target_firmware.h: regenerated from the
    rebuilt m68k BOOT.BIN — single byte-pair changed where the
    embedded version string lives.
@diegoparrilla diegoparrilla merged commit ae96dba into main May 4, 2026
1 check passed
@diegoparrilla diegoparrilla deleted the epic/06-final-touches branch May 4, 2026 22:11
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant