Epic/03 runner app#3
Merged
diegoparrilla merged 8 commits intomainfrom May 1, 2026
Merged
Conversation
m68k side (target/atarist):
- New src/runner.s: PC-relative skeleton that lives at cartridge
offset $1C00. On entry: VT52-clears the screen, paints a "DevOps
Runner [Ready] - waiting for commands" banner via Cconws, then
spins in an active foreground poll loop. RUNNER_RESET / EXECUTE /
CD command dispatch land in S2-S4. Trailing dc.w $FFFF sentinel
keeps firmware.py's "trim trailing zeros + assert even length"
pass happy. Important: the m68k cannot write to the cartridge
$FA0000-$FAFFFF region (read-only ROM emulation, any store there
bus-errors), so the runner only reads.
- src/devops.ld: new .text_runner 0x001C00 section after
.text_gemdrive. Cartridge layout commented at the top.
- src/main.s: GEMDRIVE_BLOB_SIZE shrunk from $1800 to $1400 to make
room for the 1 KB Runner slot. Added RUNNER_BLOB at $FA1C00,
CMD_START_RUNNER = 5, runner_function (jmp RUNNER_BLOB) dispatch,
and the matching cmp branch in check_commands.
- Makefile: runner.o assembly + linker dep.
- BOOT.BIN: 7324 / 8192 bytes.
RP side (rp/src):
- New include/runner.h: Runner sub-region offsets inside APP_FREE
(PATH/CMDLINE/LAST_STATUS/DONE_FLAG/CWD/HELLO/PROTO_VER) and
RUNNER_HELLO_MAGIC / RUNNER_PROTO_VERSION constants. Reserved for
S2-S4 use.
- include/display.h: DISPLAY_COMMAND_START_RUNNER = 0x5.
- emul.c: new cmdRunner handler bound to [U]. Sets a runnerActive
flag (the m68k can't handshake from the read-only cartridge area,
so the active flag is owned RP-side) and sends
DISPLAY_COMMAND_START_RUNNER. Menu footer reads
"[E]xit (launch) r[U]nner [X] Booster".
- include/emul.h: emul_isRunnerActive() getter.
- http_server.c: handle_runner_status reads emul_isRunnerActive()
and returns {ok, active, busy:false, cwd:"", last_*:null} on
GET /api/v1/runner. Route table extended; GET and HEAD allowed.
CLI (cli/sidecart.py):
- New `runner` parent subcommand with `status` child. Prints
active/busy/cwd/last completion or --json passthrough. Top-of-
file docstring keeps the existing subcommand table.
Tests (cli/test_sidecart.py):
- 3 new cases (inactive flow, active with last completion,
--json passthrough). Now 48 tests, all green.
Build: BOOT.BIN at 7324 / 8192 bytes, rp.uf2 produced; CLI tests pass.
Verified on hardware: pressing [U] paints the Runner banner;
sidecart runner status reports active=true. [E]/[F]/countdown
keep the GEMDRIVE-only path and active=false.
m68k side (target/atarist/src/runner.s):
- Added APP_RUNNER = $0500 namespace with RUNNER_CMD_RESET ($0501).
- Runner poll loop now reads the cartridge sentinel and dispatches:
RUNNER_CMD_RESET -> runner_reset. Anything else: keep polling.
- runner_reset mirrors main.s's existing .reset: brief wait,
invalidate TOS' memvalid cookies ($420 / $43A / $51A), then
jmp through the reset vector at $00000004. All PC-relative —
no writes to the read-only cartridge area.
- Cartridge size: 7338 / 8192 bytes.
RP side (rp/src):
- include/runner.h: APP_RUNNER, RUNNER_CMD_RESET, runner_last_command_t
enum (NONE / RESET; EXECUTE / CD reserved).
- emul.c: runnerLastCommand / runnerLastStartedMs /
runnerLastFinishedMs / runnerRelaunchAtMs state. New getters and
emul_recordRunnerCommand / emul_scheduleRunnerRelaunch setters.
Main loop polls runnerRelaunchAtMs and re-fires
DISPLAY_COMMAND_START_RUNNER once the ST has had time to
cold-boot back to its CA_INIT polling loop — so Runner mode is
sticky across resets for the lifetime of the RP power cycle, and
the dev iteration loop (upload -> run -> reset on failure) doesn't
need the operator to re-press [U]. Power-cycling the Pico W is
what clears the stickiness.
- include/emul.h: emul_recordRunnerCommand,
emul_getRunnerLast{Command,StartedMs,FinishedMs},
emul_scheduleRunnerRelaunch.
- http_server.c: handle_runner_status now surfaces last_command,
last_started_at_ms, last_finished_at_ms (uptime millis or null).
New handle_runner_reset: 409 runner_inactive if !active, otherwise
records last_command=RESET, fires SEND_COMMAND_TO_DISPLAY(
RUNNER_CMD_RESET), schedules a 3 s relaunch into Runner mode,
returns 202 Accepted. Prefix dispatcher routes
POST /api/v1/runner/<action> -> handle_runner_reset for "reset".
CLI (cli/sidecart.py):
- New `sidecart runner reset` subcommand. POSTs the endpoint, exits
0 on 202 with "ok RESET sent". 409 -> exit 4 (conflict).
Tests (cli/test_sidecart.py):
- 2 new cases (202 Accepted, 409 runner_inactive). Now 50 tests,
all green.
Build: BOOT.BIN at 7338 / 8192 bytes, rp.uf2 produced; CLI tests pass.
Verified on hardware: with Runner mode booted, sidecart runner
reset cold-reboots the ST and the RP auto-re-fires
DISPLAY_COMMAND_START_RUNNER ~3 s later so the Runner banner
re-appears without any keyboard input.
Adds RUNNER_CMD_EXECUTE so the host can Pexec a TOS/PRG file on the ST without operator interaction. The Runner switches the m68k current drive to the GEMDRIVE-emulated one (read from shared var slot 12) before Pexec — without this, gemdrive's .Pexec drive-detection macro sees the boot-time current drive (typically A:) and chains to TOS native, which EFILNFs on C:\... on a freshly cold-reset boot. Path/cmdline are written into APP_FREE byte-pair-swapped to compensate for the cartridge bus' 16-bit-word byte order. The RP-side runner_command_cb clears the sentinel back to NOP after recording the exit code so the m68k poll loop doesn't re-fire the same EXECUTE in a tight loop. [RUN ] / [EXIT ] / [RESET] trace lines print on the ST screen as commands arrive, so the operator can see what landed even when the program leaves the screen in residue state. CLI: sidecart runner run <path> [args...]
RUNNER_CD: GEMDOS Dsetpath dispatched from the m68k runner, RP-side mirror of the cwd, errno reported via RUNNER_CMD_DONE_CD. Runner status surfaces both the cwd and the last cd-errno. CLI exposes `sidecart runner cd <path>`. Cwd-aware run/cd: server-side validation rebases relative inputs against runnerCwd to f_stat the right file, but the path written for the m68k stays in user-supplied form so GEMDOS Pexec/Dsetpath resolves it against the m68k's TOS process cwd (which the previous CD kept in sync). `cd /TEST` then `run RUNME.TOS` works; absolute paths still work; the C:\ drive-letter prefix is no longer needed because the runner Dsetdrvs to the emulated drive before each Pexec / Dsetpath. Reset survival: runner_entry now Dsetdrvs to the emulated drive, Dsetpaths to '\\', and sends RUNNER_CMD_DONE_HELLO. The HELLO handshake clears session-transient state (busy lock, cwd mirror, cd-errno) on the RP side and cancels any pending relaunch ticker. Sticky relaunch across any reset path: the m68k's CMD_GEMDRIVE_HELLO is the unambiguous "ST cold-booted" signal — emul_onGemdriveHello schedules the relaunch ticker whenever runnerActive==true, so the ST's physical reset button (and any non-API reset) auto-relaunches into Runner mode just like `runner reset` does. Ticker is now a self-correcting retry that re-fires DISPLAY_COMMAND_START_RUNNER every 500 ms until cancelled by the m68k's HELLO. Sentinel-clear after EXECUTE/CD DONE so the m68k poll loop doesn't re-fire the same command. Path/cmdline writes use byte-pair-swap to compensate for the cartridge bus' 16-bit-word byte order.
RUNNER_CMD_RES is a stateless Setscreen — the caller passes the
target rez (low=0, med=1) explicitly so the operation is idempotent
and survives any RP loss-of-tracking. The m68k checks Getrez first
and refuses on monochrome (errno -1) so high-rez monitors stay safe.
Recovery sequence after Setscreen lands the screen unreadable
otherwise: Vsync to commit the rez switch on the next frame,
Setpalette with a default 16-entry table, then a single Cconws
that VT52-clears + repaints the runner banner. The default palette
is STE-style ($0FFF / $0F00 / etc.) — ST hardware ignores bit 3 of
each nibble and reads it as the equivalent ST max ($0777 / $0700),
so the same table degrades cleanly on plain ST.
[EXIT ] trace reformatted to [EXIT <code>] - <path> terminated, with
the Pexec exit code printed inline so the operator sees the result
without a runner status round-trip. The decimal printer is a
two-stage 16-bit divu (68000 has no 32/16 divu.l) handling negatives,
zero, and the full i32 range.
API surface: POST /api/v1/runner/res {"rez":"low"|"med"}, errno
surfaced as last_res_errno in the status envelope. CLI exposes
sidecart runner res low|med.
…f self-contained relocatable layout The Runner module now follows the same pattern as gemdrive.s: a fully relocatable, self-contained translation unit that includes inc/sidecart_macros.s and inc/sidecart_functions.s privately so the bsr's emitted by send_sync / send_write_sync resolve inside the runner's own object file. No xref / xdef. No jsr / jmp to outside- module symbols. The earlier private runner_send_sync jsr-based macro and the xref to send_sync_command_to_sidecart are gone; call sites use the standard send_sync macro from inc/sidecart_macros.s. Adding the private functions copy pushed the runner blob over its 1 KB slot, so the cartridge code budget moves from 8 KB ($2000) to 10 KB ($2800). The shared block shifts from $FA2000 to $FA2800; APP_FREE from $FA2300 to $FA2B00 (~46 KB arena, still huge). All layout addresses on both m68k and RP sides are now derived from CARTRIDGE_CODE_SIZE / CHANDLER_CARTRIDGE_CODE_SIZE — the only hardcoded values left in the m68k modules are $FA0000 (ROM4_ADDR) and the cartridge-cap constant itself, so a future bump only touches one symbol per side. devops.ld extends the runner section to 3 KB ($1C00..$27FF). The build script enforces 10240 bytes against BOOT.BIN. CLAUDE.md records the relocatability rule for gemdrive.s and runner.s explicitly: no xref/xdef, no jsr/jmp to outside-module symbols.
New stateless GET /api/v1/runner/meminfo endpoint that fires
RUNNER_CMD_MEMINFO at the m68k, spins on chandler_loop until the
runner replies with a 24-byte struct via send_write_sync, then
returns a JSON envelope with the live snapshot. Times out after
1 s if the m68k doesn't respond. Inactive runner / busy lock are
the usual 409s.
m68k reads (already supervisor — inherited from CA_INIT bit 27):
membottom $432 _membot
memtop $436 _memtop
phystop $42E _phystop
screenmem $44E _v_bas_ad (logical screen base)
basepage $4F2 _run (TOS >= 1.04; 0 on older TOS)
$FFFF8001 lower nibble decoded into bank0_kb / bank1_kb:
0000 -> 128 / 128 0100 -> 512 / 128
0101 -> 512 / 512 1000 -> 2048 / 128
1010 -> 2048 / 2048 other -> 0 / 0 (unrecognised)
Wire protocol notes worth nailing down for future commands using
send_write_sync from a chandler-receiver perspective:
- The PIO captures each transmitted word as the m68k saw it
(uint16_t native), so payload[j] is already in the right byte
order — no COPY_AND_CHANGE_ENDIANESS_BLOCK16 needed.
- The RP-side TPROTO_NEXT32_PAYLOAD_PTR skips three scratch slots
(d3.l/d4.l/d5.l) that send_write_sync prepends before the
buffer body.
- m68k stored each u32 as `move.l value, offs(a4)` (high half
first), so on the RP each u32 reconstructs as
`(payload[N] << 16) | payload[N+1]`.
CLI `sidecart runner meminfo` prints the snapshot in human form
with the ST sysvar address annotated next to each field, plus
total RAM derived from the bank decode. `--json` passes the
envelope through verbatim.
5 new RunnerMeminfoTests cover happy path, unrecognised MMU,
JSON pass-through, 504 timeout, and 409 runner_inactive.
docs/api.md gains a "Runner mode" section after the file/folder endpoints with the full reference for the six Runner verbs (status, reset, run, cd, res, meminfo) — request shape, response envelope, common error codes, and side-by-side curl + sidecart examples for every endpoint. The status-codes table picks up 504, the busy description widens to mention the Runner busy lock, and the error vocabulary lists the three new symbols (runner_inactive, gateway_timeout, no_snapshot). README's shared-region snapshot reflects the Epic 03 cap bump (10 KB cartridge, $FA2800 sentinel, $FA2B00 APP_FREE, ~46 KB free arena). The "User firmware module" template paragraph is replaced with a "Cartridge code layout" + "Runner mode" pair that documents the actual main / gemdrive / runner split, the no-xref / no-jsr guardrail for relocatable modules, and points new users at the sidecart runner verb summary as a starting point. No code changes — pure docs polish closing out Epic 03.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
No description provided.