Serial Command-Line Tool

[中文]

brookesia-usb controls the ESP-Brookesia USB service over a single serial transport: the CDC-ACM channel provided by USB Serial/JTAG, or a UART (typically the board's USB-to-UART bridge). The protocol version is 1.

Install

The USB CLI requires Python 3.9 or newer and can be installed online from PyPI:

python -m pip install brookesia-usb

The install command also installs the pyserial dependency. After installation, check the version and view the command help:

brookesia-usb --version
brookesia-usb --help

brookesia deploy in Toolkit wraps this CLI to install a built .bpk.

Devices and Ports

The CLI selects a port automatically and verifies the device with the hello command. It prefers USB Serial/JTAG and falls back to a USB-to-UART bridge when no Serial/JTAG port is present:

brookesia-usb devices
brookesia-usb status
brookesia-usb --port /dev/ttyACM0 status
brookesia-usb --port /dev/ttyUSB0 --baudrate 921600 status

Discovery order:

  1. Enumerate serial ports.

  2. If exactly one USB Serial/JTAG candidate (Espressif VID/PID or a matching port description) is present, it is used directly and is not probed during discovery; the protocol hello runs once when the command executes.

  3. If several USB Serial/JTAG candidates are present, each is probed with hello until one reports the serial_jtag transport.

  4. If no USB Serial/JTAG candidate is present, or all of them fail the probe, USB-to-UART candidates (/dev/ttyUSB* and non-Serial/JTAG ACM ports) are probed, accepting either the serial_jtag or uart transport.

If a single USB Serial/JTAG port is present but is not the target device (for example when both the USJ cable and the USB-to-UART bridge are connected), the CLI selects it and reports an error; pass --port to select the port the device actually uses.

The common connection options can also be specified explicitly:

brookesia-usb --port /dev/ttyACM0 --baudrate 115200 --timeout 10 status
brookesia-usb --port /dev/ttyUSB0 --baudrate 921600 --timeout 10 status
  • --port: Serial/JTAG device path (/dev/ttyACM*) or USB-to-UART device path (/dev/ttyUSB*); if omitted, discovery runs automatically.

  • --baudrate: ignored for USB Serial/JTAG, which has no physical baud rate. For a UART transport it must match the device console baud rate (for example CONFIG_ESP_CONSOLE_UART_BAUDRATE); the default is 115200.

  • --timeout: per-read and write timeout in seconds, defaulting to 10.

When multiple matching devices are present, pass --port explicitly using a path reported by devices. A single Serial/JTAG CDC configuration normally exposes only /dev/ttyACM0; the absence of /dev/ttyACM1 is expected.

Boards that only route a USB-to-UART bridge expose the control transport on the same port as the console. The device time-multiplexes the port: console logs are emitted while idle, and are suppressed for the duration of a control session. This requires the device firmware to select the UART transport; USB Serial/JTAG remains the default on boards that expose it.

Control Sessions and Security

Commands that require a control session send hello and validate protocol version 1, the transport reported by the device (serial_jtag or uart), and the exclusive session state before sending goodbye on exit.

Device logs are suppressed during the control session so they cannot corrupt JSON responses or file frames. USB Serial/JTAG shares its CDC channel with flashing, JTAG debugging, and console logs; a UART transport shares its port with the console output. Close idf.py monitor, minicom, or any other program reading the same serial device before running a command.

On a UART transport, a device without USB Serial/JTAG uses the same port for firmware download, console logs, and control sessions, but not at the same time: flashing runs in ROM download mode before the application starts, and log output is suppressed while a control session is active.

The USB connection is treated as a trusted physical control boundary, while protocol, path, size, checksum, and service-argument validation still apply.

Get Status

Query the USB service, transfer, and control-session status:

brookesia-usb status
brookesia-usb --port /dev/ttyACM0 status

The command prints an error and returns a non-zero status when the device is unavailable or the USB service is not running.

Call a Service Function

The call command invokes a registered Brookesia service function. Arguments must be a JSON object and must match the service schema:

brookesia-usb call Manager GetServiceNames '{}'
brookesia-usb call Manager GetServiceSchema '{"Name":"Storage"}'
brookesia-usb call SystemCore GetSystemInfo '{}'
brookesia-usb call Storage FSStat '{"Path":"/littlefs"}'
brookesia-usb call Storage FSList '{"Path":"/littlefs"}'

Use Manager.GetServiceNames and Manager.GetServiceSchema to discover available services and functions. ServiceManager validates required parameters, default values, unknown parameters, and parameter types on the device.

Functions that require a RawBuffer argument cannot be called through the JSON interface because a host pointer is not valid in device memory. Use put and install for file and package data. Calls to the Usb service itself are rejected to prevent recursion into the active USB session.

Upload a File

Upload a local file under the configured device upload root. The default upload root is /littlefs/usb and the remote path must be relative:

brookesia-usb put ./logs/session.bin logs/session.bin
brookesia-usb put ./config/device.json config/device.json --overwrite

Existing files are not overwritten by default; explicitly pass --overwrite to replace one. Absolute paths, .. path components, symbolic-link escapes, and paths outside the upload root are rejected.

The CLI calculates the file size and SHA-256 digest, sends CRC-protected binary frames, and waits for an acknowledgement after every data frame. The default frame payload is no larger than 16 KiB and the default transfer limit is 8 MiB; the device Kconfig controls the actual limits.

The device writes data to a temporary file first and commits it only after size and SHA-256 verification succeed. The temporary file is cleaned up when a transfer is interrupted.

Install an Application Package

Send a complete BPK runtime app package to the device for validation and installation:

brookesia-usb install ./build/my_app.bpk

The package is first written to the USB temporary directory. System Core then performs manifest validation, ZIP path-safety checks, staging, replacement, and rollback. The host cannot select the device-side app installation directory directly.

The CLI does not retry an installation after a disconnect or an ambiguous result. Run brookesia-usb status before retrying the command.

Abort a Transfer

Abort an active transfer by request ID:

brookesia-usb abort 42

abort is an emergency command and does not start a new control session. When request 42 is active, the device removes the temporary file and returns aborted; an unknown request ID returns a device error and a non-zero status.

Errors and Troubleshooting

The CLI returns 0 after a successful operation and 1 for transport, protocol, validation, or device errors. Common error codes include:

  • invalid_command: malformed JSON or unsupported operation.

  • busy: another control session or transfer is active.

  • bad_frame: invalid frame CRC, type, or sequence.

  • size_mismatch / hash_mismatch: declared metadata does not match the data.

  • path_denied: unsafe path or overwrite was not explicitly enabled.

  • storage_full: temporary storage cannot be created or written.

  • install_failed: System Core rejected or failed to install the package.

  • timeout / aborted: the operation timed out or was cancelled.

If the device is not found, check the USB data cable and list system devices:

ls /dev/ttyACM* /dev/ttyUSB*
brookesia-usb devices

After confirming that no other program owns the serial port, pass the device path explicitly with --port.

Additional UART transport checks:

  • hello times out or returns invalid data: the --baudrate value does not match the device console baud rate. Align them and retry.

  • The port is busy: close idf.py monitor, minicom, or any other reader of the same port. The control session and the console cannot read the UART at the same time.

  • Firmware download and BPK installation are naturally time-separated: flashing uses the ROM download mode while the application is not running, so no conflict occurs.