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.

Read-only firmware installation evidence

firmware.status is a user.rpc method advertised as firmware.status.v1 only by the opt-in ESP32_S31_MOSAICO Iris-package build. Acquire an EV-MUX v4 owner session first. Send {"imageSize": N}, where N is the exact application BIN length from the validated package (1–7340032 bytes). The request runs on the transport task, without REPL entry, Python execution, writes or reset. Missing/malformed sizes return INVALID_ARGUMENT; incomplete VM startup returns NOT_READY; unconfirmed OTA or unreadable installation evidence returns EVIDENCE_UNAVAILABLE.

A successful response has ok=true, schemaVersion=1, deviceId (factory MAC as 12 lowercase hex digits), board, firmware.id/version, a 32-character hexadecimal bootId, vmReady=true, bootConfirmed=true, numeric IDF resetReason and crashReset. The boot token is stable across VM soft resets and changes on hardware reboot; the installer compatibility layer uses its first eight bytes as the little-endian Iris Boot ID.

application contains address, partition capacity, requested imageSize, full BIN sha256, elfSha256 and otaState="valid". The full BIN hash includes all requested bytes, including the appended image digest; it is not ESP-IDF’s embedded image hash. partitionTableSha256 covers exactly 4096 Flash bytes starting at the configured partition-table offset. installation.operationId and signed installation.result expose the retained installer’s persisted commit receipt without sending or implementing its protocol. Missing or malformed receipts fail the query instead of inventing a successful receipt. A recorded nonzero result is returned unchanged and must fail host acceptance.

An installation verifier must compare the live device identity, board, exact package length/hashes/layout and operation ID against its intended installation, require zero commit result and crashReset=false, and observe a stable boot token across acceptance. A healthy query for another image or an earlier operation is not installation success. Startup confirmation does not validate user Python code or peripherals. This interface does not enter Vibe Mode; the operator uses AI + Reset for the next installation.

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.