Serial Port Guide
Videomancer exposes a text-based command interface over its USB-C Device port. Any serial terminal or scripting language with serial port support can send commands and receive responses. This opens the door to automation, preset management, integration with custom software, and deep system introspection — all from a simple terminal window.
This is the same interface LZX Connect uses for firmware updates, program library installs, settings backups, and sideloading. This guide covers firmware 1.0.1 and the 1.0.2 beta, and LZX Connect 1.3.12. Commands that need a newer firmware than 1.0.0 are marked in the text; see Version History at the end.
Connection Setup
-
Connect Videomancer to your computer via the front-panel USB-C Device port.
-
Videomancer enumerates as a USB CDC ACM virtual COM port.
-
Open the port at any baud rate — the baud rate setting is ignored for USB CDC. Common tools include:
-
macOS / Linux:
screen /dev/ttyACM0(macOS:/dev/cu.usbmodem*),picocom,minicom, or Python’spyserial -
Windows: PuTTY, Tera Term, or any COM port terminal
-
No login or authentication is needed — Videomancer accepts commands immediately.
Important
Only one program can hold the port at a time. Quit LZX Connect before opening a terminal, and close your terminal before using LZX Connect again. LZX Connect no longer has a built-in serial console (removed in 1.3.4); use a terminal or a script for the commands in this guide.
Wire Protocol
Sending Commands
Send a newline-terminated (\n) ASCII string:
<command> [arguments]\n
-
Lines are limited to 511 bytes before the newline. A longer line is discarded and answered with a
buffer_overflowerror. -
A carriage return (
\r) is treated as a newline, so\r\nline endings work. Empty lines are ignored. -
Non-printable characters below
0x20(except Backspace and Delete) are silently dropped. -
Backspace (
0x08) and Delete (0x7F) erase the most recent character. -
Command words are case-sensitive (
version, notVERSION). Program, preset, timing and language names are matched case-insensitively. -
Videomancer does not echo what you type. Enable local echo in your terminal if you want to see your input.
Receiving Responses
| Prefix | Meaning | Format |
|---|---|---|
|
Success |
|
|
Error |
|
(none) |
Log output |
Plain text line (when logging is active) |
The topic names the command family (program, fs, settings, …), not the full command. The payload is a short string (e.g., ok) or a JSON object. All responses are newline-terminated.
Most commands answer with exactly one line. The exceptions:
-
cpusends one@cpu:line per core. -
reboot bootloadersends@reboot:bootloaderand then the port disappears. -
The binary transfers (
fs put,program stream,program upload) send areadyline, wait for the raw bytes, then send a final line.
Videomancer can also send lines you did not ask for:
-
Log lines (no prefix) — boot messages and diagnostics, depending on
log leveland the build. -
MIDI monitor lines (no prefix) — while
midi monitoris on. -
@program:invalidated:variant_mismatch— a program you streamed withprogram streamstopped running because the video timing crossed between SD and HD. Stream it again.
A robust client reads lines until it sees the @<topic>: it expects or a ! line, and skips everything else. Drain any pending output after opening the port.
Error Codes
Error codes are 32-bit numbers. The top 6 bits name the firmware domain that raised the error and the low 14 bits are the error within that domain:
domain = code >> 26 # 22 = shell, 23 = presets
item = code & 0x3FFF
Shell errors (domain 22):
| Code | Item | Name | Description |
|---|---|---|---|
|
1 |
|
Unrecognized command (the message echoes the line) |
|
2 |
|
Malformed or out-of-range arguments (the message shows usage) |
|
3 |
|
Required service not ready, or no program loaded |
|
4 |
|
Command line longer than 511 bytes |
|
5 |
|
CDC port not connected (internal; never reaches the host) |
|
6 |
|
File, directory, program or setting not found |
|
7 |
|
Filesystem or settings write failure, transfer timeout |
|
8 |
|
No SD card, or the card is exposed over USB storage |
|
9 |
|
Path longer than 200 characters |
|
10 |
|
Path does not start with |
|
11 |
|
Busy with a transfer, Developer Mode off, or wrong control mode |
Preset errors (domain 23):
| Code | Item | Name | Description |
|---|---|---|---|
|
1 |
|
No program loaded |
|
2 |
|
Preset index out of range |
|
3 |
|
Preset slot empty or name not found |
|
4 |
|
Name breaks the naming rules |
|
5 |
|
Flash write failed |
|
6 |
|
All user preset slots are used |
|
7 |
|
Explicit preset data is malformed |
A failed program load (program stream, program upload) passes through the FPGA service’s own code from another domain. Treat any code you do not recognize as a failure and show its message.
Command Reference
Quick Reference
| Area | Commands |
|---|---|
General |
|
Program library |
|
Sideloading |
|
Presets |
|
Settings |
|
Video |
|
Modulation |
|
Transport |
|
Remote control |
|
Interface |
|
MIDI |
|
Diagnostics |
|
USB mass storage |
|
SD card files |
|
screen dump and ui inject need Developer Mode (SYSTEM menu, or setting key 5).
General
version
Returns the firmware version string.
> version
@version:1.0.1
Development builds include the commit count and hash:
@version:1.0.2-rc.2+161.0c261fd
serial
Returns the board’s unique hardware ID as a hex string.
> serial
@serial:E6614104035B8F2F
status
Returns product, version, build type, the number of programs in the current author index, and the loaded program.
> status
@status:{"product":"Videomancer","version":"1.0.1","build":"Release","programs":59,"current_program":"Cascade"}
current_program is none when no program is loaded. LZX Connect reads product to tell Videomancer from other LZX devices.
help
Lists the available commands, pipe-delimited.
> help
@help:version|serial|status|reboot bootloader|programs list|programs authors|programs enumerate|program load|...
reboot bootloader
Reboots Videomancer into USB mass-storage (BOOTSEL) mode for a firmware update. The LCD shows Updating… Firmware. The CDC connection drops right after the reply — copy the new .uf2 file to the RPI-RP2 drive, and reconnect once Videomancer restarts. LZX Connect’s firmware update uses this command.
> reboot bootloader
@reboot:bootloader
Program Library
Programs live on the SD card in one folder per author: sd:/programs/<author>/<program>.vmprog. The lzx author also includes the programs built into the firmware. Videomancer indexes one author at a time — the author selected in the program picker. programs list, status and the front panel all show that index.
programs authors
Firmware 1.0.1 and later. Lists the author folders found on the SD card.
> programs authors
@programs:{"count":3,"authors":["lzx","boneoh","homegrownvhs"]}
programs enumerate <author> [force]
Firmware 1.0.1 and later. Rebuilds the program index for one author, the same as choosing that author in the program picker. If that author is already indexed, the scan is skipped unless you add force.
> programs enumerate boneoh
@programs:{"author":"boneoh","skipped":false,"count":12,"before":59,"overflow":0,"active_author":"boneoh"}
| Field | Description |
|---|---|
|
|
|
Programs in the index now |
|
Programs in the index before the command |
|
Programs left out because the index was full |
|
The author now indexed |
Note
The scan reads every program header in the author’s folder before it replies. A large folder takes several seconds — use a timeout of 15 seconds or more.
programs list [offset]
Lists the programs in the current author index as paginated JSON.
> programs list
@programs:{"count":59,"programs":["Afterdark","Afterimage","Alcove",...],"more":true,"next":40}
| Field | Type | Description |
|---|---|---|
|
number |
Total number of programs in the index |
|
array |
Program names in this page |
|
boolean |
Present and |
|
number |
Offset for the next page (present only when |
To retrieve every program, page through until more is absent:
> programs list
> programs list 40
To list another author’s programs, run programs enumerate <author> first.
program load <name>
Loads a program by name. Matching is case-insensitive and accepts the display name (Roll Shutter), the display name with underscores for spaces (Roll_Shutter), or the file name without .vmprog (rollshut).
> program load cascade
@program:ok
If the program is not in the current author index, Videomancer searches the other authors' folders, switches the picker to the author that has it, and loads it (firmware 1.0.1 and later). A name found nowhere returns file_not_found.
@program:ok means the load has started. It then runs the full workflow: video sync suspension, loading screen, FPGA bitstream load, factory preset application, and video output resume.
Tip
Allow 1–2 seconds after @program:ok before querying program state or presets, or poll fpga status until state is running.
program info
Returns the loaded program’s name, ID and parameter labels with their display ranges.
> program info
@program:{"name":"Cascade","id":"cascade","parameters":[{"name":"Hue","min":0,"max":100},...]}
Returns service_unavailable when no program is loaded.
program state
Returns the live modulator state for all 12 parameter channels: manual position (m), time (t), space (sp), slope (sl) and modulation source (sr).
> program state
@program:{"m":[512,512,512,512,512,512,0,0,0,0,0,512],"t":[...],"sp":[...],"sl":[...],"sr":[0,0,0,0,0,0,0,0,0,0,0,0]}
Source numbers are the operator IDs in Modulation Operators.
Sideloading
Sideloading runs a .vmprog straight from your computer without copying it into the program library. LZX Connect’s Upload Program button uses program stream.
program stream <size> [name]
Streams a .vmprog package into the FPGA without the SD card. <size> is the file size in bytes; [name] is shown on the LCD while the program runs (default Sideload).
-
Send
program stream <size> [name]. -
Wait for
@program:ready. -
Write exactly
<size>raw bytes of the.vmprogfile. -
Wait for
@program:ok, or a!error if the package is invalid or the load fails. Allow up to 45 seconds.
> program stream 312448 MyEffect
@program:ready
... 312448 raw bytes ...
@program:ok
Videomancer buffers the package header and metadata, then streams the bitstream for the current video configuration (the variant fpga preferred-variant reports) directly into the FPGA. If no bytes arrive for 5 seconds the stream is abandoned with an io_error.
A stream cannot be rewound, so Videomancer configures the FPGA once per transfer. If the FPGA does not configure, the load fails with code 738197512 (fpga_config_timeout) and the next program stream programs the FPGA at a lower SPI speed. Send the file again, up to two more times. LZX Connect does this for you.
A streamed program is not saved. It stops running if the video timing crosses between SD and HD, and Videomancer sends @program:invalidated:variant_mismatch.
program upload <size> [name]
The older SD-staged sideload, kept for scripts written against early firmware. It writes the bytes to sd:/tmp/sideload.vmprog and then loads that file through the normal program-load workflow. It needs an SD card and accepts files up to 512 KiB. The handshake is the same as program stream: @program:ready, the raw bytes, then @program:ok once the program has loaded. A 2-second gap in the data aborts the upload.
fpga preferred-variant
Returns the bitstream variant the current video routing and timing need.
> fpga preferred-variant
@fpga:variant=hd_hdmi
Variants: sd_analog, sd_hdmi, sd_dual, sd_standalone, hd_analog, hd_hdmi, hd_dual, hd_standalone (generic means none is resolved).
Program Presets
Presets come in two types:
-
Factory presets — bundled with the program, read-only.
-
User presets — saved by you, stored in flash. Up to 32 per program.
All preset commands act on the currently loaded program.
program presets list
Lists preset indices and names, plus free flash space.
> program presets list
@program:{"factory":[{"i":0,"n":"Default"},{"i":1,"n":"Wide"}],"user":[{"i":0,"n":"MyPreset"}],"flash_free":131072}
program presets get <index> <type>
Returns full preset data. Type is factory or user.
> program presets get 0 factory
@program:{"n":"Default","m":[512,512,512,512,512,512,0,0,0,0,0,512]}
Factory presets carry manual positions (m) only. User presets also carry t (time), sp (space), sl (slope) and sr (source), in the same layout as program state.
program presets apply <index> <type>
Applies a preset by numeric index and type.
> program presets apply 0 factory
@program:ok
program presets apply <name>
Applies a preset by name (case-insensitive). Factory presets are searched first, then user presets.
> program presets apply Default
@program:ok
program presets save <index> <name> [data]
Saves a user preset at slot <index>. Without data, it captures the current state.
> program presets save 0 MyPreset
@program:ok
To save explicit values, append all five arrays in this order, 12 comma-separated values each:
> program presets save 0 MyPreset m:512,512,512,512,512,512,0,0,0,0,0,512 t:0,0,0,0,0,0,0,0,0,0,0,0 sp:0,0,0,0,0,0,0,0,0,0,0,0 sl:0,0,0,0,0,0,0,0,0,0,0,0 sr:0,0,0,0,0,0,0,0,0,0,0,0
@program:ok
Rules:
-
<index>may overwrite an existing slot or append at the end (indexequal to the number of user presets); it cannot leave a gap. -
Names are 1–15 printable ASCII characters with no spaces, quotes (
") or backslashes.
program presets delete <index>
Deletes a user preset. Returns the updated preset list, in the same format as program presets list.
> program presets delete 2
@program:{"factory":[...],"user":[...],"flash_free":135168}
program presets rename <index> <name>
Renames a user preset. The same naming rules apply.
> program presets rename 0 NewName
@program:ok
Settings
Settings are addressed by numeric key. The menu names are those in the SYSTEM menu.
settings get <key>
Returns one setting with its type.
> settings get 1
@settings:{"key":1,"type":"uint32","value":4}
> settings get 7
@settings:{"key":7,"type":"string","value":"Cascade"}
settings set <key> <value>
Changes one setting. A value starting with a digit is stored as a number; anything else is stored as text. The change takes effect the same way as a front-panel edit — for example, changing a video setting reconfigures the video chain.
> settings set 3 1
@settings:ok
settings reset
Resets every setting to its factory default.
> settings reset
@settings:ok
settings export
Writes all settings to sd:/tmp/lzx-settings-export.json on the SD card and returns the file path and the number of settings. Read the file back with fs read. Requires an SD card.
> settings export
@settings:{"path":"sd:/tmp/lzx-settings-export.json","count":43}
The file holds a format version and one entry per setting: k is the key, t the value type (1 unsigned, 2 signed, 3 float, 4 boolean, 5 string), and v the value.
{"version":3,"entries":[{"k":1,"t":1,"v":4},{"k":7,"t":5,"v":"Cascade"},...]}
settings import <path>
Imports settings from a JSON file on the SD card (the format settings export writes) and saves them. Copy the file to the card first with fs put.
> settings import sd:/tmp/lzx-settings-import.json
@settings:ok
LZX Connect’s Export Settings and Import Settings use these two commands with fs read and fs put.
Setting Keys
| Key | Menu | Values | Default |
|---|---|---|---|
1 |
Video Route Mode |
|
4 |
2 |
Analog Input Mode |
|
7 |
3 |
Analog Output Mode |
|
0 |
4 |
MIDI Channel |
|
1 |
5 |
Developer Mode |
|
0 |
6 |
Language |
|
0 |
7 |
(current program) |
Program name (text). Setting it loads the program, like |
— |
8 |
Preferred Timing |
FPGA timing code — see the table below |
0 |
9 |
(boot count) |
Number of boots (read-only in practice) |
0 |
10 |
(selected slot) |
|
0 |
11 |
(transport BPM) |
BPM × 100, |
12000 |
12 |
HDMI Output Mode |
|
0 |
13 |
Soft Pickup |
|
0 |
14 |
MIDI Output Mode |
|
0 |
15 |
Enc Test Patt |
|
0 |
16 |
Enc ED/HD Lvl |
|
1 |
17 |
H Phase Out |
|
64 |
18 |
H Phase In |
|
64 |
19 |
(program author) |
Author folder shown in the program picker (text) |
|
100–159 |
(modulators) |
Saved modulator state: key = 100 + channel × 5 + parameter ( |
— |
200–211 |
MIDI Assign |
CC assignment for channels 0–11, packed: bits 0–7 CC number, bits 8–15 LSB CC ( |
CC 0–11 |
Preferred Timing codes (key 8). The code is the FPGA’s 4-bit timing value, not a list position:
| Code | Timing | Code | Timing | Code | Timing |
|---|---|---|---|---|---|
0 |
NTSC |
5 |
720p50 |
10 |
1080i60 |
1 |
1080i50 |
6 |
720p5994 |
11 |
1080p25 |
2 |
1080i5994 |
7 |
1080p30 |
12 |
576p |
3 |
1080p24 |
8 |
PAL |
13 |
1080p2997 |
4 |
480p |
9 |
1080p2398 |
14 |
720p60 |
Video
video status
Returns the video chain’s state: the active source, resolved timing, input lock, and output state. Diagnostic register snapshots from the video encoder (enc) and HDMI transmitter (hdmi_tx), and the HDMI receiver’s raw timing measurement (hdmi_rx, firmware 1.0.2 and later), follow when available.
> video status
@video:{"source":"hdmi","timing":"1080i60","locked":true,"overridden":false,"external_vsync":true,"analog":{"locked":false,"timing":"NTSC"},"hdmi":{"locked":true,"connected":true,"timing":"1080i60"},"output":{"timing":"1080i60","hdmi_connected":true,"analog_configured":true,"sfl":false},...}
video input <analog|hdmi>
Switches the active video input.
> video input analog
@video:ok
video timing <name>
Sets the video timing by name: NTSC, PAL, 480p, 576p, 720p60, 720p5994, 720p50, 1080i60, 1080i5994, 1080i50, 1080p30, 1080p2997, 1080p25, 1080p24, 1080p2398.
> video timing 720p60
@video:ok
In Standalone routing this sets Preferred Timing (setting key 8), the same as the SYSTEM menu. In the input routing modes it is a temporary bench override of the timing taken from the input; it is not saved.
video trace
Returns one page of the video event trace — timestamped events from the video chain, for diagnosing lock and reload problems — and removes it from the trace. Repeat until left is 0.
> video trace
@video:{"dropped":0,"left":14,"entries":[{"t":10234,"ev":"fpga_loaded","a":2,"b":0},...]}
Modulation
Channels are numbered 0–11. Values are 10-bit (0–1023). Source numbers 0–31 are the operator IDs in Modulation Operators.
modulation status [channel]
Without a channel: the selected channel, the MIDI channel filter, and source (s), manual (m) and output (o) for each channel.
> modulation status
@modulation:{"active":0,"midi_ch":1,"modulators":[{"s":1,"m":512,"o":640},...]}
With a channel: every parameter of that channel.
> modulation status 3
@modulation:{"index":3,"source":1,"manual":512,"time":300,"space":800,"slope":0,"output":712,"latched":false}
modulation source <channel> <source>
Sets a channel’s modulation operator.
> modulation source 3 1
@modulation:ok
modulation set <channel> <manual> [time] [space] [slope]
Sets a channel’s manual position and, optionally, its time, space and slope. Omitted values keep their current setting.
> modulation set 3 512 300 800 0
@modulation:ok
modulation reset
Resets every channel’s modulation.
modulation latch
Latches all physical controls, so each knob and switch must be moved to its stored value before it takes over again. Has no effect unless Soft Pickup is enabled (setting key 13).
modulation cc-map
Returns the MIDI CC assignment for each channel (null when unassigned).
> modulation cc-map
@modulation:{"assignments":[{"msb":0,"lsb":32,"auto":true},{"msb":1,"lsb":33,"auto":true},...]}
modulation audio status
Timing counters for the audio and per-line modulation paths. A diagnostic for LZX support.
Transport
Motion transport commands for testing tempo-locked operators (Sync LFO, Clock Div) without an external MIDI clock.
transport status
Returns transport state and timing counters as JSON.
> transport status
@transport:{"state":"stopped","bpm_x100":12000,"phase":0,"field_count":0}
state is one of stopped, playing, or paused. bpm_x100 is BPM × 100 (e.g. 12000 = 120.0 BPM).
transport bpm <value_x100>
Sets the internal BPM. Valid range: 1000–40000 (10.0–400.0 BPM).
> transport bpm 12000
@transport:ok
transport play
Starts transport playback (the same as a front-panel Play or MIDI Start).
> transport play
@transport:ok
transport stop
Stops transport playback (the same as a front-panel Stop or MIDI Stop).
> transport stop
@transport:ok
Remote Control and FPGA Registers
These commands write the loaded program’s FPGA control registers directly. Register addresses 0–9 accept 0–1023; addresses 10–11 are the H-phase registers and accept 0–127.
remote enable / remote disable
Switches the control mode to remote (register writes from the serial port are accepted) or back to local (front-panel controls and modulation only).
> remote enable
@remote:enabled
> remote disable
@remote:disabled
remote status
> remote status
@remote:{"mode":"local"}
Modes: local, remote, hybrid, passthrough.
remote write <address> <value>
Writes one register. Refused with invalid_state while the mode is local.
> remote write 0 768
@remote:ok
fpga status
Returns the FPGA service state and the loaded program.
> fpga status
@fpga:{"state":"running","configured":true,"bridge":true,"program":"Cascade","program_id":"cascade","parameters":12,"program_count":59,"browse_author":"lzx","capacity_overflow":0}
state is one of uninitialized, idle, loading, configuring, running, or error.
fpga register <address> <value>
Writes one register regardless of the control mode — a bench command. Ranges as above.
> fpga register 10 64
@fpga:ok
Interface
language [code]
Without a code, returns the interface language. With a code, changes it.
> language
@language:en_US
> language de_DE
@language:de_DE
Codes, in setting-key order 0–17: en_US, de_DE, fr_FR, es_ES, da_DK, sv_SE, fi_FI, it_IT, pt_BR, id_ID, nl_NL, nb_NO, tr_TR, ca_ES, pl_PL, hr_HR, ro_RO, cs_CZ.
screen navigate <id>
Switches the LCD to an internal screen number (0–255). A bench command.
> screen navigate 0
@screen:ok
screen dump
Developer Mode only. Returns the 32 character cells of the 2 × 16 LCD as byte values.
> screen dump
@screen:{"cells":[67,97,115,99,97,100,101,32,...]}
ui inject …
Developer Mode only. Simulates front-panel input, for automated testing.
| Command | Effect |
|---|---|
|
Presses a button |
|
Releases a button |
|
Turns the rotary encoder; |
|
Sets a toggle switch up ( |
Each answers @ui:ok.
MIDI Monitor
midi monitor on
Prints incoming MIDI messages as log lines. Clock and Active Sensing are filtered out.
> midi monitor on
@midi:monitor on
midi monitor verbose
Like on, but also prints MIDI Clock and Active Sensing.
> midi monitor verbose
@midi:monitor verbose (includes clock+active sense)
midi monitor off
Stops the monitor.
> midi monitor off
@midi:monitor off
midi monitor on its own reports the current state.
Diagnostics
ram
Returns settings usage, the program index size, and an RP2040 heap snapshot.
> ram
@ram:{"settings":{"used":43,"capacity":128,"schemas":43},"fpga":{"programs":59},"heap":{"used":128,"free":896,"total":1024}}
cpu
Returns per-core CPU usage, one @cpu: line per core. A core without data reports {"available":false}.
> cpu
@cpu:{"core0":{"usage":12,"isr":3,"loop_avg":120,"loop_peak":450,"loops_sec":8000,"isr_avg":4,"isr_peak":30,"isrs_sec":15000}}
@cpu:{"core1":{"available":false}}
log level [trace|debug|info|warn|error|critical|none]
Without an argument, returns the current log level. With one, sets how much diagnostic logging appears on the serial stream, and returns the new level.
> log level
@log:WARN
> log level info
@log:INFO
Release firmware compiles most log lines out, so raising the level shows little there. Development builds print mount and stall messages between responses.
USB Mass Storage
Videomancer can expose its SD card to your computer as a USB drive. While it does, the card is unmounted on the Videomancer side and the fs commands answer sd_not_mounted. The serial port stays available.
msd enter
Unmounts the SD card and presents it to the computer as a drive. Needs a card in the slot; otherwise the LCD shows NO SD CARD.
> msd enter
@msd:{"status":"entering"}
msd exit
Ejects the drive from the computer, remounts the card, and rescans the program library. Eject the drive on your computer first.
> msd exit
@msd:{"status":"exiting"}
msd status
> msd status
@msd:{"active":false}
Filesystem
Filesystem commands operate on the SD card. Paths must begin with sd:/, may not contain .., and are limited to 200 characters. Paths cannot contain spaces. The internal flash filesystem is not accessible.
fs info
Returns SD card presence, mount status and capacity in bytes. fs on its own does the same.
> fs info
@fs:{"mounted":true,"present":true,"total":31914983424,"used":902119424,"free":31012864000}
Without a mounted card: {"mounted":false,"present":false}.
fs caps
Returns the filesystem protocol version and limits, so a host tool can choose the fastest transfer method. LZX Connect calls this before a program library install.
> fs caps
@fs:{"version":4,"fs_put":1,"fs_hash":1,"put_max_bytes":8388608,"put_idle_timeout_ms":120000,"read_max_bytes":256,"profile_locked":true,"sd_runtime_poll":true,"sd_present_at_boot":true,"sd_mounted_at_boot":true,"sd_mount_rc":0}
| Field | Description |
|---|---|
|
|
|
|
|
Largest file |
|
How long |
|
Largest chunk |
|
Whether the card was present and mounted at boot, and the mount result |
fs ls [path] [offset]
Lists a directory. Defaults to sd:/.
> fs ls sd:/programs/lzx
@fs:[{"name":"cascade.vmprog","type":"file","size":312448},{"name":"drift.vmprog","type":"file","size":301056},...]
A long listing is split into pages. The page then comes wrapped in an object; pass next as the offset to continue:
@fs:{"entries":[...],"more":true,"next":9}
> fs ls sd:/programs/lzx 9
fs stat <path>
Returns a file’s or directory’s type and size.
> fs stat sd:/programs/lzx/cascade.vmprog
@fs:{"type":"file","size":312448}
fs mkdir <path>
Creates a directory, including any missing parent directories. An existing directory is not an error.
> fs mkdir sd:/presets
@fs:ok
fs rm <path>
Removes a file or an empty directory.
> fs rm sd:/presets/old.json
@fs:ok
fs rename <old_path> <new_path>
Renames or moves a file or directory.
> fs rename sd:/presets/a.json sd:/presets/b.json
@fs:ok
fs read <path> <offset> <length>
Reads up to 256 bytes from <offset>. The data is base64-encoded; read is the number of bytes returned, and 0 means the end of the file.
> fs read sd:/tmp/lzx-settings-export.json 0 256
@fs:{"data":"eyJ2ZXJzaW9uIjozLCJlbnRyaWVzIjpb...","read":256}
fs write <path> <offset> <base64>
Writes base64-encoded data at <offset>. A write at offset 0 truncates the file first; an empty write at offset 0 creates an empty file. Because the whole command must fit in one 511-byte line, each write carries a few hundred bytes at most — use fs put for anything larger.
> fs write sd:/test.txt 0 SGVsbG8=
@fs:{"written":5}
fs put <path> <size>
Writes a whole file as raw bytes — much faster than fs write, with no base64. Missing parent directories are created, and an existing file is replaced. Files can be up to 8 MiB.
-
Send
fs put <path> <size>. -
Wait for
@fs:{"put":"ready","size":<size>}. -
Write exactly
<size>raw bytes. -
Wait for
@fs:{"put":"ok","written":<size>}.
> fs put sd:/programs/lzx/cascade.vmprog 312448
@fs:{"put":"ready","size":312448}
... 312448 raw bytes ...
@fs:{"put":"ok","written":312448}
A zero-byte file skips the handshake and answers {"put":"ok","written":0} at once.
If a write fails partway (firmware 1.0.1 and later), Videomancer deletes the partial file, discards the rest of the announced bytes so none are read as commands, and then sends a single ! io_error line. Wait for that line before retrying. If the data stops arriving for 120 seconds, the transfer is abandoned the same way.
Tip
Write the data in chunks of about 1 KiB, and allow at least 5 seconds for each write call to complete — an SD card can pause for hundreds of milliseconds while it commits data. For the final reply, allow about one second per 32 KiB plus 5 seconds. Firmware 1.0.2 reads incoming data at full USB speed, so large transfers finish much faster than on 1.0.1.
fs hash <path>
Returns the BLAKE2b-256 hash of a file as 64 hex digits, with its size. A missing file is not an error.
> fs hash sd:/programs/lzx/cascade.vmprog
@fs:{"hash":"9f2c1e...","size":312448}
> fs hash sd:/missing.vmprog
@fs:{"hash":null,"size":0}
Hashing a large file takes a few seconds; allow a 15-second timeout.
fs format sd: --apply
Firmware 1.0.1 and later. Erases the SD card and creates a new, empty filesystem. Without --apply the command only prints its usage. The card must be mounted.
> fs format sd: --apply
@fs:{"format":"ok"}
How LZX Connect Uses the Serial Port
LZX Connect 1.3.12 talks to Videomancer only through the commands above. Knowing the sequences helps when you script the same jobs, or when you read an LZX Connect log.
| LZX Connect feature | Commands |
|---|---|
Device detection |
|
Firmware update |
|
Install Program Library |
|
Export / Import Settings |
Export: |
Upload Program |
|
USB storage |
|
After a library install, power-cycle Videomancer or choose the author in the program picker so the new programs are indexed.
Python Example
The following script uses pyserial. It lists the authors and their programs, loads a program, applies a preset, copies a file to the SD card with fs put, and downloads a settings backup.
import base64
import json
import os
import time
import serial
class VideomancerError(RuntimeError):
def __init__(self, code, message):
super().__init__(f"error {code}: {message}")
self.code = code
self.domain = code >> 26
self.item = code & 0x3FFF
_rx = bytearray()
def open_videomancer(port="/dev/ttyACM0"):
"""Open the port and discard any boot or log output."""
ser = serial.Serial(port, timeout=0.1, write_timeout=5)
time.sleep(0.2)
ser.reset_input_buffer()
_rx.clear()
return ser
def read_line(ser, deadline):
"""Return the next complete line, or None at the deadline."""
while True:
end = _rx.find(b"\n")
if end >= 0:
line = bytes(_rx[:end])
del _rx[:end + 1]
return line.decode("utf-8", "replace").strip()
if time.monotonic() > deadline:
return None
_rx.extend(ser.read(ser.in_waiting or 1))
def read_response(ser, topic, timeout=5.0):
"""Read lines until '@<topic>:' or an error; skip log lines."""
deadline = time.monotonic() + timeout
while True:
line = read_line(ser, deadline)
if line is None:
break
if line.startswith("@"):
key, _, payload = line[1:].partition(":")
if key == topic:
return payload
elif line.startswith("!"):
code, _, message = line[1:].partition(":")
raise VideomancerError(int(code), message)
raise TimeoutError(f"no @{topic} response")
def command(ser, line, topic=None, timeout=5.0):
"""Send one command and return its payload string."""
ser.write(f"{line}\n".encode("ascii"))
return read_response(ser, topic or line.split()[0], timeout)
def command_json(ser, line, topic=None, timeout=5.0):
return json.loads(command(ser, line, topic, timeout))
def list_programs(ser):
"""Return every program in the current author index."""
names, offset = [], 0
while True:
page = command_json(ser, f"programs list {offset}", "programs")
names += page["programs"]
if not page.get("more"):
return names
offset = page["next"]
def fs_put(ser, path, data):
"""Copy bytes to the SD card with the raw-byte fast path."""
ready = command_json(ser, f"fs put {path} {len(data)}", "fs")
if ready.get("put") == "ok": # zero-byte file
return
for i in range(0, len(data), 1024):
ser.write(data[i:i + 1024])
ser.flush()
done = json.loads(read_response(ser, "fs", timeout=len(data) / 32768 + 10))
assert done["put"] == "ok"
def fs_read_all(ser, path):
"""Read a whole file from the SD card, 256 bytes at a time."""
out, offset = b"", 0
while True:
chunk = command_json(ser, f"fs read {path} {offset} 256", "fs")
if chunk["read"] == 0:
return out
out += base64.b64decode(chunk["data"])
offset += chunk["read"]
ser = open_videomancer()
print("Firmware:", command(ser, "version"))
authors = command_json(ser, "programs authors", "programs")["authors"]
for author in authors:
command_json(ser, f"programs enumerate {author}", "programs", timeout=30)
print(author, list_programs(ser))
print(command(ser, "program load cascade"))
time.sleep(2)
print(command(ser, "program presets apply Default", "program"))
fs_put(ser, "sd:/scratch/hello.txt", b"Hello from Python\n")
meta = command_json(ser, "settings export", "settings")
backup = fs_read_all(ser, meta["path"])
print("Settings backup:", backup.decode())
ser.close()
Notes
Version History
| Firmware | Changes to the serial interface |
|---|---|
1.0.0 |
Baseline for this guide. |
1.0.1 |
Adds |
1.0.2 (beta) |
|
LZX Connect 1.3.12 retries a failed fs put during a library install. Use firmware 1.0.1 or later with it.
Limits
| Limit | Value |
|---|---|
Command line |
511 bytes |
Response payload |
1536 bytes; long lists are paginated ( |
|
256 bytes |
|
8 MiB |
|
120 seconds |
|
5 seconds |
|
512 KiB |
SD paths |
|
User presets |
32 per program; names 1–15 characters |
General
-
One connection at a time: the shell is single-threaded and serves one terminal. Quit LZX Connect before opening the port yourself.
-
One command at a time: wait for each response before sending the next command. While a binary transfer is running, other commands are refused with
invalid_state. -
SD card paths: every filesystem command needs the
sd:/prefix. The internal flash filesystem is not accessible from the serial port. -
Community tools: pyvmancer is a community-maintained Python driver for this interface.