Skip to content

Reference Manual

Videomancer

← Back to Videomancer overview

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

  1. Connect Videomancer to your computer via the front-panel USB-C Device port.

  2. Videomancer enumerates as a USB CDC ACM virtual COM port.

  3. 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’s pyserial

    • 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_overflow error.

  • A carriage return (\r) is treated as a newline, so \r\n line 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, not VERSION). 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

@<topic>:<payload>\n

!

Error

!<code>:<message>\n

(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:

  • cpu sends one @cpu: line per core.

  • reboot bootloader sends @reboot:bootloader and then the port disappears.

  • The binary transfers (fs put, program stream, program upload) send a ready line, 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 level and the build.

  • MIDI monitor lines (no prefix) — while midi monitor is on.

  • @program:invalidated:variant_mismatch — a program you streamed with program stream stopped 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

1476395009

1

unknown_command

Unrecognized command (the message echoes the line)

1476395010

2

parse_error

Malformed or out-of-range arguments (the message shows usage)

1476395011

3

service_unavailable

Required service not ready, or no program loaded

1476395012

4

buffer_overflow

Command line longer than 511 bytes

1476395013

5

not_connected

CDC port not connected (internal; never reaches the host)

1476395014

6

file_not_found

File, directory, program or setting not found

1476395015

7

io_error

Filesystem or settings write failure, transfer timeout

1476395016

8

sd_not_mounted

No SD card, or the card is exposed over USB storage

1476395017

9

path_too_long

Path longer than 200 characters

1476395018

10

invalid_path

Path does not start with sd:/, or contains ..

1476395019

11

invalid_state

Busy with a transfer, Developer Mode off, or wrong control mode

Preset errors (domain 23):

Code Item Name Description

1543503873

1

no_program

No program loaded

1543503874

2

invalid_index

Preset index out of range

1543503875

3

not_found

Preset slot empty or name not found

1543503876

4

name_invalid

Name breaks the naming rules

1543503877

5

save_failed

Flash write failed

1543503878

6

presets_full

All user preset slots are used

1543503879

7

parse_failed

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

version, serial, status, help, reboot bootloader

Program library

programs authors, programs enumerate, programs list, program load, program info, program state

Sideloading

program stream, program upload, fpga preferred-variant

Presets

program presets list / get / apply / save / delete / rename

Settings

settings export, settings import, settings get, settings set, settings reset

Video

video status, video input, video timing, video trace

Modulation

modulation status / source / set / reset / latch / cc-map / audio status

Transport

transport status, transport bpm, transport play, transport stop

Remote control

remote enable / disable / status / write, fpga status, fpga register

Interface

language, screen navigate, screen dump, ui inject

MIDI

midi monitor on / off / verbose

Diagnostics

ram, cpu, log level

USB mass storage

msd enter, msd exit, msd status

SD card files

fs info, fs caps, fs ls, fs stat, fs mkdir, fs rm, fs rename, fs read, fs write, fs put, fs hash, fs format

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

skipped

true if the author was already indexed and nothing was scanned

count

Programs in the index now

before

Programs in the index before the command

overflow

Programs left out because the index was full

active_author

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

count

number

Total number of programs in the index

programs

array

Program names in this page

more

boolean

Present and true when more pages remain

next

number

Offset for the next page (present only when more is true)

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).

  1. Send program stream <size> [name].

  2. Wait for @program:ready.

  3. Write exactly <size> raw bytes of the .vmprog file.

  4. 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 (index equal 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

0 Analog In, 1 HDMI In, 2 Dual In, 3 Standalone, 4 Auto

4

2

Analog Input Mode

0 Composite, 1 S-Video, 2 YPbPr, 3 RGB SOG, 4 RGB 1V, 5 YPbPr 1V, 6 YYY 1V, 7 Auto

7

3

Analog Output Mode

0 CVBS/S-Video, 1 YPbPr, 2 RGsB, 3 YYY, 4 SOG

0

4

MIDI Channel

0 Disabled, 1 Omni, 2–17 channel 1–16

1

5

Developer Mode

0 Disabled, 1 Enabled

0

6

Language

0–17, in the order listed under language below

0

7

(current program)

Program name (text). Setting it loads the program, like program load

—

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–127

0

11

(transport BPM)

BPM × 100, 1000–40000

12000

12

HDMI Output Mode

0 HDMI, 1 DVI

0

13

Soft Pickup

0 Disabled, 1 Enabled

0

14

MIDI Output Mode

0 Disabled, 1 Manual+Mod, 2 Manual Only, 3 Mod Only

0

15

Enc Test Patt

0 Off, 1 On

0

16

Enc ED/HD Lvl

0 Studio, 1 Full

1

17

H Phase Out

0–924, center 64

64

18

H Phase In

0–127

64

19

(program author)

Author folder shown in the program picker (text)

lzx

100–159

(modulators)

Saved modulator state: key = 100 + channel × 5 + parameter (0 manual, 1 time, 2 space, 3 slope, 4 source)

—

200–211

MIDI Assign

CC assignment for channels 0–11, packed: bits 0–7 CC number, bits 8–15 LSB CC (255 = none), bit 16 auto-pair

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

ui inject button press <0-18>

Presses a button

ui inject button release <0-18>

Releases a button

ui inject encoder rotate <steps>

Turns the rotary encoder; <steps> is −127 to 127; the sign sets the direction

ui inject toggle <0-4> up

Sets a toggle switch up (down sets it down)

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

version

3 = fs put and fs hash; 4 adds the SD card boot fields

fs_put, fs_hash

1 when the command is available

put_max_bytes

Largest file fs put accepts (8 MiB)

put_idle_timeout_ms

How long fs put waits for more data before giving up

read_max_bytes

Largest chunk fs read returns

sd_*

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.

  1. Send fs put <path> <size>.

  2. Wait for @fs:{"put":"ready","size":<size>}.

  3. Write exactly <size> raw bytes.

  4. 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

version, serial, status (the product field)

Firmware update

reboot bootloader, then a copy of the .uf2 file to the RPI-RP2 drive

Install Program Library

fs info, fs caps, fs mkdir for each folder, then one fs put per file into sd:/programs/<author>/; a failed file is retried up to 3 times. fs ls, fs stat, fs read and fs rm check what is already installed

Export / Import Settings

Export: settings export, then fs read. Import: fs put to sd:/tmp/lzx-settings-import.json, then settings import

Upload Program

program stream

USB storage

msd enter, msd status, msd exit

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. settings export / settings import work through a file on the SD card.

1.0.1

Adds programs authors, programs enumerate and fs format. program load searches every author. A failed fs put discards the rest of the data and reports one error; the idle timeout grows from 2 s to 120 s.

1.0.2 (beta)

fs put reads incoming data at full USB speed, and the program scan pauses during host transfers, fixing library install timeouts. The program picker scans authors without blocking the serial port.

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 (programs list, fs ls)

fs read chunk

256 bytes

fs put file size

8 MiB

fs put idle timeout

120 seconds

program stream idle timeout

5 seconds

program upload file size

512 KiB

SD paths

sd:/ prefix, no .., no spaces, 200 characters

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.

This document was created with the assistance of AI tools. Contact us if you have any concerns.