Skip to content

Fix RFM protocol to match rfm.asm wire format#5

Open
DrPitre wants to merge 34 commits into
mainfrom
rfm-protocol-fix
Open

Fix RFM protocol to match rfm.asm wire format#5
DrPitre wants to merge 34 commits into
mainfrom
rfm-protocol-fix

Conversation

@DrPitre

@DrPitre DrPitre commented Jun 10, 2026

Copy link
Copy Markdown
Owner

Summary

Full RFM (Remote File Manager) implementation — CoCo 3 NitrOS-9 can now read and write files on the Mac host via the DriveWire Becker port in MAME.

What works

  • list /y0/file — read files from host
  • PRINT #1 in Basic09 — write text to host files
  • mdir>/y0/output — redirect command output to host files
  • Shell > redirect for any NitrOS-9 command

Protocol fixes

  • All RFM op handlers match rfm.asm wire format (open, read, readln, seek, close, getstat, setstat, write, writeln)
  • rfmPaths dictionary replaces single descriptor — multiple simultaneous paths work
  • rfmRootPath (--rfm-root) sets the host base directory; /y0/file maps to rfmRootPath/y0/file

Write correctness

  • Sequential file.write() — no seek/position tracking that caused zero-padding corruption
  • CR→LF translation and $FF padding strip for OS-9 string buffers
  • shouldCreate paths survive their first close (OS-9 shell redirect pattern: CREATE→CLOSE→WRITELNs)
  • Fallback to open writable descriptor for I$Dup2 path# remapping

Server crash fixes

  • DriveWireHost.init() calls setupTransactions() so processor is never nil
  • start() no longer discards pre-inserted virtual disks
  • Force-unwrap crashes on non-ASCII bytes replaced with safe fallbacks
  • Serial channel handlers consume correct byte counts (was causing framing desync → spurious RFM opcodes)
  • Watchdog cancelled mid-transaction

TCP server (--tcp-port)

  • DriveWireTCPServerDriver for MAME's Becker port socket connection
  • drivewire-cli gains --tcp-port and --rfm-root

Test plan

  • list /y0/filename reads host files
  • PRINT #1,"text" in Basic09 writes to host files
  • mdir>/y0/output redirects mdir output to host file
  • NitrOS-9 boots from DriveWire disk via HDB-DOS Becker port in MAME
  • Seek and random-access read/write

🤖 Generated with Claude Code

Boisy Pitre and others added 27 commits June 10, 2026 09:15
The Swift server's RFM handlers were designed for an extended protocol
that rfm.asm never implemented. Align every handler with what the CoCo
client actually sends and reads:

- Open/Create: expect path#(1)+mode(1) then CR-terminated pathname;
  send 1-byte error code
- Read/ReadLn: expect path#(1)+maxBytes(2); send count(1) then data;
  count=0 signals EOF
- Seek: expect path#(1)+X(2)+U(2) for 32-bit position; send 1-byte error
- Close: expect path#(1); send 1-byte error; close FileHandle and remove
  from path table
- GetStat: expect path#(1)+SS.code(1); send nothing (rfm.asm handlers
  are stubs that don't read a response)
- SetStat: consume nothing (rfm.asm sendit sends only the sub-opcode);
  send nothing
- Write/WritLn: expect path#(1)+count(2)+data; send nothing
- MakDir/Delete: consume nothing (rfm.asm sendit sends only sub-opcode)

Replace single rfmPathDescriptor with rfmPaths:[Int:RFMPathDescriptor]
keyed by path number to support multiple simultaneously open paths.

Add rfmRootPath property (defaults to home directory) replacing the
hardcoded /Users/boisy prefix in openLocalFile.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Restore processID and pathDescriptorAddress fields in RFMPathDescriptor
- Add shouldCreate flag; OPRFMCREATE creates file if missing, OPRFMOPEN
  returns E$PNNF (216); both share rfmOpen() helper
- Strip bit 7 from pathname bytes (OS-9 high-bit terminates last char)
- Respect mode byte: open forUpdating when write (0x02) or update (0x08)
  bits are set
