• 机器人
  • 嵌入式
  • 强化学习
  • 人工智能
  • 智能硬件
  • 计算机视觉
  • 音视频

【免费下载链接】microduck

A Tiny biped duck robot 🦆

项目地址: https://gitcode.com/gh_mirrors/mi/microduck
点击查看 免费下载

本指南围绕 microduck 仓库中 docs/design/webrtc-console.md 记录的四项落地变更展开:mediad 亲自用 HTTP 提供控制台页面、duckctl 新增 ip 与 open 两个命令、工具从 duck-btctl 更名为 duckctl、以及 index.html 从"证明传输可用"的测试页升级为"人可驾驶机器人"的控制台。读完本文,你将掌握机器人控制台的访问链路(http://<robot>:8080/ → WebRTC 信令 → 控制数据通道)、BLE 广播寻址的原理与回退策略、WebRTC 与 BLE 两套传输各自允许/拒绝的方法子集,以及页面与服务端之间端口、版本号等关键信息的注入机制。文中所有结论均可在当前仓库的源码与配置中找到对应实现。

本文是 remote-webrtc.md 的姊妹篇——后者拥有传输层(信令、会话模型、授权、peer 可调用什么),本文只负责"页面"这一端:在两者交叠的地方,页面是所有者,本文指向它。

0. 背景:测试页作为"客户端"的六个问题

mediad/webclient/index.html 最初证明了传输是可行的(浏览器能拿到视频和数据通道),但作为一个客户端它存在六处缺陷,每一处都是一道横在人跟机器人之间的坎:

缺陷说明
需要 python3 -m http.server多一个工具、多一个终端,而且页面是从笔记本上被服务出去的,再去和机器人对话
URL 需要手输ws://radxa-zero3.local:8443——而 radxa-zero3 是每一块从同一镜像刷出来的板子的主机名(configd/src/identity.rs 在回退时会用到它),同一网络上有两台机器人就会撞车
注释块里全是警告file:// 与 Private Network Access、http 而非 https——这个头已经被修正过两次(commit 7f52a34)
它验证的是协议而非机器人mediad/src/route.rs 允许 move、head、look、pose、mouth、do、sound、enable、init、relax、stop、subscribe、tof.stream、pad.input,但页面只提供了 robot.health 和一个 JSON 文本框
它没有被发布到任何地方没有 --include 提及它,因此野外机器人没有客户端
它无法说出自己连上了哪台机器人producer 的 meta 未被填充,list 只返回一个 id 和空信息

这六项没有一项跟 WebRTC 本身有关,却每一项都在拖慢"人到机器人"的距离。

1. mediad 亲自提供页面:一次删除四个问题

已落地——实现位于 mediad/src/web.rs,入口参数为 --web-port,默认 8080。

mediad 内部多了一个 HTTP 监听器,只有一条路由,返回页面本身。http://<robot>:8080/ 就是全部,不需要再跑任何别的东西。这一改动同时消掉了四个问题:

  • 没有 python3 了。指令从一个"两段式"变成"一个地址"。
  • 没有需要手输的 URL 了。页面把信令目标默认成 ws://${location.hostname}:8443——它既然是被机器人服务出来的,自然知道自己在跟哪台机器人说话。
  • Private Network Access 检查不再适用。Chrome 会拦截"从 public/opaque 源发往私有地址"的请求;从 192.168.x 服务出来的页面拥有私有源,当初让 file:// 失效的那道检查根本不会被触达。头部警告块随之删除。
  • 只有一个版本。页面和二进制一起发布(见 §1.2),checkout 出来的客户端再也无法被指向一个 release 版机器人。

从源码看,mediad/src/main.rs 中 web::serve 在 pipeline 之前就被 spawn:页面先于视频启动——"页面无法被服务不会拖垮视频",一个能推流、能应答控制调用但没有控制台的机器人,远好过一个什么都不做的。

1.1 用哪个 HTTP 服务器:axum

决定用 axum。它已经在 Cargo.lock 中——updater 把它作为 dev-dependency 用于测试镜像——所以该 crate 对当前工具链和交叉编译是已知良好(known-good)的。

备选方案是手写一个 HTTP/1.1 应答器:只服务一个文件、一个方法,大概六十行。但把六十行手写请求解析绑到 0.0.0.0,等于把"暴露给全网每个人的解析器"写出来,仅仅是为了省一个构建早已解析好的依赖;而 axum 底层的 hyper 是这门语言里被阅读次数最多的 HTTP 解析实现。

mediad/systemd/mediad.service 里的 RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6 已经允许监听器存在,unit 文件无需任何改动。

1.2 页面是嵌入的,不是安装的

const PAGE: &str = include_str!("../webclient/index.html");

页面通过 include_str! 在编译期嵌入,从内存里被服务出来。备选方案——安装到 /opt/robot/daemon/current/webclient/ 再在请求时读取——需要在三个地方各加一行 --include(_build-release.yml、dev.yml、scripts/dev-push.sh),而那正是此前漂移过的列表;它还会在一个运行 ProtectSystem=strict 的 unit 里把文件系统读取塞进网络请求背后,并让"这台机器人服务的是哪个页面"变成一个有两个答案的问题。嵌入的代价是改样式表要重新构建——对一个属于守护进程接口的页面来说,这是正确的取舍。

1.3 两个端口,而任何人都无需知道有两个

webrtcsink 拥有 8443 上的监听器(run-signalling-server,只能通过 -host 和 -port 调整),所以页面不可能成为它的一个路由。于是有两个端口:8080 服务页面,8443 仍是信令服务器。媒体路径零改动——这正是先这么做的全部理由。

两个端口是实现层面的一个事实,绝不能变成人的一步操作。 三件事把它挡在人视线之外,它们是对这次改动的硬性要求而非期望:

  1. 只需要输入一个地址:http://<robot>:8080。8443 由页面自己的 JavaScript 触达,人永远不用输入;duckctl open(§2)连那一个地址都省了。
  2. mediad 在服务页面时把信令 URL 填进去,而不是让页面携带一个常量。它知道请求到达时用的 host,也知道自己的 --port,所以页面拿到的是真实答案——启动时对嵌入字符串做一次 str::replace 即可。这正是让 --port 可以安全改动的关键:不存在第二处保存过时副本的地方。
  3. 两个端口唯一可能产生的故障要用文字说清。如果 8080 应答了而 8443 没有——最常见的是中间有防火墙——页面必须说"页面来自这台机器人,但它的信令端口没有应答",而不是一个笼统的 websocket error。这是第二个端口唯一可能到达人的路径,它应该以诊断的形式到达。

从源码看,mediad/src/web.rs 用两个 token 完成注入:

const PORT_TOKEN: &str = "{{SIGNALLING_PORT}}";
const API_TOKEN: &str = "{{API_VERSION}}";

pub fn page(signalling_port: u32) -> String {
    PAGE.replace(PORT_TOKEN, &signalling_port.to_string())
        .replace(API_TOKEN, &proto::API_VERSION.to_string())
}

配套的测试(如 both_tokens_are_filled_in、the_page_carries_the_api_token_this_module_replaces)钉死了"页面绝不能在带 token 的状态下发出去"这一事实,同时仓库里的原页必须保留 token——那是远端 Space 副本读取"没有机器人服务过我"信号的依据,scripts/publish-console.sh 在部署时断言了另一半。

单端口方案的终局:运行我们自己的信令服务器(上游把其作为插件旁的库发布),并把 webrtcsink 的 signaller 指向 ws://127.0.0.1:8080/ws。那样一个 axum 服务器在同一源上同时服务页面与协议。这需要更多工作,还要维护一份与所交付 .so 保持同步的协议版本副本,所以不在第一波改动里。但它就是这条路的终点:浏览器不会给纯 http 服务到私有地址的页面麦克风,某些浏览器连 gamepad 都不给——这些是 secure-context API,而 http://192.168.1.42 不是安全上下文(http://localhost 是,这正是迄今没人撞上它的原因)。双路音频见 remote-webrtc.md §2。机器人终将需要提供 TLS,而 webrtcsink 内置服务器无法配置证书,axum 服务器则有常规途径。所以序列是:现在两个端口;想要音频或浏览器 gamepad 时换成自有信令服务器;同一天上 TLS。

2. 找到机器人:两个命令,其中一个早已被手搓过

已落地——duckctl ip 与 duckctl open,先读广播、net.status 兜底。

btd 在 company id 0xFFFF 下把机器人的 IPv4 写进广播,而 duckctl 早已会解析它——duckctl/src/main.rs 中的 Address 枚举是三个答案而非两个:At、Unassigned、Unsaid,且 scan 今天就打印它。所以这部分的活是一个命令,而不是一个新机制。

2.1 duckctl ip

机器人的地址打印在 stdout 上,除此之外什么都没有,于是 ssh radxa@$(duckctl ip) 可以直接工作——这保持了工具既有的分工:诊断走 stderr,数据走 stdout。

这在本仓库并非新点子,而是已经被写砸过一次的点子。scripts/dev-push.sh 的 resolve_board() 恰好需要这个,并手搓了它:调用 duckctl wifi status 并把 JSON 管道给一段嵌在 shell 脚本里的六行 Python,从中提取 result.ip4。那就是这个命令,只是没有安家。

读广播而非调用 net.status,在三个维度上都更好,且每个维度都显现在 dev-push.sh 必须围着写的那堆东西里:

  • 无需连接,因此无需配对、不会输错 PIN。resolve_board 为"机器人拒绝了 net.status——最常见是 PIN 错误"扛了整整一个失败分支;读广播不会被拒绝,那个分支就此消失。
  • 秒级而非几十秒级。dev-push.sh 之所以按机器人缓存地址,正是因为"BLE 发现要花十到二十秒";扫描停在第一个匹配广播上的话大约一秒,因为 duckctl 已经是轮询直到有东西出现,而不是睡满 SCAN_TIME。
  • 它不陈旧。btd 的 reconcile_advertisement 每 ADV_POLL(5 秒,见 btd/src/bluez.rs)重读一次 net.status,答案移动就重新广播——所以广播就是带五秒滞后的 net.status,远在新租约已经弄断 ssh 的时间窗之内。

有一个回退,且不可省略。与此 Mac 配对过的机器人经常不再向它广播该服务——duckctl 的扫描分层就是为此存在的——所以没看到广播时,ip 会连接并询问 net.status,这正是 dev-push.sh 今天的做法。先做廉价读取,再打电话。没有这个回退,该命令会恰好在最常用的那些笔记本上失败。

三态 Address 已经带着正确的失败文案,此处正是它兑现价值的地方:

  • Unassigned——机器人没有网络。修法是 duckctl wifi connect,而且必须走 BLE,因为 net.connect 在设计上被 WebRTC 拒绝("一台从未见过网络的机器人不能被那个网络配置")。
  • Unsaid——btd 开始广播地址之前的 release。回退照样回答;更新后它就变快了。

它的第三个调用者不是上面两者:docs/robot/install-dev.md 开头就索要"板子的 IP 地址",并注明该镜像上 mDNS 不可靠,却又不提供获取途径——这正是 ip 补上的空位。

2.2 duckctl open

解析地址,然后在浏览器里打开 http://<address>:8080/。--print 改为只打印 URL(给没有浏览器的机器或脚本用);--port 用于一台以非默认 --web-port 启动的机器人。

用命令而非文档化的 shell 替换,理由只有一个:端口默认值应该只存在于一个地方,而人永远不必去读它。open "http://$(duckctl ip):8080" 也能工作,但它是那种"写一次然后查一辈子"的行。源码中 duckctl/src/main.rs 的 Command::Open 明确注释了这一点:--port 的默认值 8080 与 mediad --web-port 相同,正是该命令存在的意义。

2.3 不做这些

  • duckctl url——那就是 open --print。第三个命令的全部内容就是一个端口号。
  • scan 上加 URL 列——scan 也会列出耳机,而机器人行已经带着地址。列表下加一条指向 open 的注释就够了。
  • 任何需要连接的东西——两个命令都是广播读取加回退。需要 bond 的命令是另一种命令,它应归入 wifi 和 update 之列,而不是"找机器人"之列。

2.4 这个闭环,以及一个后续

BLE 配置网络,然后交给你这个网络的 URL。两个传输不再是二选一的替代品,而是成为一个序列——值得一提,因为 mediad/src/route.rs 有意拒绝经 WebRTC 的 net.connect,这正是那个拒绝的另一半。

后续事项(独立于这四项之外):dev-push.sh 的 resolve_board 变成 client --name "$1" ip,删掉嵌入的 Python 和 wrong-PIN 分支。之所以独立,是因为它触碰发布路径,且应该等 ip 被手工用过几次之后再落。

duckctl 是手机 App 的临时替身,所以这里保持两个命令、不加新机制。能活过它的是那些更持久的半边:btd 广播地址,mediad 在已知端口服务。App 会用原生的方式做同样的两步。

3. 工具名源于一个它即将不再是唯一的传输

已决定并落地——duck-btctl 更名为 duckctl,并成为独立的 workspace 成员。其余部分是决策记录,保留是因为"备选方案"才是值得重读的部分。

open 会在一个 http URL 上启动浏览器,而这个工具的名字却写着 bt。值得揪出来,因为它指向比一个命令更大的东西。

open 本身并不错位。它的实质就是蓝牙:扫描广播以得知机器人在哪,末尾的 xdg-open 只有一行。duckctl open 读作"用无线电找到机器人,然后把它的控制台给我看",这正是它做的事——就像 wifi connect 是一个机制全靠 BLE 的 wifi 命令。

但名字仍然错了,原因是随 mediad 而非随 open 到来的。这台机器人即将有第二条传输,而两条传输在设计上触达不同的方法集:robot.move 被 BLE 拒绝、被 WebRTC 允许;net.connect 恰好相反。所以一个人两条都要,他真正想要的不是二选一的二进制——而是一个知道哪种传输能服务哪个调用、两者都不能时把话说出来的工具:

wifi connect 只能走蓝牙,而这台机器人没有在广播。

那个工具不能叫 btctl。而一个叫 btctl 的工具会悄悄教会每个人"蓝牙才是抵达机器人的方式",恰好在它不再是唯一方式的时刻。

3.1 是搬家,不是改名,并顺手修掉两个陈年疣

一个传输无关的客户端不能继续待在 btd/examples/duck-btctl.rs——它会同时需要 btleplug 和 WebSocket 客户端,而 btd 是 BLE 守护进程,它的 example 是另外一半的错误归宿。诚实的形态是独立的 workspace 成员,两个今天别扭的地方随之不再别扭:

  • 安装行。cargo install --path btd --example duck-btctl 别扭到 scripts/dev-push.sh 要为从未跑过它的 clone 扛一个回退——它改为 shell 出去执行 cargo run -q -p btd --example duck-btctl。cargo install --path duckctl 不需要回退。
  • "其实是产品的 example"。它之所以是 example,是为了让 btleplug 不上机器人——example 的 dev-dependencies 永远不会进入交付产物。独立 crate 免费保住这一点:因为它不是任何守护进程的依赖,同一个保证被直接陈述,而不是作为文件位置的副作用。

3.2 duckctl

它跟 robotctl 配对的方式与两者的实际用法一致:robotctl 在机器人上,duckctl 在它跟前。它保留了仓库本就以 duck- 命名的家族,而且短到不需要别名就能打。

duck 单独更好打,但已被占用(Cyberduck 有一个同名的 CLI,在 Mac 上很常见)。microduck 是仓库名,作为"每小时打二十次"的命令又太长了。

3.3 没有重写什么

约二十个文件里共有 196 处引用,多数是散文。带日期的记录保留旧名。docs/project/update-over-ble.md 和 docs/project/install-path-gap.md 描述的是一个瞬间——一次更新会话、四个安装路径 bug 及修复它们的过程——把工具名改写进一件发生在工具拥有该名前的事的叙述里,会让记录显得有点假。它们的链接被重新指向以保证仍能解析;散文不动。

docs/project/roadmap.md 在同目录但属例外,因为它不是某一瞬间的记录:它描述仓库的现状,细到 crate-by-crate 的布局表。一张指向不存在目录的布局表就是错的。

没有兼容垫片,没有别名。一个用户一台机器人:一个仍能工作的 duck-btctl 是第二个需要同步的名字,也是旧名在某人 shell 历史里再存活一年的理由。

3.4 什么让它不上板子

cargo board --bins——在 scripts/dev-push.sh 和 release workflow 中——为 aarch64 构建每个默认成员。作为 example 它被免费排除;作为 crate 它会被交叉编译,意味着在发布路径上为一个绝不该见到蓝牙的板子构建蓝牙栈。

解法是 workspace 根目录的 default-members:除了 duckctl 全部列入。一个列表,而不是在两个 --bins 调用点各自点名二进制并手工保持同步——那正是本仓库反复写下的失败模式。--workspace 不受影响,CI 的 lint 与测试照旧。

4. 页面变成控制台

已落地——mediad/webclient/index.html,仍是单文件、仍无构建步骤(该文件当前约 1754 行)。

被允许的子集很大,但页面几乎都够不着。按"人来做什么"重新组织:

区块内容
header机器人名、release、API 版本——来自 hello 与 system.info,通道一打开就自动发送,而非点击
video视频,外加来自 getStats() 的链路质量:bitrate、fps、loss、RTT。今天一个劣化的流只是画面变差、日志什么也不说
drive按键加屏上摇杆 → 固定频率的 robot.move;在视频上拖拽 → robot.look
posturerobot.enable、init、relax、stop、shutdown——后两个要确认
do / soundDo 与 Sound 枚举做成菜单
telemetry2 Hz 的 robot.subscribe 进实时面板:mode、health
console原始 JSON 框、日志、两个拒绝按钮——折叠着,因为它们证明的是路由表而非驾驶机器人

三个约束:

  • 停止按钮绝不能读起来像急停(e-stop)。mediad/src/route.rs 允许 robot.stop 的理由是:这条通道可靠,且 intents 停止到达时死曼(deadman)本就会刹停机器人——然后它明说 UI 不应暗示这是物理急停。用标签,而不是大红圆钮。页面里 robot.stop 按钮带 data-confirm("Zero the robot's intents?"),注释直言:确认正是让 stop 不像急停的东西——急停是拍下去的,而这是一个机器人可以拒绝的请求。
  • 版本差异是横幅,不是锁死的门。hello 上报 skew 并让页面继续工作——与 duck-btctl 在 #102 中定的规矩相同。这正是 mediad/src/web.rs 在服务页面时替换 API_VERSION token 的原因:页面里的字面量会是 API_VERSION 的第二个副本,在它被 bump 的那天错误,且错在"报告一致"的方向上。
  • 仍是单文件、仍无构建步骤。这个约束是客户端能跑起来的原因,它存活了下来。若它超过一个文件,就变成三个——index.html、app.js、app.css、三个 include_str!——仍无构建步骤、仍无 npm。

从页面驾驶机器人,也是 remote-webrtc.md 两个从未被验证过的论断第一次被真实测试:会话掉线时死曼刹停机器人(§6),以及 control 上的有序性足以让 intents.rs 保持诚实(§6 亦然)。两者都便宜到可以相信,但错了就会很贵。

5. 会话开始前,机器人先自报家门

已落地——mediad/src/producer.rs。

webrtcsink 接收一个 meta 结构,信令服务器把它随 list 交给每个 peer——页面早已记录它,但它一直是空的。现在从 configd 填进去:name、serial、release、API_VERSION(外加 simulated)。

从源码看,Producer 的字段设计处处带着理由:

  • name——让页面用机器人名而非 id 作标题,也让发现两个 producer 的客户端能区分彼此。这是该字段今天的全部价值。
  • serial——持久句柄。名字会改、peer id 每次会话都变,serial 比两者都活得久,这正是 App 按键索要的东西(见 docs/design/app-path-design.md §8.6)。
  • release——当前跑的是什么。客户端行为诡异时,可以区分"对这台机器人行为怪异"与"落后一个 release",而无需开会话去问。
  • api_version——与会话 hello 报告同一份 skew,但早一个往返。客户端可以在开始协商前就挂上横幅。
  • simulated——这只鸭子是否在 MuJoCo 孪生里。configd 拥有这个事实,mediad 只是携带它;--sim-camera 是兜底(一台帧来自模拟器的机器人不可能真实)。

名字来自 configd,而它的缺席不是失败。system.info 拥有名字与 serial,mediad 本就有通向 configd 的连接。但 mediad 与 configd 并行启动而非在其后,unit 文件说得明白——After= 而不是 Requires=,服务宕机绝不能阻止本服务启动。所以 learn() 询问、等 ASK_TIMEOUT(3 秒,一次 unix socket 往返绰绰有余),然后无论结果如何带着已知信息继续:没有名字的 producer 是一台能推流的机器人,而一台因学不到自己名字就不推流的机器人会是糟糕得多的交易。只在启动时问一次而非每 peer 一次;改名在下次重启生效(configd 对 btd 的广播也这样),想要实时答案的 peer 可以经自己的控制通道调用 system.info。

fields() 中缺席的字段保持缺席而非空字符串——客户端读到 serial: "" 必须知道它意为"无",而缺失的键不需要任何约定;键是 snake_case,与这条线上其他字段一致。这一点有小、但每处都测过的测试钉住。

小,却付三次账:页面在开会话前就能给机器人命名;发现两个 producer 的客户端能说出谁是谁;§7 的 rendezvous 服务恰好需要这个字段来路由。这是本波最便宜的一项。

6. 它开启的、但还不是的东西:get_frame

remote-webrtc.md §11 推迟了"给服务端程序用的 WebSocket 表面——同样的 JSON-RPC,无媒体栈,get_frame 返回 JPEG",并称一旦 §5 的路由存在它只是几十行。当 mediad 里有了 axum 服务器,它就是一台已在运行的服务上的一个路由,帧也已在:main.rs 中 _frames 是 tee 上未经编码的 NV12 抽头,至今没人读它。

值得一提的是:从当前源码看,mediad/src/web.rs 已经比设计文档多走了一步——router() 里除了 / 还挂了一条 /frame 路由,返回经 snapshot::png 编码的即时相机快照(带 4 个并发槽位的信号量、no-store 缓存头,测试覆盖了"竖起的 PNG"与"相机不可用时的 503")。这印证了文档的论断:一旦 axum 已在进程里,帧也在那里,加一个路由的成本接近零。

文档特意给这个形状命名,但不是在这里提议:它是第二条传输,第一条应该先做好。

7. 顺序,以及剩下什么

四项变更,每一项都独立成立、分别落地,顺序如下:

  1. 服务页面,且信令 URL 在服务时填入。删掉了 python 指令和警告块。小而基础,其余一切在它之上都更顺。API 版本以同样方式替换——为 §4 的横幅服务;页面里的字面量会是 API_VERSION 的第二个副本,在它被 bump 的那天错误,且错在报告一致的方向上。
  2. Producer meta。更小,且独立。
  3. duckctl 增加 ip 与 open。纯客户端,没碰任何守护进程。
  4. 控制台。最大的一项,最后做,落在一个已经可达的页面上。

没做、且有意独立的:scripts/dev-push.sh 的 resolve_board 仍用 duckctl wifi status 加六行嵌入 Python 手搓这件事(§2.4)。它将变成 duckctl ip,删掉 Python 和 wrong-PIN 分支——独立,因为它触碰发布路径,且应等 ip 被手工用过几次之后再落。

8. 明确不做的

  • 控制台加门禁。remote-webrtc.md §4 拥有那个决定,此处不改变其条款。一个把页面关掉的 --no-web 旗标值得为那节点名的家用场景保留——一个旗标,不是一个机制。
  • JS 框架、bundler 或 gstwebrtc-api。页面手写协议,因为需要 npm 的客户端是没人会跑的客户端。在四倍体量时依然成立。
  • 用 TLS 服务。§1.3 说了何时,以及为何现在不做。
  • 让广播学习端口。广播里就四字节 IPv4;非默认 --web-port 的机器人用 duckctl open --port 处理,而不是改线上格式。

附:控制面——route.rs 到底允许 WebRTC peer 做什么

原文档处处提及"exercise the control surface route.rs actually permits",这一节把那张表展开,因为它是"页面变成控制台"的底层依据。

mediad/src/route.rs 是 btd::route 的姊妹,刻意同构:哪个服务应答一个调用、应答会占用连接多久只存在一次(proto::Call::destination),本文件只回答"这个传输上的 peer 能不能问"。match 是穷尽的,这正是每个传输各有一张表的要点:给 proto::Call 加一个 variant,会让这里的构建和 btd 一样失败——一个新方法不可能因为"有人忘了这个文件"而到达远端 peer。共享表加 _ 通配符会同时成为两个传输的漏洞。

WebRTC 的子集比 BLE 宽,因为 BLE 窄化的两个理由在这里都不成立:无线电慢(20 字节通知预算),且几米内谁都能说。数据通道既不慢也不限房间,所以 BLE 以容量为由拒绝的调用恰好是本传输存在的原因:intents、遥测、pad tap、深度流。留在外面的分三类,理由各不相同:

  1. 它授权的是另一条传输——配对 PIN。能重写它的 peer 可以把手机锁在 BLE 之外,而 BLE 是恢复路径。PIN 不上任何网络传输,与它本身对 BLE 不可路由同一条规则。
  2. 它会断掉被询问所经的会话——更新类变更(apply/rollback/select)与 wifi 重配置。前者的修法记在 remote-webrtc.md §8(能重连重订阅的客户端,以及 RobotRemoteSessionActive 学会区分旁观会话与发起会话);后者无可推迟之物——配置网络正是 BLE 存在的意义。
  3. 那不是客户端的问题——updaterd 对 robotd 的内部查询(robot.safeToRestart、robot.modelApi、robot.remoteSessionActive)。

一个值得单列的类别进来了:account.*——把机器人绑定到 Hugging Face 账户的调用。它们是本传输承载的第一批有持久副作用的变更,控制台正是给机器人签名的地方;其论证见 docs/design/remote-access-design.md §2.6,route.rs 里那段长注释是它的摘要:机器人已属他人时 account.login 会拒绝(除非 force)、account.status 从任何传输都可读、account.logout 或撤销 HF 授权可随时吊销。

路由表里还隐藏着一个跨文件的一致性约束:mediad/src/route.rs 的测试 only_these_mutating_calls_are_reachable_over_webrtc 钉死了一张可变更调用名单,因为 Call::is_mutating 也是 updaterd 授权判定的依据,而 deploy/updater.toml 把 mediad 列进 allow_users——任何同时"可变 + 被允许"的调用,都是 LAN peer 能让 updaterd 执行的调用。这张名单曾经揪出过真 bug:policy.install 与 policy.fetch 被路由进来,但 mediad 不在 allow_users 里,控制台能展示 Hub 浏览器而其安装按钮永远失败——为 account.login 加 mediad 顺手修好了它们。

最后,route.rs 也承载了文档 §4 与 §0 之间的一条设计张力:robot.stop 在这里被允许而在 BLE 被拒绝,区别在于诚实而非权威——可靠有序的 control 通道让 stop 按钮名副其实(死曼本就兜底),但它仍然不是物理急停,UI 不得暗示它是。控制台页面用带确认的普通按钮和一段说明文字兑现了这一点,正是本节与 §4 交汇之处。

结语:从"测试页"到"驾驶台"

四项变更的价值不在任何一项单独成立,而在它们合成的那条路径:BLE 广播告诉你机器人在哪(duckctl ip),mediad 在 8080 服务页面(duckctl open),页面经 8443 信令拿到 control 通道,通道接通 route.rs 允许的整个控制面。btd 广播地址与 mediad 已知端口服务是两个会活得比 duckctl 更久的半边——手机 App 终将用原生方式做同样的两步,而届时这篇设计里写下的每一个"为什么",都仍然适用。

  • 机器人
  • 嵌入式
  • 强化学习
  • 人工智能
  • 智能硬件
  • 计算机视觉
  • 音视频

【免费下载链接】microduck

A Tiny biped duck robot 🦆

项目地址: https://gitcode.com/gh_mirrors/mi/microduck
点击查看 免费下载
Logo

火山引擎视频云技术社区,是面向 AI 音视频开发者的技术交流平台。这里汇聚源自抖音、豆包等亿级 DAU 产品的 RTC、直播、点播、AI 媒体处理、音视频互动技术,提供接入指南、最佳实践、性能调优、场景案例、Demo 代码、开源项目、白皮书和 API 文档。社区汇聚官方工程师与一线开发者,为 AI 视频通话、数字人、AI 视频处理等应用的开发与落地提供技术支持。

更多推荐