# 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](test-results/companion_usb_client_validation.md) 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](./terminal_chat_cli.md). ## Wire formats Host-to-device Binary Companion frames use this layout: ```text '<' length-low length-high payload[length] ``` Device-to-host frames use the same little-endian length with a different marker: ```text '>' length-low length-high payload[length] ``` For example, a representative two-byte device query is: ```text 3C 02 00 16 03 ``` The ASCII terminal is line-oriented and accepts commands such as: ```text 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 ```text 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: ```text +++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: ```text +++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: ```bash meshcli -s /dev/ttyACM0 -b 115200 ver ``` An explicit terminal start token is still useful when the device is already in binary mode: ```bash 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](usb_logging_watchdog.md) 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`](https://github.com/mikecarper/MeshCore/blob/keymindCascade/src/helpers/UsbAsciiBinarySwitch.h), with stream ownership in [`main.cpp`](https://github.com/mikecarper/MeshCore/blob/keymindCascade/examples/companion_radio/main.cpp) and framing in [`ArduinoSerialInterface.cpp`](https://github.com/mikecarper/MeshCore/blob/keymindCascade/src/helpers/ArduinoSerialInterface.cpp).