- Guard against path traversal: reject resolved paths outside rfmRootPath
- Return E$PNNF for empty pathnames
- Add --rfm-root CLI option (defaults to home directory)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
DriveWireTCPServerDriver listens for an incoming TCP connection using
Network framework's NWListener. When MAME is launched with
  -bitb socket.127.0.0.1:<port>
it connects to this server and the DriveWire host handles the session.

Usage:
  drivewire-cli --tcp-port 65400 --rfm-root ~/rfm-test --disk0 image.dsk

--port (serial) and --tcp-port are mutually exclusive; one is required.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Remove dead do/catch blocks (nothing inside can throw)
- Remove unused offset parameter; filePosition from the struct is always used
- Fix off-by-one in readLineFromFile: <= maximumCount allowed one extra byte
- readFromFile now uses a Data slice instead of a byte-by-byte loop
- readLineFromFile uses data.append() instead of allocating a new Data per byte

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The watchdog fires on wall-clock time, which can desync the state machine
when MAME runs faster than real-time: a legitimate multi-byte transaction
can straddle the 500ms window and get reset mid-packet. Invalidate the
watchdog as soon as bytes are consumed so it only fires during true idle
periods (no bytes arriving at all).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…disks

start() was creating a new DriveWireHost, throwing away any virtual disks
inserted into d.host before calling start(). Change to just set the
delegate on the existing host so disks survive the call.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The default init() left processor=nil, causing a fatal unwrap when
send() was called on a host created without a delegate. Now both init()
paths call setupTransactions() so the state machine is always ready.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Protocol desync can cause sector data bytes ($D6 $02) to be misread
as OP_VFM + DW.open, producing a pathname of null bytes that confuses
FileManager. Return E$PNNF (216) immediately for null-containing paths.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
String(bytes:data:encoding:.ascii)! crashes when data contains non-ASCII
bytes (null bytes, control chars) from protocol desync during NitrOS-9
boot. Replace with ?? "" fallback in both OP_NAMEOBJ_MOUNT2 instances.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
All 8 OP_SERxxx handlers were consuming only 1 byte (the opcode) instead
of the full packet. This caused protocol framing desync: leftover channel#,
stat_code, and data bytes were misinterpreted as new opcodes, eventually
producing spurious OP_VFM ($D6) triggers and RFM errors.

