Python API Reference

Auto-generated from module docstrings.

ROM builder

Build a 32 KB ROM image for the Pico-as-ROM 65C02 system.

The 32 KB image maps to CPU addresses $8000-$FFFF. File offset $0000 = CPU address $8000 File offset $7FFC = CPU address $FFFC (reset vector low byte) File offset $7FFF = CPU address $FFFF

exception romulan.build_rom.InvalidInstructionError(message)[source]

Bases: Exception

Raised when a byte sequence contains an undefined 65C02 opcode.

__str__()[source]

Return the error message.

Returns:

The message passed to the constructor.

exception romulan.build_rom.SkippedInstructionError(message)[source]

Bases: Exception

Raised when a required instruction address is missing from the ROM dump.

__str__()[source]

Return the error message.

Returns:

The message passed to the constructor.

romulan.build_rom.cpu_to_offset(cpu_addr)[source]

Convert a CPU address ($8000-$FFFF) to a file offset (0-$7FFF).

Parameters:

cpu_addr (int) – A 65C02 address in the ROM region.

Returns:

The corresponding byte offset in a 32 KB ROM file.

Raises:

ValueError – If cpu_addr is outside $8000$FFFF.

Return type:

int

romulan.build_rom.parse_hex_file(path)[source]

Parse an annotated hex dump file into a dict of CPU address -> byte.

Expected line format:

0x0000   0x18   @ CLC
0x0001   0xA9   @ LDA 0x5

File addresses are in the range 0x00000x7FFF and are mapped to CPU addresses by adding ROM_BASE_ADDR (0x8000). Everything after @ on a line is treated as a comment and ignored.

Parameters:

path (Path) – Path to the annotated hex dump file.

Returns:

A mapping from CPU address to byte value.

Raises:

ValueError – If a line cannot be parsed or an address is out of range.

Return type:

dict[int, int]

romulan.build_rom.verify_instructions(data, error_list)[source]

Validate opcode bytes and append errors instead of raising immediately.

Distinguishes undefined opcodes from immediate or address operands that follow a valid instruction byte. Errors are appended to error_list and ERROR_COUNTER is incremented.

Parameters:
  • data (List[int]) – Byte values parsed from the ROM image.

  • error_list (List) – List that receives human-readable error messages.

Return type:

None

romulan.build_rom.verify_instruction_order(data, error_list)[source]

Check that file addresses are contiguous (no gaps before the vectors).

Parameters:
  • data (List[int]) – File-offset addresses from the parsed ROM dump.

  • error_list (List) – List that receives human-readable error messages.

Return type:

None

romulan.build_rom.error_processing(data_dict)[source]

Run opcode and address-order validation on a parsed ROM dump.

Parameters:

data_dict (Dict[int, int]) – Mapping of CPU address to byte value from parse_hex_file().

Returns:

A list of human-readable error messages (empty when validation passes).

Return type:

List

romulan.build_rom.build_rom(input_path, output_path)[source]

Parse a hex dump file and write a 32 KB ROM binary.

Runs opcode and address-order validation before writing. Unused bytes are filled with $EA (NOP). The reset and IRQ/BRK vectors at $FFFC$FFFF must be present in the input or the build fails.

Side effects:

Resets ERROR_COUNTER, may print errors and call sys.exit(1), creates parent directories for output_path, writes the binary file, and prints a summary to stdout.

Parameters:
  • input_path (Path) – Path to the annotated hex dump file.

  • output_path (Path) – Destination path for the 32 KB .bin file.

Raises:

ValueError – If parsing fails or required vectors are missing.

Return type:

None

Plain-text upload

Plain-text ROM upload for the Pico-as-ROM 65C02 firmware.

Uploads a 32 KB ROM image over USB serial using the firmware’s loadbin handshake: the host sends loadbin, the Pico responds with OK send 32768 bytes, the host streams 32,768 raw bytes, and the Pico acknowledges with loaded 32768 bytes.

Also handles CPU reset sequencing and ROM-emulator toggling so the CPU restarts cleanly on the new image.

romulan.upload_rom.find_pico_port()[source]

Auto-detect a Raspberry Pi Pico serial port.

