Skip to content

Epic/03 runner app#3

Merged
diegoparrilla merged 8 commits intomainfrom
epic/03-runner-app
May 1, 2026
Merged

Epic/03 runner app#3
diegoparrilla merged 8 commits intomainfrom
epic/03-runner-app

Conversation

@diegoparrilla
Copy link
Copy Markdown
Contributor

No description provided.

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.
@diegoparrilla diegoparrilla merged commit 36b7e73 into main May 1, 2026
1 check passed
@diegoparrilla diegoparrilla deleted the epic/03-runner-app branch May 1, 2026 16:55
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