Correct byte counts:
  SERINIT/SERTERM/SERREAD/SERREADM: 2 (opcode + channel#)
  SERWRITE/SERGETSTAT/SERSETSTAT:   3 (opcode + channel# + byte)
  SERWRITEM: variable               3 + count (opcode + channel# + count + data)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add writeToFile() to RFMPathDescriptor: seeks to filePosition, writes
data via FileHandle, keeps fileContents in sync for subsequent reads.

OPRFMWRITE: writes raw bytes at current position.
OPRFMWRITLN: same but translates CR→LF (OS-9 uses CR, host uses LF).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
try seek(toOffset:) was silently catching exceptions in do/catch, causing
writes to fail with no indication. Switch to seek(toFileOffset:) which
doesn't throw so failures are not swallowed.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
OS-9 I$Dup2 maps parent's path# X to child's path# 1 without calling
the file manager, so the server never gets an OPEN for path# 1. When
shell redirects (mdir>/y0/xx), mdir writes to path# 1 but rfmPaths only
has path# X. Fall back to the single open writable descriptor when the
given path# isn't found.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
OS-9 shell redirect: CREATE → CLOSE (shell releases ref) → WRITELNs.
If the path was opened via CREATE, treat the first CLOSE as the shell
handing off to the child writer — keep the descriptor alive. Second
CLOSE does the real cleanup.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
OS-9 pads string buffers with $FF bytes after the CR terminator.
WriteLn data is fixed-length (80 bytes) with $FF fill after the
actual content. Break at the first $FF to avoid writing garbage.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The seek+fileContents approach caused zeros to appear between written
chunks. FileHandle already tracks position after each write; just call
file.write() sequentially without seeking.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Receives same packet format as rfmOpen (path# + processID + mode +
pathname + CR). Creates the directory at rfmRootPath + pathname using
FileManager.createDirectory(withIntermediateDirectories: true).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
mdir packs data like [name$0D][rest_of_info$0D][$00 padding]. The
previous break-at-first-CR discarded everything after the first CR.
Now: strip trailing $00/$FF padding, convert ALL $0D→$0A, preserving
complete multi-part line content.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…fers

mdir's writeBUF appends CR then resets bufptr and sends all 80 bytes.
Everything after the first CR is leftover from previous writeBUF calls.
Break at first CR (converting to LF), discard the rest.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
OPRFMSEEK sets filePosition in the descriptor but writeToFile was just
appending at the FileHandle's current position. Now seeks to filePosition
first, so a SEEK to 0 followed by WRITE rewrites from the beginning.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Same packet format as rfmOpen (path# + processID + mode + pathname + CR).
Uses FileManager.removeItem which handles both files and directories.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
In OS-9, del only removes files. deldir clears the DIR bit first then
calls I$Delete. Return E$FNA (214) if the target is a directory so the
OS-9 del command behaves correctly.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
OPRFMCHGDIR was calling OPRFMOPEN which waited for more bytes, consuming
the following GETSTT packet as path data and corrupting state. Fixed to
reset immediately since chgdir uses sendit (no path data).

OPRFMDELETE now allows deleting empty directories (for deldir path) and
returns E$DNE (215) for non-empty ones.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…cking

- Track per-process CWD; resolve relative pathnames using pid then ppid.
- Expand OS-9 multi-dot path components (N dots = N-1 parent refs).
- Synthesize OS-9 directory entries with stable LSNs for dir opens.
- Synthesize FD sectors from host file attributes for SS.FD/SS.FDInf
  and intercept matching OP_READEX LSNs to serve the same FD bytes.
- Handle SS.Size, SS.Pos, SS.EOF in OPRFMGETSTT.
- READ/READLN now use 2-byte length (allowing >255-byte transfers).
- OPRFMOPEN/CREATE/MAKDIR/CHGDIR/DELETE now receive ppid (4-byte header);
  CHGDIR actually receives and resolves a path.
- Reset RFM state on guest disconnect, OP_INIT, and OP_RESET so a
  reconnecting client starts clean.
- Remove pendingClose/shouldCreate descriptor fields; pass shouldCreate
  through openLocalFile() instead.
- Add drivewire-cli shared scheme.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
hathaway3 added a commit to hathaway3/DriveWire that referenced this pull request Jun 15, 2026
…re#5)

init_drives overwrote self.drives[i] in place, so reloading config
discarded any unflushed dirty sectors of the previous drive and leaked
its open file handle. Rework init_drives to iterate all slots, keep an
unchanged live mount (preserving its dirty buffers), flush+close a drive
whose slot is cleared, and route replacements through swap_drive.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
hathaway3 added a commit to hathaway3/DriveWire that referenced this pull request Jun 15, 2026
Move DrPitre#1/DrPitre#5/DrPitre#6/DrPitre#7/#8/#9/#10/#11/#12 and F2 to Fixed with commit refs,
note DrPitre#2/DrPitre#3 pending on-device verification, and record why DrPitre#4 (server
listen mode) and feature gaps F1/F3/F5 are deferred to an on-device pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Boisy Pitre and others added 6 commits June 16, 2026 14:53
OS-9 maintains distinct data and execution directories per process.
When the EXEC bit (0x04) is set in the mode byte of ChgDir, Open,
Create, MakeDir, or Delete, resolve and store the path against the
execution directory map (rfmCurrentExecDir) rather than the data
directory map (rfmCurrentDir).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Improve legibility of bordered controls on the dark dashboard by tinting
the Copy, Disconnect, Serial Port, and Baud Rate controls white, and give
Eject a readable tint in both enabled and disabled states. Also restrict
the Mount/Replace open panel to .dsk and .img files.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Populate the AppIcon asset with all macOS sizes (16/32/128/256/512 at 1x
and 2x) generated from the ObjC project's DriveWire Icon.icns.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Adds a formal spec section for OP_RFM ($D6), covering the envelope,
path-descriptor model, dual data/exec directory contexts, error
codes, and byte-level request/response layouts for all 13
sub-opcodes, plus the synthesized directory listing and file
descriptor formats. Derived from the Swift host implementation,
currently the only one that implements RFM.
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