帧格式、逻辑通道与 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 提供损坏检测,不提供身份认证,也不证明操作最终完成。
主机必须处理任意拆包与粘包
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 / 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 中返回 JPEG。 |
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、脚本与文件操作。