Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

USB protocol

nord-usb speaks the protocol Nord Sound Manager uses, worked out from USB captures. It moves bytes to and from the instrument. nord-format owns the bytes.

What works

Verified on an instrument from macOS: inventory, object info, dependencies, geometry, focus, walking the occupied slots, reading and writing programs and set lists, moving, deleting, renaming, duplicating and selecting slots, reading and writing live slots and settings in place, and reading, deleting and writing sample and piano libraries. From Linux, reads and a multi-chunk sample write are verified, and every recorded request is byte-identical to its macOS counterpart. The WebUSB backend is verified for reads and writes from Chrome. Windows passes the replay tests but has not been run against an instrument.

A bundle is read with these primitives alone (nord_usb::bundle): a set list’s dependency rows name its programs by slot, a program’s name its pianos and samples, and each of those is found by name, since an object’s info does not carry the id programs know it by. A piano’s or sample’s info does carry when it was last changed.

Not implemented: writing a bundle to the instrument, firmware updates, and relink.

The wire

Every message is a length-prefixed frame of big-endian 32-bit words with a CRC-16 trailer:

┌────────┬─────────┬───────────┬─────────┬───────────────┬───────┐
│ length │ service │ subsystem │ command │ args…         │ crc16 │
└────────┴─────────┴───────────┴─────────┴───────────────┴───────┘

The CRC is CRC-16/CCITT-FALSE. A response carries the request’s command plus one, with a status word inserted before the echoed arguments. Two things bite. Request codes are not reliably even, so direction is recorded at decode time rather than inferred. And operations are generic primitives parameterized by object class, so one delete command serves every class. The wire carries a class as a position in the partition table the instrument reports, and each model orders its table its own way: the Electro 5 keeps programs at 4. Classes finds each class by its partition’s name (Piano, Samp Lib, Program, Set List, Live, Settings, and on a Stage 4 Organ Preset, Piano Preset, Synth Preset), and a partition with any other name is an unknown class, which drawbar only lists. The table and every partition’s banks are read before any session: the instrument answers both with no HELLO and no session open. The piano and sample libraries refuse rename and copy, and refuse a write named like an object they already hold, matching case.

The instrument reads a message until a short packet ends it. A frame that is an exact multiple of the packet size needs a zero-length packet after it, or the device never answers.

The closing exchanges of a transaction clear the instrument’s progress display. Abandoning a transaction after a progress label has been sent leaves the device stuck until it is power-cycled, which is why every session closes on the error path.

Writing over a slot

The instrument does not overwrite an occupied program, set list, sample or piano slot in place, so a write there deletes the occupant first. drawbar and nord both read the occupant back before the delete and write it back if the new write fails. Both read it into a file under a .partial name, sync it, check its length and checksum, and only then rename it, all before the delete, so a process that dies with the slot empty leaves the whole occupant on disk, and one that dies during the read leaves only a .partial file and the slot as it was. nord writes its rescue file in the working directory, nord-rescued-<bank>-<slot>.<tag>, and deletes that file once the slot holds what it should; drawbar’s handling is under Reading by range. Live slots and settings are written in place.

Writing settings reloads the selected program, so unsaved panel changes are lost. After a write or rename succeeds, drawbar asks for the focus, and if the panel is on the slot just written, selects it again so the keyboard plays the new sound. Nothing is reloaded after a failed write. The instrument reports no progress percentage; progress shows on its own display.

Layering

ModuleRole
wireFraming and codec. No I/O.
transportThe byte pipe, and the only code that touches a device.
sessionThe transaction every operation runs inside.
opTyped operations.
deviceAn instrument as a value: session brackets and its geometry.

Features

FeatureDefaultGives
nusbyesDesktop backend for macOS, Linux and Windows, in pure Rust.
webBrowser backend over WebUSB. Chrome and Edge only.
replayDrive the protocol from captures, with no hardware.
blockingBlock on the async API from synchronous code.
corpusTests against the private capture corpus. Implies replay.
fault-injectionDeliberate protocol faults for research. A wrong value can stall the instrument’s endpoints.

WebUSB handles are not Send, so neither is the Transport trait, which keeps the crate free of any particular async runtime. Building web needs --cfg=web_sys_unstable_apis, which crates/.cargo/config.toml supplies when Cargo runs from crates/.

A write reads its file through the FileSource trait, one transfer chunk at a time, so memory does not grow with the file. The body is read twice: once to check its checksum before the first frame, and again to send it. If the bytes change in between, the last chunk is held back and the write fails unfinished. envelope::verify runs the first of those checks alone, for a caller that must know the file is whole before it touches the instrument.

A read hands its file to the FileSink trait the same way: op::read_into writes each chunk of the body as it arrives, behind the room its CBIN header takes, and the header last, once the body’s CRC-32 is known. It returns the slot’s object info and that CRC-32. op::read_program is read_into into memory, so the two send the same frames. A sink that keeps nothing, std::io::sink(), takes a slot’s checksum without holding its body.

The device reports a body CRC-32 in object info for programs and set lists, and 0xffffffff for pianos, samples and Settings, so the only way to learn what a piano or sample slot holds is to read it. Its object info does report the body’s length, format tag, version and name.

The API documentation is on docs.rs, and Testing covers the replay suite.