Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 40 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,27 @@ both on every push and PR.
programs that never return to the prompt). `run_crt` runs the posted cart
on purpose, so no parking there. `$CC` reads 0x00 at the READY prompt,
verified.
- **Disk images (verified on 1.1.0)**: the DMA socket's `RUN_IMG` (0xFF0B,
header `0B FF <len16 LE> <len bits 16-23>` then the image) saves the
payload as `/temp/tcpimage.d64`, mounts it on drive A and runs
`C64_DRIVE_LOAD` with `RUNCODE_MOUNT_LOAD_RUN`: the firmware resets,
types `LOAD"*",8,1` and `RUN` itself (screen RAM confirms). Payload cap
is `SOCKET_BUFFER_SIZE` = 200000 bytes (every .d64 variant fits; the
firmware silently truncates beyond). Always saved as .d64, so only that
type autostarts; `POST /v1/drives/a:mount?type=<d64|g64|d71|g71|d81>`
with the image as the body mounts the others (lands as `/Temp/temp0000`).
`MOUNT_IMG` (0xFF0A) is the socket twin of that mount. With a network
password the socket needs `AUTHENTICATE` (0xFF1F, password as payload,
one-byte reply 1/0, 1 s throttle on failure) before any other command,
or the firmware drops the connection; `dma_connect` does it for both
the keyboard channel and image runs. `C64U_DMA_PORT` redirects port 64
for tests (fakeultimate.py logs `DMA cmd=FFxx len=N`). **One DMA client
at a time**: `dmaThread` accepts a connection and serves it until it
closes before accepting the next, so the viewer's open keyboard
connection stalls an image transfer behind it (seen as a 30 s stall then
EAGAIN; a bare socket takes 0.1 s for a .d64 whether or not the stream
runs or the C64 is loading). Hence `run_file_async` closes the keyboard
socket and `keyb_try_connect` stays off port 64 while `g_run_busy`.

## Dev workflow (no hardware needed)

Expand Down Expand Up @@ -186,6 +207,25 @@ control + password, drag-and-drop run, help overlay) shipped in v0.2.0.
comes from SDL3's HIDAPI drivers + mapping db (worst case Steam udev
rules or SDL_GAMECONTROLLERCONFIG); code against generic SDL_Gamepad.

3. **Persistent drop storage** (agreed 2026-09-02, not started): the drop
path keeps the firmware's temp area (RAM disk, gone at power-off) as
the fast default; a `--store <folder>` flag and/or a modifier held
during the drop switch to FTP-upload-then-mount-by-path. FTP is the
only upload route: the REST files API has no upload on any firmware
(verified: `curl -T` to `ftp://<ult>/Temp/` works, `files/<path>:info`
then sees the file, the FTP service is on by default on 1.1.0). Sequence:
check `files/<path>:info` (refuse to overwrite), `curl -T` the file,
`PUT drives/a:mount?image=<path>&mode=readwrite`, then for autostart
`machine:reset` + readiness gate + `LOAD"*",8,1` / `RUN` over the
keyboard channel (no firmware autostart for a path mount). Michal's
preference: upload to `/Temp` and move the file from the Ultimate menu
himself. Open questions: whether SDL reports a modifier held during a
drag on Wayland (`SDL_GetKeyboardState` at drop time; if not, flag
only), and the static release build needs curl rebuilt with FTP
(`--disable-ftp` today in release.yml). Follow-up on top of it: in the
F9 view, upload into the folder the menu currently shows (path line
parse; truncated long paths need a fallback).

Dormant follow-up: when official firmware ships `machine:input`, re-verify
the matrix-keyboard mapping against real hardware and activate the gamepad
path alongside it.
33 changes: 25 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,9 +46,14 @@ cable.

| Service | Needed for |
|---|---|
| Web Remote Control Service (REST API) | starting/stopping the streams |
| Ultimate DMA Service | keyboard input (port 64) |
| Telnet Remote Menu Service | the F9 menu view (port 23) |
| Web Remote Control Service (REST API, port 80) | discovery, starting/stopping the streams, the Ctrl+R/P/M machine controls and `--do`, running dropped `.prg`/`.crt`/`.sid` files, mounting `.g64`/`.d71`/`.g71`/`.d81` images, cartridge parking, and matrix-level typing on firmware that has `machine:input` |
| Ultimate DMA Service (port 64) | typing into the C64 (KERNAL buffer), RUN/STOP, and mount-and-run of a dropped `.d64` |
| Telnet Remote Menu Service (port 23) | the F9 menu view |

