Frame format, logical channels and RPC protocol

[中文]

Three version identifiers

firmware.version identifies the Git/release build; evMuxVersion defines the required session protocol; EVMUX/1 identifies wire framing. Determine compatibility from evMuxVersion/capabilities, then display, record or verify firmware.version. Do not infer protocol from date strings.

Byte layout of a frame

0x1e + ASCII "EVMUX/1 h=<H> p=<P> c=<CRC32>\r\n"
       + H bytes of UTF-8 metadata JSON
       + P bytes of payload
       + 0x1f

Figure 12: H/P define body boundaries; EOF is not a payload delimiter. JPEG, binary file data and text can contain 0x1e, 0x1f and CRLF. Never truncate the body on those bytes.

H/P count UTF-8 or binary bytes. A character such as U+4F60 typically occupies three UTF-8 bytes; JavaScript string length is not payload_bytes.

CRC32 covers only payload, not metadata/header. Use standard CRC32, e.g. host Python zlib.crc32(payload) & 0xffffffff, encoded as eight hexadecimal digits. Device frames include payload CRC; incoming c=00000000 skips verification. CRC detects corruption, not identity or final operation completion.

For JPEG responses and previews, a valid EV-MUX frame alone does not establish a valid image. Firmware validates encoder output before queueing it; Core additionally checks the JPEG structure and dimensions before delivery. Invalid capture responses return INVALID_IMAGE locally and invalid previews are dropped; see Preview, debug capture, backpressure and statistics for the supported image checks and their limits.

Hosts must handle arbitrary fragmentation and coalescing

read #1: [SOF][header first half]
read #2: [header tail][metadata first half]
read #3: [metadata tail][payload first half]
read #4: [payload tail][EOF][whole next frame][next SOF]

Figure 13: A streaming parser retains state across reads and delivers only fully validated frames. Record format/length/CRC/deadline errors and resynchronize in a controlled manner; never pass partial frames into REPL/file writes.

Current device RX limits are 384 metadata bytes and 2048 payload bytes. RX parser deadlines are approximately 3000 ms idle and 10000 ms per complete frame. These limit host commands received by the device, not preview image size or host receive timeouts for large images.

Metadata fields

Common fields

Field

Type

Purpose

channel

String

Select channel, e.g. user.rpc or repl.stdout.

type

String

RPC request req, response rsp, notification event or data data.

seq

uint32

Request/response correlation number; identical numbers across channels/types do not complete each other.

method

String

RPC method or stdio source descriptor; c-stdio is not an RPC method.

encoding

Optional string

Semantic hint such as json, text or binary.

sessionId

32-character lowercase hexadecimal string

Current owner authorization/output scope; discovery requests omit tokens.

vmEpoch

uint32

VM operation/output generation scope.

sid

Optional string

Logical source/domain such as stdio; not sessionId.

ts_ms

Optional integer

Device timestamp, not automatically synchronized with host time.

contentType、width、height

Channel-defined

Describe image payload; JPEG bytes remain in payload, not JSON.

Metadata and RPC request payload must be complete top-level JSON objects with correctly typed known fields and no duplicates. Unknown extensions may be ignored. Escaped strings require a real JSON parser; trailing garbage is not a valid complete object.

Channels and streams

Wire channels

channel

Direction

stream

Payload / purpose

user.rpc

Bidirectional

user

JSON discovery/session/script/device/network control.

debug.rpc

Bidirectional

debug

Debug RPC; capture success responds directly with binary JPEG.

repl.stdin

Host to device

user

Raw REPL input bytes, authorized by session/epoch with a separate size limit.

repl.signal

Host to device

user

Currently accepts only one byte 0x03 for Ctrl-C.

repl.stdout

Device to host

user

Text blocks from Python/REPL/HAL and C stdout.

repl.stderr

Device to host

user

Explicit stderr paths such as C stderr; Python exceptions may use stdout.

log.idf

Device to host

debug

IDF/platform log text.

preview.frame

Device to host

user

JPEG data frames produced by img.flush().

Channel-to-stream assignment is fixed. Hosts do not bind separate serial ports for preview/stdout/RPC. The owner holds both streams; RPC replies return to the exact request link.

RPC methods and execution layers

EV-ATP control plane

channel

method

Execution layer

Description

user.rpc

hello / capabilities

transport

Discovery; hello can explicitly acquire ownership.

user.rpc

session.keepalive / session.release

transport

Renew lease, query ready/epoch, release owner.

user.rpc

transport.uart.prepare / transport.uart.probe / transport.uart.commit

transport

UART owner baud negotiation; ordinary UART output pauses until commit or rollback.

user.rpc

file.write

transport / native VFS

Verified binary upload; session/VM-scoped without interpreter execution.

user.rpc

script.write / script.run

VM

Legacy Base64 writer / queue script execution.

user.rpc

device.control

Primarily transport

Dispatch stop/reset/status, network.*, storage.writer.*, launcher status and other actions.

debug.rpc

transport.state / route.get

transport

Observe state/routes without changing public stream bindings.

debug.rpc

debug.info

VM, except stats

