ADP
ADP is Bizmuth’s text debugging protocol: one command per line over TCP, one text reply per command. You drive it by hand with nc, from a shell script, or from a Rhai script. It drives the same debugger engine as DeZog.
$ nc 127.0.0.1 11001
Bizmuth debug protocol (ADP) -- type 'help'
bp add pc 8000
bp #1 added
Servers
| Protocol | Port | For |
|---|---|---|
| ADP | 11001 | people, shells and scripts |
| DZRP | 11000 | the DeZog VS Code extension |
Both servers are off until asked for. --debug-protocol adp, dzrp or both starts them for one run; debug.protocol in the config, or Preferences ▸ Debug, keeps one on. --adp-port N moves ADP to another port, and --adp-bind ADDRESS sets the address it listens on.
bizmuth --machine next --debug-protocol adp --adp-bind 127.0.0.1bizmuth --machine next --debug-protocol adp--headless runs the machine with the servers bound and no window, until the process is killed. On the Next it attaches the SD image from the ROM folder when --sd names none.
⚠ The servers ask for no password, and listen on every network interface. Anything that can reach the port can read and write the guest’s memory, set its registers and run a script. --adp-bind 127.0.0.1 limits ADP to this machine; DZRP has no such option.
One client at a time. A second ADP connection is answered *busy -- ADP serves one client at a time and closed. A second DZRP connection is closed without a reply.
Message format
On connect the server sends a banner. Every block it sends, whether the banner, a reply or an event, ends with a blank line, so a client reads a multi-line reply, such as mem or bp list, up to the first empty line.
A block whose first line starts with * is an event, sent unasked. While the machine runs, a stop arrives as one:
*stopped pc=8000 reason=bp id=1With an SLD loaded the line ends at=FILE:LINE, the source line the stop address was assembled from. It is always the last field, because a path may contain a space. An assertion stop has reason=assert, and its at= names the line the assertion was written on. See Assertions.
Command names are not case-sensitive. A path containing a space is written in double quotes; inside quotes, \ takes the next character literally.
Numbers
An address or value in a command is hex: 8000, $8000 and 0x8000 are the same address, and #32768 is decimal. A count, such as the n of step n, is decimal unless prefixed. Output is upper-case hex without a prefix.
Where a command takes <addr|sym>, a symbol name from the loaded SLD works in its place. A number wins, so a label spelled like valid hex, such as beef, add or face, is read as the number; sym NAME says what a name resolves to. An EQU constant resolves to nothing, rather than to its value.
Execution
| Command | Effect |
|---|---|
c, continue | resume |
pause | stop at the current instruction |
s, step [n] | execute n instructions, 1 by default, stepping into calls |
next, stepover | step over a CALL, RST or block instruction; a single step otherwise |
runto ADDR | run to an address, through a breakpoint removed when it fires |
finish, fin | run to the current subroutine’s return address |
sb, stepback [n] | go back n saved states |
runframes N | run N frames, then pause; see runframes |
reset | reset the machine and leave it paused; see reset |
frame | the frame counter |
frametimes, ft [clear] | the time the emulator spent on each of the last 300 frames: mean, 99th percentile, worst, and how many overran the frame |
machine [KIND] | report the machine, or switch to 16k, 48k, 128k, plus2a, plus3 or next at the start of the next frame |
A machine switch is refused while an earlier one is still pending.
On the Next a step advances every clock a running frame does: the video beam, copper, DMA, CTC, UART, AY, mouse and tape move on with the instruction, at the CPU speed set. A step that completes a frame mixes its audio and redraws the screen, and a run resumed after steps finishes the frame from where they left it. Breakpoints and script events are not checked during a step.
Step-back restores saved states, so it is exact, but it reaches back only as far as the history: a state is saved before each step and pause, not while the machine runs freely. Past the first one, stepback answers that there is no earlier state.
runframes
Syntax: runframes N
runframes runs the machine for N complete frames and pauses it. N is a decimal count. If N is 0, the machine pauses at once. If N is greater than 100000, the command is refused. If a breakpoint, a watchpoint or pause stops the machine first, the run ends there. While a run is active, a second runframes is refused, and so is every command that changes the machine except pause and reset. The reply and status report the run as JSON: frames requested and executed, and why it ended.
With the machine paused at frame 7: runframes 3 answers {"active":true,"end_frame":7,"executed":0,"reason":null,"requested":3,"start_frame":7}. Once the run ends, status reports "frame_run":{"active":false,"end_frame":10,"executed":3,"reason":"frame_limit","requested":3,"start_frame":7}.
reset
Syntax: reset
reset resets the machine and leaves it paused. It clears the temporary breakpoints, the step-back history and any injected key or mouse input, ends a bounded run, and cancels a machine switch not yet made. The controller, breakpoints, watchpoints, symbols, page windows, the loaded script and the mounted SD cards are kept. An argument is refused.
reset answers: {"cleared":["temporary_breakpoints","step_history","injected_input"],"paused":true,"preserved":["controller","breakpoints","watchpoints","symbols","maps","script","mounted_media"],"reset":true}.
Inspection
| Command | Effect |
|---|---|
status | the machine’s run state, registers and paging as JSON; see status |
state | one line: pc sp af bc de hl ix iy i im iff and paging |
reg [NAME[=VALUE]] | every register, one register, or set one. Names: af bc de hl ix iy sp pc af' bc' de' hl' i r |
mem ADDR [LEN] | hex dump of LEN bytes, 16 by default, as the CPU would read them |
mem write ADDR BYTES… | write bytes, as the CPU would: mem write 8000 DE AD BE EF |
pmem PAGE:OFFSET [LEN], pmem write PAGE:OFFSET BYTES… | read or write a physical page of Next RAM; see pmem |
d, disasm ADDR [N] | disassemble N instructions, 8 by default, Z80N included |
nr REG | a Next register: the value software reads back, and the byte last written |
get PATH | any field of the machine state, as JSON; see State paths |
bt, backtrace | the call and interrupt stack, as recorded |
isr | whether the CPU is inside an interrupt handler |
overlays, ov | what is mapped at $0000–$3FFF: boot ROM, DivMMC, config mode, alternate ROM or an MMU page |
sprites, spr | the visible sprites: raw attributes, and their composed position, transform and pattern |
sprslots START [END] | sprite pattern slots as JSON, 256 bytes each |
sprram FILE.png | the sprite pattern RAM as a PNG, in the live palette |
screenshot, shot FILE.png [LAYERS] | the frame as a PNG; LAYERS is a comma list of ula, lores, tilemap, layer2 and sprites to capture only those |
bankshot PAGE FILE.png | a physical 8K page drawn as the screen; page 10 is bank 5 |
layer, ly [NAME] [on|off] | list the layers, or hide or show one for inspection |
dma | recent zxnDMA transfers |
copper, cop | the copper’s mode and position, and its list decoded |
pctrace FILE | append each instruction’s pc sp to a file from now on. Next only |
mem and disasm read through the live paging (boot ROM, Layer 2 mapping, config mode, DivMMC, Multiface), so they show what the CPU would fetch, not raw RAM. With an SLD loaded, a CALL, JP, JR, DJNZ or RST target gains ; label, named from the page mapped at the target.
layer hides a layer from the picture only. It never turns on a layer the program has turned off.
status
Syntax: status
status reports the machine as one JSON object: its kind, whether it is paused and why, the source line of the stop, the controller, the frame counter, any bounded run or machine switch, the registers, the page in each MMU slot, and the physical memory’s geometry. A register is a decimal number. A slot that holds no RAM page, such as one holding ROM, reports null, and so does every slot of a machine other than the Next. On a machine other than the Next, physical_memory is null.
With the Next paused at the NextZXOS menu: status answers {"banks":[null,null,10,11,4,5,14,15],"controller":"adp","frame":965,"frame_run":null,"machine":"next","machine_switch":null,"paused":true,"physical_memory":{"cpu_page_count":224,"page_count":480,"page_size":8192},"process_id":17826,"protocol":1,"registers":{"af":810,"af2":68,"bc":2426,"bc2":2585,"de":0,"de2":8,"hl":23611,"hl2":65359,"i":9,"iff":3,"im":1,"ix":56272,"iy":23610,"pc":3216,"r":20,"sp":23539},"stop":{"pc":3216,"reason":"pause","source":null}}.
pmem
Syntax: pmem PAGE:OFFSET [LEN] Syntax: pmem write PAGE:OFFSET BYTES…
pmem reads or writes the Next’s RAM by physical 8K page, without changing the CPU’s memory map. PAGE and OFFSET are hex, LEN is a count, 16 by default, and each byte is hex. The reply is JSON giving the page, the offset, the physical address and the bytes read or the count written. If LEN, or the number of bytes written, is outside 1 to 4096, the command is refused. If the range runs past the end of the page, the command is refused. If PAGE is past the end of the machine’s RAM, the command is refused. If the machine is running, pmem write is refused.
With the Next paused, page 0A is bank 5: pmem 0A:0000 8 answers {"bytes":[0,0,0,0,0,0,0,0],"offset":0,"page":10,"physical_address":81920,"view":"physical"}. pmem write 0A:0000 DE AD answers {"offset":0,"page":10,"physical_address":81920,"view":"physical","written":2}. pmem 0A:0000 2 then answers {"bytes":[222,173],"offset":0,"page":10,"physical_address":81920,"view":"physical"}.
Next only: other machines answer error: physical memory not supported by this machine.
Breakpoints, watchpoints and windows
bp, wp and map are on Breakpoints and Conditions, with the condition language a breakpoint carries.
Symbols
| Command | Effect |
|---|---|
sld, symbols [FILE] | load an SLD or a plain symbol export, or report what is loaded |
sym NAME | what a name resolves to: addr, bank, phys, traits and source line |
sld FILE replaces every loaded file. --sld on the command line can be repeated, for a program built as several separately linked images; the files become one table. Two files that place one name differently keep the later one and say so; two whose memory models disagree are refused.
> sym main_loop
main_loop addr=8123 bank=2 phys=008123 traits=used at=main.asm:40State and snapshots
| Command | Effect |
|---|---|
savestate FILE | write a compressed save state |
loadstate FILE | restore a save state |
loadsnapshot FILE | load a .sna, .snx, .z80 or .nex, then pause |
See Save States.
Input
| Command | Effect |
|---|---|
key TOKEN [FRAMES] | hold a key for FRAMES frames, 8 by default |
enter, down, up, left, right, space, y, n | the same, for that key |
mouse DX DY [BUTTONS] [WHEEL] | move the mouse and set the buttons held |
A token is any key of the Spectrum keyboard (a letter, a digit, enter, space, caps or sym), or several joined with + and pressed together: sym+p types ". down, up, left and right press 6, 7, 5 and 8, the keys NextZXOS’s menus read, so down, down, enter launches the third entry with no window. Keys advance only while the machine runs.
mouse takes signed decimal. DX and DY are movement since the last report, used by the next frame. BUTTONS is a held level, 1 left, 2 right and 4 middle, that stays until a later mouse changes it, so mouse 0 0 1 presses the left button and mouse 0 0 0 releases it. Movement adds to whatever the window contributes, and the guest sees it scaled by the DPI setting in nextreg $0A.
Serial faults
| Command | Effect |
|---|---|
serial | what is armed |
serial drop [N] | discard the next N received bytes, 1 by default |
serial stall MS | hold the receiver, so arriving bytes overrun |
serial delay MS | stop asking the far end for bytes |
serial framing-error | latch the framing error |
serial break on|off | hold or release a break |
serial clear | disarm everything, the break included |
These are link failures that cannot be produced on demand on real hardware. They act on the UART’s receive path, so they work against every --esp endpoint. Nothing is armed until you arm it, and a save state carries none of it.
drop costs the guest the bytes and not the time: each still spends its frame period on the wire. stall holds the receive register, so the next byte to arrive finds it full and sets the real overrun bit, $133B bit 2. delay leaves the connection open and the far end silent.
Durations are milliseconds of a 28 MHz system clock; serial reports what remains in cycles.
⚠ Only the ESP’s UART 0 carries faults. The Pi’s UART has no endpoint. A machine with no UART answers serial: this machine has no UART.
SD cards
sd
Syntax: sd [status] Syntax: sd mount SLOT PATH MODE Syntax: sd unmount SLOT
sd reports the Next’s SD cards, or inserts or removes one. SLOT is 0 or 1. PATH is an SD image file. MODE is read_only_overlay, which keeps the guest’s writes in memory and leaves the file unchanged, or writable, which writes them to the file. The reply is JSON with one entry per slot, null for an empty one: the image’s path and size, its mode, whether the overlay holds changes, and the sectors read and written. If the machine is running, mount and unmount are refused. If SLOT is not 0 or 1, the command is refused. If the image cannot be opened, the card already in the slot stays.
With a card in slot 0 opened read-only: sd status answers {"slots":[{"bytes":1073741824,"dirty":false,"overlay_sectors":0,"path":"/Users/me/Documents/Bizmuth/cspect-next-1gb.img","read_only":true,"reads":473,"slot":0,"valid":true,"write_policy":"read_only_overlay","writes":0},null],"supported":true}.
Next only: other machines answer error: SD mounting not supported by this machine.
Audio capture
audio
Syntax: audio [status] Syntax: audio capture SOURCE FRAMES PATH Syntax: audio finish
audio capture records FRAMES frames of one audio source to a WAVE file, running the machine for those frames and then pausing it. SOURCE is mixed, the stereo output as played, or beeper, ay or dac, each recorded in mono before mixing. FRAMES is a decimal count from 0 to 10000. PATH names a file that does not yet exist. Samples are 32-bit floating point at the audio output rate, 44100 Hz when there is no audio device. audio finish ends a capture at once, and audio or audio status reports the sources and the last capture. If the machine is running, capture and finish are refused. If a capture is already active, another is refused. If the machine has no such source, or PATH exists, the capture is refused. If a breakpoint, a watchpoint or pause stops the machine first, the capture ends there and the file keeps what was recorded.
The Next offers all four sources. The 128K, +2A and +3 offer mixed, beeper and ay. The 16K and 48K offer mixed and beeper.
With the 128K paused at frame 2: audio capture ay 5 /tmp/tune.wav runs five frames and pauses, and audio status then answers {"capture":{"active":false,"channels":1,"end_frame":7,"error":null,"executed_frames":5,"machine":"128k","path":"/tmp/tune.wav","reason":"frame_limit","requested_frames":5,"sample_format":"float32_le","sample_frames":4408,"sample_rate":44100,"source":"ay","source_stage":"source_output","start_frame":2},"sources":["mixed","beeper","ay"],"supported":true}.
Control
control
Syntax: control [status] Syntax: control acquire Syntax: control release Syntax: control handoff NAME
control names the one debugger client allowed to change the machine. A text ADP client acts under the name adp. control status reports the holder and the asking client. control acquire takes control and pauses the machine. control release gives control up and pauses the machine. control handoff NAME passes control to NAME and pauses the machine; NAME is up to 128 letters, digits, /, :, - or _. If another client holds control, acquire is refused. If the asking client does not hold control, release and handoff are refused.
While a client holds control, a command from any other client that changes the machine is refused with control error: control held by NAME, and a command that only reads is answered. DeZog’s changes are refused too, and the debugger panel’s. The desktop’s keyboard, pad and mouse are ignored, and so are its menu actions that change the machine. When nobody holds control, every client may change the machine. When the holder’s connection closes, control is released and the machine pauses.
With nobody holding control: control status answers {"controller":"adp","owner":null}. control acquire answers {"controller":"adp","owner":"adp"}.
Structured requests
A line that starts with { is a structured request: a JSON object with id, a number the reply repeats, command, one ADP command as it would be typed, and optionally controller, the name the request acts under, adp by default. The reply is one JSON object. On success it is {"id":…,"result":{"text":…,"status":…,"data":…}}, where text is the plain reply, status is as status reports it, and data is the reply’s content as JSON, or null for a command that has none. On failure it is {"id":…,"error":{"code":…,"message":…}}.
| Code | Meaning |
|---|---|
invalid_request | not a JSON object of the three fields |
invalid_command | empty, more than one command, or quit |
invalid_controller | the controller name is desktop or dzrp |
invalid_arguments | the command’s arguments were refused before it ran |
unsupported | the command does not apply to this machine |
controller_conflict | another client holds control, or an mcp/ client has not acquired it |
command_failed | the command ran and failed |
desktop and dzrp are refused as controller names, since they belong to the window and to DeZog. A name beginning mcp/ must hold control before its request can change the machine. quit is refused; closing the connection ends the session.
After a client’s first structured request, its events arrive as JSON, each with a sequence number counting up from 1:
| Event | Fields |
|---|---|
stopped | pc, reason, breakpoint_id, source, frame, and assertion for a broken claim |
script_log | pc, frame, handler, message, level (info or error), original_bytes, truncated |
lost | count of events dropped because the client read too slowly |
A script log line longer than 32768 bytes is cut short, and truncated says so. A text client receives no script log events.
With control held by adp: {"id":2,"controller":"mcp/other","command":"reg pc=8000"} answers {"error":{"code":"controller_conflict","message":"control error: acquire control before issuing mutations"},"id":2}. {"id":3,"controller":"desktop","command":"status"} answers {"error":{"code":"invalid_controller","message":"desktop and dzrp are reserved controller names"},"id":3}. {"id":4,"command":"script eval \"40 + 2\""} answers with "data":{"value":"42"} and "text":"42".
Script commands
| Command | Effect |
|---|---|
script load FILE | load a Rhai script |
script eval EXPR | evaluate a Rhai expression against the live machine |
script list | the handlers the loaded script defines |
help, ? | the command list |
q, quit | close this connection; the machine keeps running unless this client holds control, see control |
See Rhai Scripting.
State paths
get PATH walks the machine’s state, the fields a save state records, and prints the value as JSON. A path is dotted, with [INDEX] for an array element:
> get bus.memory.mmu.slots
[255,255,10,11,4,5,0,1]
> get bus.next_regs.regs[192]
8Useful roots are cpu, bus, bus.memory.mmu.slots, bus.next_regs.regs (the bytes last written to each Next register), bus.layer2, bus.lores and bus.sprites. Large RAM blocks are left out. A path that does not exist is answered with the field it could not find, so the tree can be explored a step at a time.