Skip to content

Companion USB CLI and binary switcher

USB Companion uses one primary USB serial interface for two incompatible wire formats:

  • a human-readable ASCII command line;
  • the framed Binary Companion protocol used by MeshCore apps and meshcli.

The primary interface starts in the ASCII terminal after each boot and returns to ASCII after a USB session reset that the hardware can observe. A Binary Companion client does not need to send a special mode command: its first valid frame automatically hands the interface to the binary parser.

Current source keeps the default terminal quiet until the host sends ASCII input. Press Enter or type a command to see the banner. This prevents the ASCII > prompt from being mistaken for the beginning of an app's first binary response. See USB client compatibility validation for the tested firmware and release status.

These mode controls apply to Full and ordinary USB Companion builds. The explicit +++MESHCORE-TERM-START and +++MESHCORE-TERM-STOP controls remain available, including for older firmware. See the Terminal Chat CLI guide.

Wire formats

Host-to-device Binary Companion frames use this layout:

'<'  length-low  length-high  payload[length]

Device-to-host frames use the same little-endian length with a different marker:

'>'  length-low  length-high  payload[length]

For example, a representative two-byte device query is:

3C 02 00 16 03

The ASCII terminal is line-oriented and accepts commands such as:

get radio.cad
set display.rotation 90
reboot

Changing the configured baud rate does not select a mode. Use 115200 for compatibility even though native USB CDC hardware does not use UART timing.

State transitions

                         complete '<' frame
                 +--------------------------------+
                 |                                v
boot ------> ASCII terminal                  Binary Companion
                 ^                                |
                 |                                | +++MESHCORE-TERM-START
                 | incomplete '<' probe           |
                 | (one-second timeout)            |
                 +--------------------------------+

ASCII terminal -- +++MESHCORE-TERM-STOP ------> Binary Companion
any mode ------- observable USB session reset ---> ASCII terminal
ESP32 logging terminal -- usb.logging off -----> ASCII terminal
any mode ------- reboot ------------------------> ASCII terminal

Serial mOTA and single-TTY logging add exclusive ownership states described below. BLE, WiFi, Ethernet, and hardware-serial Companion transports are not switched; they remain binary.

How automatic detection works

  1. Companion initializes the normal USB Binary Companion interface, then gives its primary stream to the ASCII terminal before normal loop service begins.
  2. While the prompt has no buffered input, the terminal skips blank CR/LF delimiters and peeks at the next byte without removing it. A queued binary frame after a blank line is detected before any ASCII banner is emitted.
  3. If the byte is <, the terminal temporarily releases the stream and enables the existing ArduinoSerialInterface frame parser.
  4. The parser consumes the original <, the two-byte length, and the payload. There is no second parser and no copied or synthetic frame.
  5. A monotonically increasing completed-frame counter confirms that the parser received a complete frame. The interface then remains in Binary Companion mode.
  6. If no complete frame arrives within one second, the parser state is reset and the ASCII terminal prints a new banner and prompt.

The switcher checks framing, not client identity. A complete Binary Companion frame with 1-176 payload bytes confirms binary mode; it does not require the first command to be CMD_APP_START or CMD_DEVICE_QUERY. Normal command validation still occurs after the frame parser returns the payload.

Oversized frames are consumed and discarded, never truncated into executable commands or counted as proof of a Binary client. ASCII, framed Binary, and mOTA input handlers process at most the initial queued-byte count, so a continuously sending host cannot keep the radio loop trapped in one input call.

ASCII and mOTA reject an entire line containing an embedded NUL or exceeding its line buffer, discarding through the next CR/LF. A valid-looking suffix is not executed. Binary payloads may still contain NUL bytes. The immediate manual terminal-stop token remains an exception to ordinary line-oriented handling.

The empty-prompt requirement prevents a literal < in the middle of a command from silently changing modes. A literal < typed as the first character does start a probe, but the terminal returns after the one-second timeout if no binary header and payload follow.

Manual controls

The original controls remain available.

From Binary Companion, send this exact unframed line while the parser is idle:

+++MESHCORE-TERM-START