Tries USB vendor ID 0x2E8A (Raspberry Pi) first, then falls back to common port name patterns on Linux, macOS, and Windows.

Returns:

The device path of the detected Pico (e.g. /dev/ttyACM0).

Raises:

RuntimeError – If zero or more than one matching port is found.

Return type:

str

romulan.upload_rom.read_until(ser, needle, timeout=3.0)[source]

Read from a serial port until a substring appears or time runs out.

Non-empty lines received are echoed to stdout prefixed with <<.

Parameters:
  • ser (Serial) – An open serial.Serial connection.

  • needle (str) – Substring to search for in the accumulated receive buffer.

  • timeout (float) – Maximum wait time in seconds. Defaults to 3.0.

Returns:

The full receive buffer once needle is found.

Raises:

TimeoutError – If needle is not seen before the deadline.

Return type:

str

romulan.upload_rom.upload(port, path)[source]

Upload a 32 KB ROM image via the plain-text loadbin protocol.

Side effects:

Opens the serial port at 115200 baud, asserts CPU reset (r0), disables ROM emulation (roms), streams the binary, re-enables ROM and clock (rom, c100), releases reset (r1), captures ~5 s of live output, then closes the port.

Parameters:
  • port (str) – Serial device path for the Pico.

  • path (Path) – Path to the ROM binary; must be exactly ROM_SIZE bytes.

Raises:
  • SystemExit – If path is not exactly 32 KB (via sys.exit).

  • TimeoutError – If the firmware does not respond during handshake.

Return type:

None

romulan.upload_rom.main()[source]

CLI entry point for standalone upload-rom.py usage.

Accepts optional [PORT] [BIN] arguments; auto-detects the port and defaults to bin/rom.bin when omitted.

Side effects:

Delegates to upload(), which opens the serial port and drives the Pico firmware.

Return type:

None

Protocol v1 helpers

Wire-format helpers for the Pico-as-ROM hardware API (protocol v1).

This module defines the version-1 JSON envelope used to talk to the Pico firmware: constants describing size limits, the dataclasses that model firmware responses and events, a request builder, and parsers that validate and unpack each kind of frame. It contains no I/O; romulan.hardware_api uses these helpers to build commands and interpret replies.

exception romulan.protocol_v1.ProtocolV1Error(message, *, error=None, detail=None)[source]

Bases: Exception

Invalid frame or firmware error response.

Parameters:
  • message (str)

  • error (str | None)

  • detail (str | None)

Return type:

None

class romulan.protocol_v1.CycleEvent(seq, addr, data, rw)[source]

Bases: object

A single captured 65C02 bus cycle.

Parameters:
  • seq (int)

  • addr (str)

  • data (str)

  • rw (int)

seq

Monotonic sequence number assigned by the firmware.

Type:

int

addr

Address on the bus, as a hex string.

Type:

str

data

Data byte on the bus, as a hex string.

Type:

str

rw

Read/write flag (1 = read, 0 = write).

Type:

int

class romulan.protocol_v1.DoneEvent(ok, reason, cycles, addr)[source]

Bases: object

Terminating event that ends a bus-capture stream.

Parameters:
  • ok (bool)

  • reason (str)

  • cycles (int)

  • addr (str)

ok

Whether the capture completed successfully.

Type:

bool

reason

Why capture stopped (e.g. "stp" or "max_cycles").

Type:

str

cycles

Total number of cycles the firmware reports it captured.

Type:

int

addr

Address at which the capture stopped, as a hex string.

Type:

str

class romulan.protocol_v1.ReadResult(ok, reason, cycles=<factory>, stopped_addr='')[source]

Bases: object

Aggregated result of a capture: the done status plus all cycles.

Parameters:
  • ok (bool)

  • reason (str)

  • cycles (list[CycleEvent])

  • stopped_addr (str)

ok

Whether the capture completed successfully.

Type:

bool

reason

Why capture stopped.

Type:

str

cycles

The captured CycleEvent items in order.

Type:

list[romulan.protocol_v1.CycleEvent]

stopped_addr

Address at which the capture stopped, as a hex string.

Type:

str

