帧格式、逻辑通道与 RPC 协议
三个版本号
firmware.version 表示 Git/发布构建身份;evMuxVersion 表示客户端必须遵守的会话协议;帧头 EVMUX/1 表示 wire framing。应先用 evMuxVersion 与 capabilities 判断兼容性,再以 firmware.version 展示、记录或校验指定固件,不按日期字符串猜测协议。
一帧的字节布局
0x1e + ASCII "EVMUX/1 h=<H> p=<P> c=<CRC32>\r\n"
+ H bytes of UTF-8 metadata JSON
+ P bytes of payload
+ 0x1f

图 12:只有 H/P 给出正文边界,EOF 字节不是 payload 分隔符。 JPEG、二进制文件数据或文本中可以包含 0x1e、0x1f、CRLF;不能按这些字节截断正文。
H/P 使用 UTF-8 或实际二进制的**字节数**。例如字符“你”通常占三个 UTF-8 字节,不能把 JavaScript 字符串 length 直接当成 payload_bytes。
CRC32 只覆盖 payload,不覆盖 metadata/header;使用标准 CRC32(主机 Python 可用 zlib.crc32(payload) & 0xffffffff),以八位十六进制表示。设备生成的帧带 payload CRC;入站命令 c=00000000 被解释为跳过校验。CRC 提供损坏检测,不提供身份认证,也不证明操作最终完成。
JPEG 响应和预览还需独立验证图像有效性。固件在入队前校验编码输出,Core 在交付前再次检查 JPEG 结构与尺寸;不能只凭 EV-MUX 帧完整或 CRC 正确就交付图像。坏抓图响应在本地返回 INVALID_IMAGE,坏预览整帧丢弃;校验范围及限制见 预览、调试抓帧、背压与统计。
主机必须处理任意拆包与粘包
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]

图 13:流式 parser 保存跨 read 的状态,完整帧验证后才交付。 若格式、长度、CRC 或期限不满足,应记诊断并执行受控重同步;不得把半帧送进 REPL 或文件写入。
当前设备 RX metadata 上限 384 字节、payload 上限 2048 字节。RX parser 有约 3000 ms 空闲和 10000 ms 整帧期限。这些是**设备接收主机命令**的限制,不是 preview 最大图像尺寸,也不应机械作为主机收大图的超时。
metadata 字段
字段 |
类型 |
用途 |
|---|---|---|
|
字符串 |
指定 channel,例如 |
|
字符串 |
RPC 请求 |
|
uint32 |
请求响应相关序号;不同 channel/类型上的相同值不能互相完成请求。 |
|
字符串 |
RPC 方法或 stdio 来源描述;不能把 |
|
可选字符串 |
|
|
32 字符小写十六进制字符串 |
当前 owner 授权/输出范围;发现请求不带 token。 |
|
uint32 |
VM 相关操作及输出范围。 |
|
可选字符串 |
逻辑来源/域,例如 |
|
可选整数 |
设备时间戳,不与主机时钟天然同步。 |
|
按 channel 定义 |
描述图像 payload;JPEG 字节仍在 payload,而非 JSON 中。 |
metadata 与 RPC 请求 payload 必须是完整顶层 JSON object;已知字段类型正确、不能重复。未知扩展字段可以忽略。字符串中的转义不能用简单的逗号分割器解析,JSON 后多余垃圾也不能被视为完整合法对象。
channel 与 stream 对照
channel |
方向 |
stream |
payload / 用途 |
|---|---|---|---|
|
双向 |
user |
JSON 控制:发现、会话、脚本、设备/网络事务。 |
|
双向 |
debug |
调试 RPC;capture 的成功响应直接是二进制 JPEG。 |
|
主机→设备 |
user |
原始 REPL 输入字节;有 session/epoch 授权与独立长度上限。 |
|
主机→设备 |
user |
当前仅合法单字节 |
|
设备→主机 |
user |
Python/REPL/HAL 与 C stdout 的文本块。 |
|
设备→主机 |
user |
C stderr 等显式 stderr 路径;Python 异常也可能在 stdout。 |
|
设备→主机 |
debug |
IDF/平台日志文本。 |
|
设备→主机 |
user |
|
所有 channel 所属 stream 固定;host 不为 preview、stdout 和 RPC 各绑定一个不同串口。owner 持有两组 stream,RPC 响应回到请求的准确 link。
RPC 方法清单与执行层
channel |
method |
执行层 |
说明 |
|---|---|---|---|
user.rpc |
|
transport |
发现;hello 可显式接管。 |
user.rpc |
|
transport |
续租、查询 ready/epoch、释放 owner。 |
user.rpc |
|
transport |
UART owner 协商波特率;提交或回退前暂停普通 UART 输出。 |
user.rpc |
|
transport / native VFS |
可靠二进制上传;绑定会话/VM 代次,不执行解释器。 |
user.rpc |
|
VM |
旧版 Base64 写入 / 排入脚本执行。 |
user.rpc |
|
主要为 transport |
stop/reset/status、network.*、storage.writer.*、launcher 状态等按 action 分发。 |
debug.rpc |
|
transport |
状态与路由观测,不更改公开 stream 绑定。 |
debug.rpc |
|
VM,stats 例外 |
device/memory/sensor/fs.list/fs.read;transport.stats 不依赖 VM。 |
debug.rpc |
|
VM |
在此 RPC rsp 中编码应用最近发布的独立采帧副本;不初始化传感器或物理采帧。要求 |
debug.rpc |
|
VM |
LCD 最后 RGB565 帧编码 JPEG,在此 RPC rsp 中返回;不调用摄像头。 |
debug.rpc |
|
拒绝 |
v4 返回 |
高层 Device Core 的 files.read/remove 与 python.execute 是客户端 API,不代表线上存在同名 RPC。文件与 Python 的具体组合见 交互式 Python、脚本与文件操作。
debug.capture_screen 请求通过 debug.rpc 携带当前会话与 VM epoch,可选 quality 为 1–100。成功响应保持原请求 seq/method,encoding:binary、contentType:image/jpeg,携带 width / height 与 JPEG payload。无 LCD 返回 JSON error NOT_SUPPORTED,尚无有效显示帧返回 DISPLAY_NOT_READY;不能用摄像头图像代替。Core 的 capture({ source: 'screen', format: 'jpeg' }) 使用这一 RPC,详见 预览、调试抓帧、背压与统计。
请求、响应与异步事件可以交错