Terminate it with CR or LF. The binary parser accepts control tokens only as complete delimiter-bounded lines; prefixes, suffixes, and partial tokens are ignored.

From the ASCII terminal, send this exact sequence to return to binary mode:

+++MESHCORE-TERM-STOP

The stop sequence takes effect as soon as its last byte arrives in ASCII mode. The start sequence is recognized only as a completed line while the binary parser is idle and is not examined inside a length-prefixed frame.

meshcli can normally connect directly after boot:

meshcli -s /dev/ttyACM0 -b 115200 ver

An explicit terminal start token is still useful when the device is already in binary mode:

picocom -b 115200 \
  --imap spchex \
  --initstring $'+++MESHCORE-TERM-START\r' \
  /dev/ttyACM0

Logging and mOTA ownership

The switcher never attempts to mix ASCII, framed Companion traffic, or binary mOTA traffic on the same stream.

Situation Primary USB behavior
USB Companion after boot or observable session reset ASCII; a complete < frame switches to Binary Companion
nRF52 logging enabled Primary interface still follows the switcher; logs use the optional second interface
ESP32 logging enabled at boot Logging terminal owns the only USB TTY; automatic < detection is disabled
nRF52 USB serial mOTA active mOTA owns primary USB; ASCII and Binary Companion are unavailable there
BLE/WiFi/Ethernet/hardware serial Always Binary Companion and unaffected by the USB mode

On RAK4631 and other nRF52 Full Companions, ASCII and Binary Companion both use the primary interface (*-if00 on Linux). The optional *-if02 interface is for logging. OK - Binary mode acknowledges a mode change on the same primary port; it does not move the connection to the logging port.

On every ESP32 Full Companion, use set usb.logging off in its logging terminal before trying to use USB with an app. That command stops logging and returns to the ordinary ASCII terminal; it does not select Binary Companion. The app's first valid framed probe can perform the normal automatic switch, or a human can send +++MESHCORE-TERM-STOP first. The framed/binary Companion parser is disabled while logging owns that TTY. BLE and WiFi Companion transports remain available while USB is logging. ESP32 Full builds use the Arduino-ESP32 2.x base where supported; RC32 and ESP32-C6 keep their board-required Arduino 3.x platform but follow the same one-TTY policy. No ESP32 Full build enumerates a second CDC interface.

usb.logging is the saved master for USB packet output. usb.debug separately saves verbose diagnostic intent and defaults off, including when older preferences do not contain the new field. RAW/RX/TX packet records need only the master; diagnostics need both switches and compiled debug support. Turning the master off preserves debug intent. get usb.debug reports that saved choice, not whether USB currently owns a plaintext logging stream.

Full Companion now emits complete received frames as uppercase ASCII RAW: hex, separately from decoded RX summaries and TX summaries. Leave debug off for USB-to-MQTT capture. These are bounded best-effort records; drops and host disconnects can separate a RAW/summary pair. The dedicated nRF52 log CDC cannot answer CLI/identity queries: programs expecting one bidirectional CLI/log port need separate control-port support, not just a change to the serial pathname. Companion supports get public.key in the primary terminal or framed CLI; the reply is > followed by 64 uppercase hex characters. This query does not make the dedicated log CDC input-capable or establish full bridge compatibility. Send get/set usb.debug to the primary ASCII terminal; changing verbosity needs no USB-interface reboot and does not relax Binary/mOTA ownership rules.

Current source adds get usb.watchdog and saved set usb.watchdog off|on|auto, defaulting to Auto until 14 continuous healthy logging-client days qualify it for On. Existing USB stats polls renew a 15-minute client lease; a cable or DTR alone does not count. A lost confirmed client can trigger USB-only recovery after lease expiry plus five minutes. MCU resets additionally require On and a full physical-fault interval, a verified tier save, and clear protocol/update ownership. Active Binary USB and mOTA sessions veto recovery. On dual CDC, only logging CDC1 accepts the bounded stats markers; primary CDC0 stats do not renew that lease. CDC1 still provides no CLI/statistics replies. See USB logging watchdog for status and limitations.