class romulan.protocol_v1.StatusResponse(phi2_hz, rom_active, reset_asserted, last_addr, read_active, monitor_enabled, upload_active=False)[source]

Bases: object

Snapshot of firmware/hardware state returned by the status command.

Parameters:
  • phi2_hz (float)

  • rom_active (bool)

  • reset_asserted (bool)

  • last_addr (str)

  • read_active (bool)

  • monitor_enabled (bool)

  • upload_active (bool)

phi2_hz

Current CPU clock (PHI2) frequency in hertz.

Type:

float

rom_active

Whether the ROM emulator is driving the bus.

Type:

bool

reset_asserted

Whether the CPU RESET line is asserted.

Type:

bool

last_addr

Last address seen on the bus, as a hex string.

Type:

str

read_active

Whether a bus-capture read is currently running.

Type:

bool

monitor_enabled

Whether the ASCII monitor output is enabled.

Type:

bool

upload_active

Whether a ROM upload is in progress.

Type:

bool

class romulan.protocol_v1.UploadProgress(action, received, expected=32768, offset=None, reset_vector=None)[source]

Bases: object

Progress reported by the firmware during a ROM upload.

Parameters:
  • action (str)

  • received (int)

  • expected (int)

  • offset (int | None)

  • reset_vector (str | None)

action

Which upload phase this reply corresponds to ("begin", "chunk", or "commit").

Type:

str

received

Total number of bytes received so far.

Type:

int

expected

Total number of bytes expected (defaults to ROM_SIZE).

Type:

int

offset

Byte offset of the acknowledged chunk, if reported.

Type:

int | None

reset_vector

Reset vector read back after commit, if reported.

Type:

str | None

romulan.protocol_v1.build_request(cmd, *, req_id=None, **fields)[source]

Build a v1 request envelope for a firmware command.

Adds the protocol version and command name, and (as a convenience) renames the reserved assert_reset keyword to the wire field assert.

Parameters:
  • cmd (str) – The command name (e.g. "status" or "upload_rom").

  • req_id (str | None) – Optional request id echoed back by the firmware.

  • **fields (Any) – Extra command-specific fields to include in the envelope.

Returns:

A dict ready to be serialized to JSON and sent to the Pico.

Return type:

dict[str, Any]

romulan.protocol_v1.parse_frame(raw)[source]

Parse a JSON frame and validate the v1 envelope.

Parameters:

raw (bytes | str) – The raw frame payload as bytes (UTF-8) or str.

Returns:

The decoded frame as a dict.

Raises:

ProtocolV1Error – If the payload is not valid JSON, is not a JSON object, or declares an unsupported protocol version.

Return type:

dict[str, Any]

romulan.protocol_v1.parse_command_response(msg)[source]

Return a command acknowledgement, or raise on a firmware error.

Parameters:

msg (dict[str, Any]) – A parsed frame expected to be a command response (not an event).

Returns:

The same frame, unchanged, when it represents a successful ack.

Raises:

ProtocolV1Error – If the frame is an event, reports ok false, or declares an unsupported protocol version.

Return type:

dict[str, Any]

romulan.protocol_v1.parse_cycle_event(msg)[source]

Parse a cycle event frame into a CycleEvent.

Parameters:

msg (dict[str, Any]) – A parsed frame expected to be a cycle event.

Returns:

The captured bus cycle.

Raises:

ProtocolV1Error – If the frame is not a cycle event or the version is unsupported.

Return type:

CycleEvent

romulan.protocol_v1.parse_done_event(msg)[source]

Parse a done event frame into a DoneEvent.

Parameters:

msg (dict[str, Any]) – A parsed frame expected to be a done event.

Returns:

The terminating capture event.

Raises:

ProtocolV1Error – If the frame is not a done event or the version is unsupported.

Return type:

DoneEvent

romulan.protocol_v1.parse_status(msg)[source]

Parse a status command response into a StatusResponse.

Parameters:

msg (dict[str, Any]) – A parsed frame expected to be a successful status response.

Returns:

The decoded status snapshot.

Raises:

ProtocolV1Error – If the frame reports an error or the version is unsupported.

Return type:

StatusResponse