device/memory/sensor/fs.list/fs.read; transport.stats does not require VM.

debug.rpc

debug.capture_frame

VM

Encode the last independently published application acquisition in this RPC rsp; no sensor initialization or physical acquisition. Requires debug.capture_frame.observed.v1.

debug.rpc

debug.capture_screen

VM

Encode the last LCD RGB565 frame as JPEG in this RPC rsp, without camera calls.

debug.rpc

route.bind / route.auto

Rejected

v4 returns UNSUPPORTED_OPERATION.

Device Core APIs files.read/remove and python.execute do not imply identically named wire RPCs. Their file/Python composition is described in Interactive Python, scripts and file operations.

debug.capture_screen requests use debug.rpc with the current session and VM epoch; optional quality is 1–100. Successful responses retain request seq/method, set encoding:binary and contentType:image/jpeg, include width / height and carry JPEG payload. An absent LCD returns JSON error NOT_SUPPORTED; no valid displayed frame returns DISPLAY_NOT_READY. Never substitute camera output. Core’s capture({ source: 'screen', format: 'jpeg' }) uses this RPC; see Preview, debug capture, backpressure and statistics.

Requests, responses and asynchronous events can interleave

Figure 14: While waiting for seq=20, the reader must continue handling other complete frames. Match at least type, seq, channel and method, then session/epoch as appropriate. Even an identically numbered event/hello cannot serve as rsp/hello.

Acquisition replies may allocate a new session; discovery replies grant no owner authorization. Successful VM replies must match the requested epoch; control replies may report a later current epoch. Some authorization errors omit session fields: handle their codes without accepting returned values as new authorization.

Reject owner repl.stdout, repl.stderr and preview.frame output from other sessions or old VMs. Never attribute images/control replies to whichever request was most recent.

A verifiable host encoder

This host CPython example explains framing. Use the shared client in Host integration, Wi-Fi configuration and recovery for actual communication.

import json
import zlib

def frame(metadata, payload):
    meta = json.dumps(metadata, ensure_ascii=False,
                      separators=(",", ":")).encode("utf-8")
    data = payload if isinstance(payload, bytes) else payload.encode("utf-8")
    crc = zlib.crc32(data) & 0xffffffff
    header = ("\x1eEVMUX/1 h=%d p=%d c=%08x\r\n" %
              (len(meta), len(data), crc)).encode("ascii")
    return header + meta + data + b"\x1f"

request = frame(
    {"channel": "user.rpc", "type": "req", "method": "hello",
     "seq": 1, "encoding": "json"},
    json.dumps({"evMuxVersion": 4, "acquire": True,
                "clientNonce": "0123456789abcdef0123456789abcdef"}),
)

This function only encodes frames. It implements no session, concurrent reader, deadlines, RPC correlation or recovery, and cannot replace a complete client. Do not assume host zlib/thread/socket combinations are available unchanged in device MicroPython.

Authorized file.write requests are the bounded exception: binary payloads can carry 16384 file bytes plus a 20-byte ID/offset prefix, with CRC32. All other control payloads retain the 2048-byte limit. See Interactive Python, scripts and file operations for begin/data/commit/abort.

Runtime UART baud negotiation

A UART-enabled firmware advertises capabilities.uartBaud with baseline:115200, current, candidates:[2000000,1500000,921600,460800,115200], probeBytes:512, timeoutMs:5000 and switchDelayMs:100. The field is null without a UART backend. These values describe application communication; ROM/stub flashing has its own protocol and lifecycle.

The authenticated UART owner sends the following transport-scoped methods on user.rpc with its current session ID; they remain available during a VM soft reset. USB-OTG, USB-Serial-JTAG and TCP owners cannot change the UART baud rate.

  1. transport.uart.prepare takes {baudRate,token}, where the rate is a listed candidate and token is 1–32 ASCII letters, digits, underscores or hyphens. Its response includes {ok:true,baudRate,token,timeoutMs,switchDelayMs} at the old rate. Firmware pauses ordinary UART output, finishes any already-started frame, drains the hardware FIFO and shift register, then switches after the advertised delay. Host transports reopen/update their serial port and wait at least the advertised delay plus a guard before probing.

  2. transport.uart.probe takes {token,data}, where data is 1–512 printable ASCII bytes. The response returns identical token and data plus baudRate. Hosts compare several different lengths and patterns in both directions; the first usable rate wins in the advertised candidate order. Probes use normal EV-MUX CRC32.

  3. transport.uart.commit takes {token} after at least one probe and returns {ok:true,token,baudRate,...}. Ordinary output resumes after the commit response is queued to hardware. Probe/commit with the same committed token are idempotent, allowing a host to recover a lost commit acknowledgement.

An unfinished negotiation expires after 5000 ms, releases its owner, restores 115200 and clears UART RX/parser state. After a successful negotiation, session release or lease expiry also restores 115200; a lost final commit response may therefore require recovery through the 15000 ms session lease. Hard reset starts at 115200. VM soft reset preserves the physical rate and session lifecycle. Firmware without this capability stays at the initial rate; no host should send speculative baud commands to it.