With logging enabled on a shared port, the stop token is rejected with ERROR: set usb.logging off before Binary mode. Turn logging off first; a successful binary-mode acknowledgement must not immediately revert to logging.

On nRF52, serial motatool is also text-first. Its exact initial ota folder on line is recognized in either startup ASCII or Binary Companion mode. From ASCII, the firmware leaves terminal mode and directly enters exclusive mOTA ownership; from binary, the idle frame parser recognizes the same control sequence. The following mOTA request/reply frames therefore cannot be consumed by the ASCII line editor. motatool serve --serial can be the first client after boot and does not require a terminal token or disconnect workaround.

Only the exact completed line selects mOTA from ASCII. Extra arguments, leading/trailing whitespace, or a partial line remain ordinary terminal input.

Shortcomings and edge cases

This mechanism is deliberately small and deterministic, but it is not a full protocol negotiation layer.

Session detection depends on the USB hardware

After the first complete binary frame, the device stays in Binary Companion mode for that session. Native USB with DTR/session events restores ASCII when the host closes or resets the connection. Old input, queued output and mOTA ownership are cleared before selecting the default for the next session.

Some clients, including meshcore 2.3.10 used by stock MeshCLI, deliberately deassert DTR. The nRF52 primary interface also accepts actual incoming USB data as evidence of a client in the current session. Closing, changing line coding, or resetting USB invalidates that evidence. The dedicated logging interface continues to require its own connected reader.

ESP32 hardware USB Serial/JTAG reports bus reset and physical host loss, but cannot reliably report a terminal program closing its handle. A USB-to-UART bridge also usually cannot report that close. On those ports, use the terminal start token to return from Binary mode without rebooting. Idle time alone never changes the selected protocol.

An active network terminal temporarily delays restoring the USB ASCII prompt. If a fresh USB app sends a complete binary frame during that delay, its Binary mode remains selected when the network terminal closes.

Detection works only at an empty prompt

If part of an ASCII command is already buffered, an incoming < is treated as ordinary terminal input. Clear or submit the line before starting a Binary Companion client. Only one process should have the serial port open.

The first frame has a one-second deadline

The complete marker, length, and payload must arrive within the probe window. This is generous for local USB but may reject a heavily buffered serial proxy, a debugger that pauses the MCU, or a tool that writes the header and body with a long delay. A timed-out client can retry after the ASCII banner appears.

Framing confirmation is not authentication

Any complete length-prefixed frame selects binary mode, even if its command is unknown or malformed at the application layer. This is safe for stream separation but means a random complete frame can leave the device in binary mode until an observable USB session reset or a manual switch back.

The default terminal waits for input

The firmware enters ASCII mode during boot but suppresses the banner and unsolicited terminal events until ASCII input identifies a terminal client. Send Enter or a harmless get command to display the banner. A binary client instead gets a clean first response. Older builds that immediately printed > could consume the first binary response's marker as part of a bogus frame length; scanning past leading text alone did not reliably recover that reply.

Text and binary output cannot be interleaved

Primary USB suppresses Binary Companion output while the terminal owns the stream. Packet/debug logging must use its dedicated CDC interface or the exclusive single-TTY logging mode. Writing diagnostic text directly to the primary binary stream will corrupt clients regardless of the switcher.

A literal leading < briefly hides the prompt

Typing < as the first terminal character begins a binary probe. With no complete frame, the prompt returns after one second and the banner is printed again. There is currently no escape syntax for entering a literal leading <; prefix it with another character if it is needed as command text.

Troubleshooting

If meshcli cannot connect:

  1. close every terminal or logging reader using the primary interface;
  2. on ESP32, confirm that saved USB logging is off;
  3. reboot and let meshcli be the first process to open the data interface;
  4. use the stable /dev/serial/by-id/*-if00 path on Linux when available;
  5. if a prompt appears after one second, the client's first frame was not completed inside the probe window.

If an ASCII terminal shows no banner, press Enter and issue a harmless query such as get radio.cad. If binary bytes appear, send the exact terminal start token or reboot.

The switch policy is implemented in UsbAsciiBinarySwitch.h, with stream ownership in main.cpp and framing in ArduinoSerialInterface.cpp.