ESPectre 资源化设备 API 架构决策(ADR)深度解析:从 RPC 到 /espectre/v1 的演进与实现
【免费下载链接】espectreWi-Fi CSI motion sensing for ESP32. C++ SDK, ESPHome, Native, and Matter frontends, browser tools, and a CLI for the full device lifecycle. GPLv3 and commercial licensing.项目地址: https://gitcode.com/GitHub_Trending/es/espectre
本文基于 ESPectre 仓库的架构决策记录 docs/adr/2026-09-03-adopt-resource-oriented-device-api.md(状态:Accepted),完整剖析该 ADR 的前因后果:为何放弃单一的POST /requestRPC 端点,如何用/espectre/v1下的资源化 Direct HTTP 重写读写与动作语义,以及如何在 HTTP 与 MQTT 之间保持单一规范资源/操作模型。读完本文,你将掌握 ESPectre 3.0 时代设备 API 的资源划分、HTTP 方法语义、GET /csi独占集合生命周期、MQTT 保留策略与可执行契约(C++ 协议注册表 + 命令引擎)的实际落地方式,并能据此为浏览器工具、CLI、benchmark 与 SDK 客户端编写兼容的调用代码。
一、背景:POST /requestRPC 模型的四大痛点
在资源化 API 之前,第一版 Direct API 把所有读取、变更与动作都压缩到唯一一个POST /requestRPC 端点里。该 ADR 记录了这一模型在多传输、多前端环境下的系统性缺陷:
- 生命周期概念重复:原始 CSI 采集需要显式的 start/stop 命令、一个 bearer 绑定以及 bind 超时,客户端才能打开二进制响应流——协议自造了一套"会话状态机",而不是依赖传输层已有的连接语义。
- 字段冗余:每条消息都重复携带身份(identity)与版本(version)字段,payload 臃肿且容易漂移。
- 浏览器缓存困难:所有请求都是 POST,浏览器无法按资源做条件请求或缓存控制。
- 传输机制泄漏为应用命令:传输层的细节(如 session、bearer、bind timeout)以应用命令的形式暴露在协议表面,HTTP 与 MQTT 的行为因此难以对齐。
与此同时,MQTT 侧也积累了一批与资源归属不匹配的查询命令和 topic payload。诊断、发现、配置、可用性与高帧率传感事件需要不同的保留(retention)与安全策略:例如传感事件是高频、可丢弃的,而配置与可用性状态必须是持久的、受控的。单一命令模型无法表达这些差异,这正是推动资源化重构的根本原因。
该 ADR 部分取代了三份先前的决策记录:
- docs/adr/2026-07-02-use-one-message-model-and-command-engine-across-transports.md(单一消息模型与跨传输命令引擎,其"单一模型多传输"的结论被保留,但旧的查询/变更命令名被资源目录取代);
- docs/adr/2026-07-03-unify-raw-csi-collection-over-http.md(原始 CSI 统一走 HTTP,二进制帧、排序、来源与固定环形缓冲行为继续生效,但显式命令建会话与 bearer 绑定被
GET /csi连接生命周期取代); - docs/adr/2026-08-17-adopt-improv-serial-and-direct-http-for-local-control.md(Improv Serial 配网 + Direct HTTP 本地控制,其传输选择、端口、CORS 边界与发现服务继续生效,但
/requestRPC 被资源 API 取代)。
二、核心决策:资源化 Direct HTTP 与 HTTP 方法语义
ADR 的关键决策是:保留应用版本1.0,但用一个尚未发布的资源化 Direct HTTP 取代旧 API,基础路径为/espectre/v1。语义如下:
| HTTP 方法 | 语义 | 在 API 中的典型用途 |
|---|---|---|
GET | 读取资源 | health、device、capabilities、sensing、wifi、diagnostics、events、csi |
PATCH | 部分更新资源 | device(label)、sensing(阈值/检测器/命中次数等)、mqtt |
PUT | 替换式设置 | wifi/bssid(BSSID 固定选择,整体替换) |
DELETE | 移除资源 | wifi/bssid、wifi/credentials、mqtt |
POST | 子资源动作 | sensing/calibrations、wifi/scans、OTA 的checks/updates |
关键约束:不存在/request路由,也没有任何兼容别名。旧客户端的POST /request在 3.0 起直接失效,必须迁移。
源码中的可执行路由表
资源化 API 并非停留在文档层面,而是由一个 C++ 常量路由表直接驱动。在 src/cpp/runtime/espectre_protocol.cpp 中,kApiRoutes定义了完整的基础路由,每条路由携带{http_method, path, name, command, method, kind, asynchronous}:
{"GET", "/espectre/v1/health", "health", "health", ..., RESOURCE, ...}, {"GET", "/espectre/v1/device", "device", "device", ..., RESOURCE, ...}, {"GET", "/espectre/v1/capabilities", "capabilities", "capabilities", ..., RESOURCE, ...}, {"GET", "/espectre/v1/sensing", "sensing", "sensing", ..., RESOURCE, ...}, {"GET", "/espectre/v1/wifi", "wifi", "wifi", ..., RESOURCE, ...}, {"GET", "/espectre/v1/wifi/access-points", "wifi/access-points", "wifi_access_points", ..., RESOURCE, ...}, {"GET", "/espectre/v1/mqtt", "mqtt", "mqtt", ..., RESOURCE, ...}, {"GET", "/espectre/v1/diagnostics", "diagnostics", "read_diagnostics", ..., RESOURCE, ...}, {"GET", "/espectre/v1/devices", "devices", "devices", ..., RESOURCE, ...}, {"PATCH", "/espectre/v1/device", "update_device", "update_device", ..., OPERATION, ...}, {"PATCH", "/espectre/v1/sensing", "update_sensing", "update_sensing", ..., OPERATION, ...}, {"PATCH", "/espectre/v1/mqtt", "update_mqtt", "update_mqtt", ..., OPERATION, ...}, {"POST", "/espectre/v1/sensing/calibrations", "recalibrate", "recalibrate", ..., OPERATION, true}, {"POST", "/espectre/v1/wifi/scans", "scan_wifi", "scan_wifi", ..., OPERATION, true}, {"PUT", "/espectre/v1/wifi/bssid", "set_wifi_bssid", "set_wifi_bssid", ..., OPERATION, true}, {"DELETE", "/espectre/v1/wifi/bssid", "clear_wifi_bssid", "clear_wifi_bssid", ..., OPERATION, true}, {"DELETE", "/espectre/v1/wifi/credentials","clear_wifi_credentials", "clear_wifi_credentials", ..., OPERATION, true}, {"DELETE", "/espectre/v1/mqtt", "clear_mqtt", "clear_mqtt", ..., OPERATION, ...}, {"GET", "/espectre/v1/events", "events", "", ..., STREAM, ...}, {"GET", "/espectre/v1/csi", "csi", "", ..., STREAM, ...},从这张表可以看到三个设计要点:
- 资源(RESOURCE)全部走 GET,操作(OPERATION)全部是 PATCH/POST/PUT/DELETE,两个流式端点(STREAM:
/eventsSSE、/csi二进制)也是 GET。 - OTA 不在基础路由表中。根据 docs/API.md,OTA 是前端拥有的"规范消息模型的扩展":Native 通过同一个扩展目录(
EspectreProtocolExtension)向 Direct HTTP 与 MQTT 注册自己的资源、操作、事件与参数校验。validate_protocol_extension强制扩展路由必须以/espectre/v1/开头、与基础路由不得冲突(见 src/cpp/runtime/espectre_protocol.cpp)。这样感测 SDK(sensing SDK)不需要实现固件升级。 diagnostics路由是唯一的"带参数 GET":HTTP 端既支持在 GET body 中携带 JSON 参数对象,也支持浏览器友好形式GET /espectre/v1/diagnostics?fields=%5B%22traffic_tx_pps%22%5D(URL 编码的 JSON 数组)。路由解析器对 query string 做了严格限制:只有 diagnostics、只有 GET、且不能与 body 同时使用,其他路径带 query 一律拒绝(见 src/cpp/runtime/direct_http_protocol.cpp)。
端点与请求/响应框架
Direct HTTP 监听 TCP 端口62587(即0xF47B,字符 U+1F47B GHOST 的低 16 位,定义于 src/cpp/runtime/direct_http_protocol.h)。客户端从GET /espectre/v1/capabilities开始,用返回的resources与operations目录推断支持面,而不是凭前端名称硬编码——这是"能力协商优先"的核心原则。
协议框架约定(详见 docs/API.md):
- 成功的 GET 直接返回资源对象;请求体(若有)必须是 JSON 对象,空 body 等价于
{};C++ 前端最多接受 2,048 字节请求体;非空 body 必须带Content-Type: application/json。 - 变更操作返回统一结果对象:
{"accepted":true,"code":"ok","message":"operation accepted","data":{}},其中data可选。 - 同步变更返回 HTTP
200;异步与破坏性路由在请求通过应用分发后返回 HTTP202。 - 校验、能力、冲突类错误使用 docs/API.md 中的状态码(
400 invalid_params、404 unsupported、409 busy/conflict/busy_raw_collection等);分发前被 HTTP 服务拒绝的失败(如413超限、415非 JSON、429限流、503停止中)不是规范结果对象,客户端必须先检查状态码与Content-Type再解析 JSON。
路由解析的具体实现可见 src/cpp/runtime/direct_http_protocol.cpp:parse_direct_http_request先分离 query string,再在基础路由表与扩展路由中按(http_method, resource)精确匹配,匹配失败即返回"unsupported Direct resource or method";随后做 body 大小检查与 JSON 对象字段解析(重复字段会被拒绝,测试 test/cpp/suites/runtime/test_espectre_protocol.cpp 验证了这两类错误)。HTTP 服务端在 src/cpp/runtime/esp_idf/direct_http_service_esp_idf.cpp 中按GET/POST/PUT/DELETE四种方法注册/espectre/v1/*通配 URI(对应测试 test/cpp/suites/runtime/test_direct_http_service.cpp 断言了注册结果)。
三、跨传输的单一资源与操作模型
ADR 明确要求:HTTP 与 MQTT 共享同一套规范资源与操作模型——资源 payload、操作名、校验规则与结果码完全一致,只有传输封装与投递策略不同(这一结论继承自已部分取代的 docs/adr/2026-07-02-use-one-message-model-and-command-engine-across-transports.md)。
版本与身份字段的收敛
为了消除冗余字段,协议对protocol_version与device_id的出现位置做了硬性限定:
protocol_version只出现在capabilities资源与发现(discovery)元数据中,普通应用 JSON 不再携带。device_id只出现在device资源、多设备发现结果以及既有的二进制 CSI 记录元数据中。- MQTT 命令请求(
commands/request)顶层只有command_id与command,参数也在顶层,既无protocol_version也无device_id。
capabilities资源承载协商所需的一切:protocol_version(当前1.0)、resources(GET 可用的资源名数组)、operations({name, method, path}数组)、events(事件名数组)、features.csi(是否支持GET /csi)以及可选的csi对象。客户端必须容忍资源的增量式扩展(additive fields)。
MQTT 保留与可用性策略
MQTT 侧的主题布局与保留策略同样按资源边界划分(docs/API.md):
| Topic 后缀 | Retained | 用途 |
|---|---|---|
health | 是 | 健康、可用性与 Last Will |
device | 是 | 设备身份与构建信息 |
capabilities | 是 | 协商与支持面 |
sensing | 是 | 感测状态与调参 |
wifi | 是 | 脱敏后的无线状态 |
ota | 是 | OTA 状态 |
motion | 否 | 每次检测评估的运动事件 |
fault | 否 | 运行时故障 |
commands/result | 否 | 关联的命令结果 |
要点:
health兼作 Home Assistant 可用性主题与 MQTT Last Will:设备非优雅断开后由 broker 发布 retained 的离线health。除了 MQTT keepalive 之外不再有应用层心跳。- 设备状态按资源 retained 发布(device/capabilities/sensing/wifi/ota),而每个评估周期的
motion、fault与关联命令结果都不保留——高频、可替换的事件不应占用 broker 存储。 wifi的 retained payload 做了脱敏:只含configured、connected、band、channel、rssi_dbm、apply_state、apply_message,不发布 SSID、BSSID 与 IP。- MQTT 不提供 MQTT 配置、诊断、发现与 CSI 相关主题;
read_diagnostics的结果通过commands/result.data返回。诊断走"关联响应"而非事件流。
MQTT 命令集与 HTTP 操作一一对应:update_device↔PATCH /device,update_sensing↔PATCH /sensing,recalibrate↔POST /sensing/calibrations,read_diagnostics↔GET /diagnostics,check_ota/start_ota↔POST /ota/checks//ota/updates。命令 payload 限制 2,048 字节,command_id为 1–64 字符(ASCII 字母、数字与_、-、.、:)。MQTT 没有 HTTP 状态码,订阅者依据commands/result中的accepted与code判断;不在已发布命令集内的命令返回forbidden。
三个 C++ 前端共享一个分发器
"单一模型多传输"在代码上的落点,是所有 C++ 前端(Native、ESPHome、Matter)共用同一个FrontendCommandEngine(src/cpp/runtime/frontend_command_engine.cpp 的execute入口)与同一个 CSI 会话控制器。各适配器只负责传输帧、来源(Origin)与访问策略,随后在前端任务上串行执行规范命令。ESPHome 实体写入与 Direct 变更最终收敛到同一份权威运行时状态。
四、GET /csi:独占集合生命周期
资源化 API 最激进的一处变化,是用连接生命周期取代显式命令管理的会话。ADR 的表述是:
GET /csi拥有整个独占集合生命周期的所有权:TCP 响应开始集合,其关闭停止集合,客户端采用第一帧二进制数据的会话 ID。
具体协议行为(详见 docs/API.md):
GET /espectre/v1/csi打开唯一的独占二进制 CSI 集合会话。没有setup 请求、bearer token、会话删除或 bind 超时;关闭 TCP 响应即结束集合。- C++ 运行时要求在集合开始前感测服务已武装(armed)。若感测被禁用或因重配置/维护而挂起,打开集合会失败,且不会偷偷重新启用 CSI 采集或流量生成。
- 集合期间暂停所有传输上的派生感测事件(motion 及未来所有派生事件),但控制与资源事件保持活跃;集合结束(含需重新校准时)后先恢复感测与 readiness,派生事件才恢复。
- 集合期间第二次
/csi请求,以及感测、Wi-Fi、OTA 变更,都返回409。 - 每个 CSI V8 记录保留既有的 60 字节小端 HTTP 前缀;客户端采用第一帧的 16 字节会话标识符,同一连接内变更即拒绝。生产方保序,固定环形缓冲的丢弃在传输计数器中可观察(
fresh_record_total + raw_drop_total == classified_frames_offered_to_raw)。 - 原始工作线程在无可用 CSI 记录时检测到客户端断开,会直接关闭会话,不合成记录。
源码佐证:显式命令已彻底消失
src/cpp/runtime/esp_idf/raw_csi_session_controller.cpp 中的handle_command展示了这一转变的直接后果:旧的"开始/停止集合"命令处理函数被保留为一个空壳,任何到达的命令都返回code = "unsupported",消息固定为"CSI collection is opened with GET /csi"。也就是说,从协议层面就杜绝了经由命令引擎打开 CSI 会话的路径——集合只能由GET /csi发起。会话开始时会用esp_fill_random生成 16 字节会话 ID(src/cpp/runtime/esp_idf/raw_csi_session_controller.cpp),并在begin()中校验能力(supports_raw_csi)与互斥状态(RAW_COLLECTION),确保独占性。
外部 CSI 流量
external模式下,ESPHome、Native 与 Matter 接受发往设备的 UDP 标记与单播 ICMP Echo Request。UDP 可用设备 IP 或csi_traffic_multicast_group(默认239.255.0.1,置空则关闭组播加入、保留单播接收);UDP 监听端口5555,只接受恰好四字节的 UTF-8 标记F0 9F 91 BB。外部主机负责两种协议的节奏控制;./espectre collect会持续选择external并导入标准库实现的ExternalTrafficGenerator(tools/espectre_traffic_generator.py),其--pps只控制该 UDP 生成器与数据集来源记录。详细命令见 docs/CLI.md,投递限制见 docs/CSI.md。
五、规范拆分与可执行契约
ADR 将公共规范一分为二:
- docs/API.md:拥有 Direct HTTP、SSE 与 MQTT 的公开应用契约,包括全部资源 schema、操作参数、事件定义、CSI 集合、错误码与安全版本策略;
- docs/DISCOVERY.md:拥有 DNS-SD/mDNS、浏览器引导发现与
/devices资源(TXT 记录、bootstrap 行为、/devices扫描 schema 与序列化限制)。
两者通过 TXT 记录中的protovers=1.0与path=/espectre/v1衔接:客户端从发现拿到基础路径后,再通过GET /capabilities协商精确表面。
C++ 注册表与命令引擎是可执行契约
"可执行契约"(executable contract)的含义是:文档描述行为,但真正裁决行为的是代码。kApiRoutes路由表(src/cpp/runtime/espectre_protocol.cpp)、参数校验(validate_sdk_command_parameters)与FrontendCommandEngine共同构成权威实现;HTTP 与 MQTT 的映射关系由 C++ 专门测试维护,espectre_transport_mapping_payload()直接序列化该映射供跨语言对账。测试 test/cpp/suites/runtime/test_espectre_protocol.cpp 覆盖了规范消息解析、未知资源返回"unsupported Direct resource or method"、畸形 JSON 与重复字段拒绝;test/cpp/suites/runtime/test_direct_http_service.cpp 则验证了扩展路由(vendor 资源/动作)通过EspectreProtocolExtension注入的机制。
Micro-ESPectre:只读交集
由于 MicroPython 无法共享 C++ 实现,Micro-ESPectre 维护等价的注册表与分发器,但只发布受支持的只读交集。从 src/python/micro_espectre/direct_api.py 可以看到其实质:
_status()/_config()/_wifi()/_diagnostics()提供health、sensing、wifi、diagnostics的只读快照;_config()输出与 C++ 同构的sensingschema(enabled/ready/calibrating/mode/threshold/detector/motion_on_hits/motion_off_hits/csi_traffic_mode/traffic_generator_mode/csi_target_pps);- 唯一支持的变更动作是校准:
take_recalibration_request()认领一个排队的 Direct 校准请求,complete_recalibration()允许 Direct worker 接受下一个请求——对应POST /sensing/calibrations(HTTP202,繁忙时409 busy); publish_motion()在存在事件客户端时发布规范 motion 事件(state为idle或motion,携带score)。
根据 docs/API.md 的支持矩阵,Micro 支持全部 GET 资源(含calibrations),但不支持wifi/access-points、devices、csi、mqtt、ota及一切 PATCH/PUT/DELETE 变更。不支持的资源与方法组合统一返回 HTTP404——未受支持的前端资源不会出现在 capabilities 中,且返回 404,这是 ADR 后果清单里明确的一条。
六、后果与迁移影响
ADR 的 Consequences 可以归纳为六点,逐一与代码对应:
- 浏览器、CLI、benchmark 与 SDK 客户端操作显式资源。Web 侧 docs/web/content/tools/monitor.html(Monitor)、docs/web/content/tools/device-settings.html(设备设置)、docs/web/content/tools/csi-visualizer.html(CSI 可视化)以及 tools/benchmark_firmware.py 都按资源直接请求所需字段;通用 CLI 的
read_diagnostics默认["*"],显式传fields: []可查看目录。 - 三个 C++ 前端共享一个分发器与一个 CSI 会话控制器。
FrontendCommandEngine与RawCsiSessionController是共享实现,前端只提供传输适配。 - MQTT 的保留、可用性与命令安全遵循资源边界。retained 策略、
healthLast Will、wifi脱敏、commands/result不保留,均按上表执行。 - 显式原始会话命令、bearer 绑定、每消息版本字段与冗余身份字段消失。
raw_csi_session_controller的handle_command直接返回unsupported即为代码级证明;protocol_version/device_id的位置约束写入契约。 - 未受支持的前端资源不在 capabilities 中并返回
404。能力协商(GET /capabilities)是权威来源,自定义构建禁用某特性时同样生效。 - 使用旧 RPC 模型的预发布客户端必须迁移,且没有兼容期。API.md 明确"不存在旧路由、命令、主题或别名";在 3.0.0 release-candidate 阶段,协议契约保持
1.0(Direct/espectre/v1、MQTT 前缀espectre/v1/devices、DNS-SDprotovers=1.0),诊断字段选择行为也在该预发布契约内演化——使用匹配版本的固件与客户端,没有到旧诊断响应的自动回退。
七、总结与阅读路径
资源化设备 API 是 ESPectre 从"单端点 RPC + 命令会话"走向"资源化、能力协商、跨传输一致"的关键架构转折。它以最小的字段占用(版本与身份只在必要处出现)、显式的资源所有权(GET /csi独占集合)、按资源划分的保留与安全策略,统一了 Native、ESPHome、Matter 与 Micro 四类前端的可编程表面,并把可执行契约交给 C++ 注册表与命令引擎,而不是停留在文档层面。
若想继续深入,建议按以下顺序阅读仓库内容:
- 本 ADR 及其决策谱系:docs/adr/2026-07-02-use-one-message-model-and-command-engine-across-transports.md、docs/adr/2026-07-03-unify-raw-csi-collection-over-http.md、docs/adr/2026-08-17-adopt-improv-serial-and-direct-http-for-local-control.md;
- 完整契约:docs/API.md 与 docs/DISCOVERY.md;
- 可执行实现:src/cpp/runtime/espectre_protocol.cpp、src/cpp/runtime/direct_http_protocol.cpp、src/cpp/runtime/frontend_command_engine.cpp、src/cpp/runtime/esp_idf/raw_csi_session_controller.cpp;
- 只读 Micro 实现:src/python/micro_espectre/direct_api.py;
- 契约测试:test/cpp/suites/runtime/test_espectre_protocol.cpp、test/cpp/suites/runtime/test_direct_http_service.cpp;
- 相关底层协议:docs/CSI.md、docs/CLI.md。
【免费下载链接】espectreWi-Fi CSI motion sensing for ESP32. C++ SDK, ESPHome, Native, and Matter frontends, browser tools, and a CLI for the full device lifecycle. GPLv3 and commercial licensing.项目地址: https://gitcode.com/GitHub_Trending/es/espectre
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考