romulan.protocol_v1.parse_upload_response(msg)[source]

Parse an upload_rom command response into an UploadProgress.

Parameters:

msg (dict[str, Any]) – A parsed frame expected to be a successful upload_rom reply (begin, chunk, or commit).

Returns:

The decoded upload progress.

Raises:

ProtocolV1Error – If the frame reports an error or the version is unsupported.

Return type:

UploadProgress

Hardware API client

High-level client for the Pico-as-ROM 65C02 hardware over the v1 serial protocol.

This module provides HardwareAPI, a client-side wrapper around the framed JSON protocol implemented by the Pico firmware. It handles the low-level ENQ/STX/ACK/EOT framing, encodes commands built by romulan.protocol_v1, and exposes friendly methods for the common operations: querying the current CPU address, asserting/releasing reset, toggling the ASCII monitor, reading status, uploading a ROM image, and capturing bus cycles.

Opening a HardwareAPI immediately opens the underlying serial port. The class supports the context-manager protocol so the port is always closed:

with HardwareAPI("/dev/ttyACM0") as api:
    api.upload_rom(rom_bytes)
exception romulan.hardware_api.HardwareAPIError[source]

Bases: Exception

Raised when the Pico responds with NACK or a frame error occurs.

class romulan.hardware_api.CaptureResult(reason, cycles=<factory>)[source]

Bases: object

Result of a bus capture (read until STP).

Parameters:
  • reason (str)

  • cycles (list[dict[str, Any]])

reason

Why the capture stopped (e.g. "stp" or "max_cycles").

Type:

str

cycles

One dict per captured bus cycle, each with seq, addr, data, and rw keys.

Type:

list[dict[str, Any]]

__repr__()[source]

Return a concise debug representation.

Returns:

A string showing the stop reason and cycle count.

Return type:

str

classmethod from_read_result(result)[source]

Build a CaptureResult from a protocol ReadResult.

Parameters:

result (ReadResult) – The parsed read result returned by the capture loop.

Returns:

A CaptureResult with each cycle flattened into a plain dict.

Return type:

CaptureResult

class romulan.hardware_api.HardwareAPI(port, baudrate=115200, timeout=3.0, verbose=False)[source]

Bases: object

Context-manager compatible hardware API for Pico-as-ROM firmware v1.

Each instance owns a single serial connection to the Pico. The connection is opened as soon as the object is constructed, and closed by close() or on exit from a with block.

Parameters:
  • port (str)

  • baudrate (int)

  • timeout (float)

  • verbose (bool)

port

The serial device path the client is connected to.

baudrate

The serial baud rate in use.

timeout

Default per-read timeout in seconds.

verbose

When True, protocol traffic is logged to stderr.

close()[source]

Close the serial port if it is open.

Safe to call multiple times; subsequent calls are no-ops.

Return type:

None

__enter__()[source]

Enter a with block and return this client.

Return type:

HardwareAPI

__exit__(exc_type, exc_val, exc_tb)[source]

Exit a with block, closing the serial port.

Parameters:
  • exc_type (Any)

  • exc_val (Any)

  • exc_tb (Any)

Return type:

None

property ser: Serial

The live serial.Serial connection.

Returns:

The open serial connection.

Raises:

HardwareAPIError – If the port has already been closed.

request_addr()[source]

Ask the firmware for the address currently on the CPU bus.

Returns:

The current CPU address as an integer.

Raises:
  • HardwareAPIError – If the response is missing the addr field or the firmware reports an error.

  • TimeoutError – If the Pico does not respond in time.

Return type:

int

reset(assert_reset)[source]

Assert or release the 65C02 RESET line.

Side effects:

Changes the CPU run state: asserting reset halts the CPU, while releasing it lets the CPU start executing from its reset vector.

Parameters:

assert_reset (bool) – True to hold the CPU in reset, False to release it.

Raises:
  • HardwareAPIError – If the firmware reports an error.

  • TimeoutError – If the Pico does not respond in time.

Return type:

None

monitor(enable)[source]

Enable or disable the firmware’s unstructured ASCII monitor output.