Everything else the viewer does needs no service: the video/audio streams
arrive on UDP 11000/11001 once started. The REST API alone gets you a
picture and sound; add the DMA service to type, and telnet to browse the
Ultimate's menu from the viewer.

### Wired Ethernet is required for the streams

Expand Down Expand Up @@ -89,11 +94,23 @@ refuses to start a stream toward an address missing from its ARP table.
| Ctrl+M | press the Ultimate's menu button (the on-screen menu then answers the physical keyboard only; use F9 to drive a menu from the viewer) |
| Ctrl+Q | quit |

**Drop a `.prg`, `.crt`, or `.sid` file onto the window** and the Ultimate
runs it (DMA program run, cartridge run, or SID player). If a cartridge is
configured on the machine, it is parked for the run and restored afterwards,
so a freezer cart won't hijack the program's reset; the cart is back on your
next manual reset. The same works headless: `c64uv --run game.prg`.
**Drop a `.prg`, `.crt`, `.sid`, or `.d64` file onto the window** and the
Ultimate runs it (DMA program run, cartridge run, SID player, or for a
`.d64`: mount on drive A, reset, `LOAD"*",8,1` and `RUN`, all done by the
firmware over the DMA socket). If a cartridge is configured on the machine,
it is parked for the run and restored afterwards, so a freezer cart won't
hijack the program's reset; the cart is back on your next manual reset. The
same works headless: `c64uv --run game.prg`.

The window title shows the transfer progress ("sending game.d64 42%").
The Ultimate serves one DMA-socket client at a time, so the viewer drops its
keyboard connection for the duration and reconnects it on your next keypress;
a second drop while one is in flight is refused.

Other disk images (`.g64`, `.d71`, `.g71`, `.d81`) are mounted on drive A
without touching the machine; type `LOAD"*",8,1` yourself. Every image is
copied to the Ultimate's temp area first, so writes never reach the file you
dropped.

The same machine controls work headless: `c64uv --do reset` (also `reboot`,
`pause`, `resume`, `menu`, `poweroff`) issues one REST call and exits. If
Expand Down
7 changes: 7 additions & 0 deletions src/compat.h
Original file line number Diff line number Diff line change
Expand Up @@ -75,8 +75,15 @@ int compat_wait_readable(const compat_sock *socks, int n, int timeout_ms);

void compat_close(compat_sock s);

// Waits until `s` can take more data or timeout_ms elapsed. 1 when
// writable, 0 on timeout, -1 on error.
int compat_wait_writable(compat_sock s, int timeout_ms);

// Text for the last socket error (errno / WSAGetLastError).
const char *compat_neterr(void);
// True when the last socket error was a would-block / timeout (EAGAIN,
// EWOULDBLOCK, EINTR; WSAEWOULDBLOCK, WSAETIMEDOUT), i.e. worth retrying.
bool compat_neterr_transient(void);

// Source address the OS would use to reach `ip` (UDP connect +
// getsockname). Dotted quad in out (>= COMPAT_IP_STRLEN bytes).
Expand Down
11 changes: 11 additions & 0 deletions src/compat_posix.c
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,12 @@ int compat_wait_readable(const compat_sock *socks, int n, int timeout_ms)
return poll(pfd, (nfds_t)n, timeout_ms); // negative fds are ignored
}

int compat_wait_writable(compat_sock s, int timeout_ms)
{
struct pollfd pfd = {.fd = s, .events = POLLOUT};
return poll(&pfd, 1, timeout_ms);
}

void compat_close(compat_sock s)
{
if (s >= 0)
Expand All @@ -145,6 +151,11 @@ const char *compat_neterr(void)
return strerror(errno);
}

bool compat_neterr_transient(void)
{
return errno == EAGAIN || errno == EWOULDBLOCK || errno == EINTR;
}

bool compat_route_source_ip(const char *ip, char *out, size_t cap)
{
int s = socket(AF_INET, SOCK_DGRAM, 0);
Expand Down
2 changes: 1 addition & 1 deletion src/keys.c
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,7 @@ const struct viewer_binding viewer_bindings[] = {
// ASCII only: the overlay renders through the 8x8 basic font
{.label = "Tab, PgUp, Ins, End", .desc = "CTRL, RESTORE, pound, + (matrix)",
.action = VA_INFO, .gap = true},
{.label = "file drop", .desc = "run a .prg/.crt/.sid on the machine",
{.label = "file drop", .desc = "run .prg/.crt/.sid/.d64 on the C64",
.action = VA_INFO},
{.key = SDLK_F9, .label = "F9", .desc = "toggle the Ultimate menu view",
.action = VA_MENU_VIEW},
Expand Down
Loading
Loading