图 14:等待 seq=20 的响应时,reader 仍应处理其他完整帧。 响应匹配至少比较 type、seq、channel、method;随后按适用规则检查 session/epoch。event/hello 即使恰好使用同 seq,也不能作为 rsp/hello。
接管响应可分配新的 session;发现响应没有 owner 授权范围。VM 成功响应必须属于请求的 epoch;控制响应可能报告更晚的当前 epoch。部分授权失败响应不带 session 字段,须按错误码处理,不采纳里面的值为新的授权。
收到 owner 的 repl.stdout、repl.stderr、preview.frame 时,拒绝其他 session 或旧 VM 的输出;图像与控制回复不能根据“最近的请求”猜归属。
一个可验证的主机编码器
以下为**主机 CPython**示例,用于解释 framing;真正通信推荐使用 主机接入、Wi-Fi 配置与恢复实践 中的共享客户端。
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"}),
)
该函数只编码帧,不实现会话、并发 reader、期限、RPC 关联或恢复。它不能替代完整客户端,也不能在设备 MicroPython 中直接假定可用主机 zlib/线程/socket 组合。
file.write 是经过会话授权后的有界例外:二进制 payload 可携带 16384 字节文件内容和 20 字节 ID/offset 前缀,并校验 CRC32;其他控制 payload 仍限制 2048 字节。begin/data/commit/abort 见 交互式 Python、脚本与文件操作。
运行态 UART 波特率协商
启用 UART 的固件通过 capabilities.uartBaud 发布 baseline:115200、current、candidates:[2000000,1500000,921600,460800,115200]、probeBytes:512、timeoutMs:5000 和 switchDelayMs:100。未启用 UART 时该字段为 null。这些参数用于应用运行期间的通信;ROM/stub 烧录使用独立的协议与连接生命周期。
已授权的 UART owner 在 user.rpc 上携带当前 session ID 调用以下传输层方法,VM 软复位期间仍可调用。USB-OTG、USB-Serial-JTAG 和 TCP owner 无权改变 UART 波特率。
transport.uart.prepare接受{baudRate,token},速率须为公布的候选值,token 为 1–32 个 ASCII 字母、数字、下划线或连字符。固件以 旧速率 返回{ok:true,baudRate,token,timeoutMs,switchDelayMs}。协商期间暂停普通 UART 输出,让已开始的帧发送完毕,并确认硬件 FIFO 及移位寄存器 排空,再在公布的延迟后切换。主机传输适配器重新打开或调整串口,并等待公布的延迟加保护时间后开始探测。transport.uart.probe接受{token,data},data 为 1–512 字节可打印 ASCII。响应原样返回token、data以及baudRate。主机使用不同长度和内容进行多组双向校验,按候选列表顺序选择首个通过的速率并停止。探测帧使用普通 EV-MUX CRC32。至少一次探测后调用
transport.uart.commit,参数为{token},响应为{ok:true,token,baudRate,...}。提交响应进入硬件后恢复普通输出。相同已提交 token 的 probe/commit 支持幂等重试,以恢复丢失的最终确认。
未完成的协商在 5000 ms 后超时,释放 owner、恢复 115200,并清空 UART RX 与解析状态。协商成功后,正常释放会话或租约过期同样恢复 115200;丢失最终提交响应的情况可能需要通过 15000 ms 会话租约恢复。硬复位从 115200 启动,VM 软复位保留物理速率与会话生命周期。旧固件未发布该能力时保持初始速率,主机不得试发变速命令。