The ASCII monitor must be disabled before framed operations such as upload_rom() and read_until_stp(), otherwise its free-form text would corrupt the framed protocol stream.

Parameters:

enable (bool) – True to turn the monitor on, False to turn it off.

Raises:
  • HardwareAPIError – If the firmware reports an error.

  • TimeoutError – If the Pico does not respond in time.

Return type:

None

status()[source]

Query the firmware for its current status.

Returns:

A StatusResponse describing the clock frequency, ROM/reset/monitor state, and last bus address.

Raises:
  • HardwareAPIError – If the firmware reports an error.

  • TimeoutError – If the Pico does not respond in time.

Return type:

StatusResponse

upload_rom(data)[source]

Upload a full 32 KB ROM image to the Pico in a begin/chunk/commit sequence.

The image is sent as base64-encoded chunks and committed at the end. The reset vector is reported back by the firmware after commit.

Side effects:

Disables the ASCII monitor and flushes serial input before transferring, so the framed protocol is not corrupted.

Parameters:

data (bytes) – The ROM image; must be exactly ROM_SIZE (32 KB) bytes.

Returns:

A dict with keys ok, bytes (bytes committed), reset_vector, and expected (expected total size).

Raises:
  • ValueError – If data is not exactly ROM_SIZE bytes.

  • HardwareAPIError – If a chunk stalls or the firmware reports an error.

  • TimeoutError – If the Pico does not respond in time.

Return type:

dict[str, Any]

read_until_stp(max_cycles=10000, frame_timeout=12.0)[source]

Capture CPU bus cycles until the CPU executes STP or a limit is hit.

Streams cycle events from the firmware and stops on the terminating done event (reached when the CPU halts via STP or max_cycles is reached).

Side effects:

Disables the ASCII monitor and flushes serial input before starting the capture.

Parameters:
  • max_cycles (int) – Maximum number of bus cycles to capture before the firmware stops. Defaults to 10000.

  • frame_timeout (float) – Per-frame read timeout in seconds while waiting for cycle/done events. Defaults to READ_FRAME_TIMEOUT.

Returns:

A CaptureResult with the stop reason and captured cycles.

Raises:
  • HardwareAPIError – If the read is rejected or an unexpected frame arrives.

  • TimeoutError – If the Pico stops sending frames before done.

Return type:

CaptureResult

romulan.hardware_api.open_hardware_api(port=None)[source]

Open a HardwareAPI, auto-detecting the Pico port if needed.

Side effects:

Opens the serial port via HardwareAPI.

Parameters:

port (str | None) – Explicit serial device path. If None, the port is auto-detected with find_pico_port().

Returns:

A connected HardwareAPI instance.

Raises:

HardwareAPIError – If no Pico serial port can be found.

Return type:

HardwareAPI

CLI entry point

Romulan CLI entry point.

Usage:

romulan input.txt –build –upload [–port PORT] romulan hardware <subcommand> …

Examples

romulan program.txt –build # Build bin/rom.bin only romulan program.txt –build –upload # Build and upload romulan –upload # Upload existing bin/rom.bin romulan program.txt –upload –port /dev/ttyACM0 romulan hardware upload bin/rom.bin –port /dev/ttyACM0 romulan hardware capture –max-cycles 500 romulan hardware monitor –disable romulan hardware reset –assert romulan hardware request-addr

romulan.main.create_parser()[source]

Build the argument parser for the default build/upload workflow.

Returns:

A configured argparse.ArgumentParser accepting the input file plus the --build, --upload, --output, and --port options.

Return type:

ArgumentParser

romulan.main.main()[source]

Entry point for the romulan command-line tool.

Routes to the hardware sub-command when it is the first argument; otherwise runs the default workflow, which builds a ROM image from an input file (--build) and/or uploads it to the Pico (--upload).

Side effects:

Parses sys.argv, may read/write files, and may open the serial port to talk to the Pico. Exits via sys.exit() (or parser.error) on invalid arguments or failures.

Return type:

None

Package

Romulan — host client for the Piclone 65C02 system.

Romulan assembles 32 KB ROM images from annotated hex dumps and communicates with the Raspberry Pi Pico firmware over USB serial. Submodules: