从刷机开启 SSH,到自定义 Agent 与 AirPlay:小爱音箱 OH2P 完整记录

小米手机已经可以正常投放到小爱音箱,但 iPhone/Mac 需要 AirPlay 接收端。这篇文章不只记录最终效果,也把使用哪个项目获取刷机包、如何制作补丁固件、如何备份、如何刷入,以及刷完后如何登录 SSH 和部署 Agent/AirPlay 全部写清楚。

本文实际使用的设备是 Xiaomi 智能音箱 Pro(OH2P),固件为 1.62.2。不同型号、不同固件不要直接照搬。

一、这次到底用了哪个项目

这次不是用群晖上的 AirConnect 获取刷机包,也不是下载一个来路不明的 rootfs,而是使用:

  • 主项目:stevenjoezhang/xiaoai-agent
  • 固件制作目录:deploy/client-patch/
  • macOS 刷机工具:deploy/flash-tool/
  • OH2P 构建记录:deploy/OH2P_1.62.2_BUILD_NOTES.md

xiaoai-agent 项目本身包含端侧 Agent、带 SSH 和启动钩子的补丁固件制作流程,以及 macOS 刷机工具。固件制作脚本会通过小米 OTA 接口获取与设备匹配的原始固件,再自动生成补丁版本;不是直接使用上游预构建的 patched 固件。

最终会得到两个关键文件:

root.squashfs          原始 rootfs,留作恢复和对照
root-patched.squashfs 补丁 rootfs,用于刷入

真正刷入的是 root-patched.squashfs,root.squashfs 必须留好。

二、为什么群晖的 DLNA 没有直接解决问题

最开始的现象是:

  • 小米手机可以正常投放到小爱音箱;
  • 群晖上安装了 AirConnect;
  • lsof -i:49154 能看到 airupnp 监听;
  • 但 iPhone/Mac 找不到小爱音箱。

这并不矛盾。小米手机投放可能走小米私有投放或 MiNA 链路,不代表 OH2P 对局域网公开了标准 DLNA MediaRenderer。

AirConnect 的主要方向是:

AirPlay 发送端 → AirConnect → 标准 DLNA/UPnP 播放器

而这台 OH2P 没有稳定响应标准 DLNA 发现请求,所以群晖上的 49154 端口在监听,也不能自动把小爱音箱变成 AirPlay 接收端。

最终采用的方向是:

iPhone / Mac
    ↓ AirPlay
小爱音箱本机上的 AirPlay 接收服务
    ↓ ALSA
OH2P 扬声器

三、获取 OTA 并制作带 SSH 的补丁固件

1. 克隆项目

git clone https://github.com/stevenjoezhang/xiaoai-agent.git
cd xiaoai-agent/deploy/client-patch

这一步进入的是 deploy/client-patch,不是 xiaoai-agent/ Rust 程序目录。

2. 准备 .env

复制项目里的 .env.example 为 .env,填写自己的设备信息:

MI_USER=<小米数字账号ID>
MI_PASS=<小米账号密码>
MI_DID=<小爱音箱名称或DID>
SSH_PASSWORD=<自己设置的SSH密码>

如果使用现成的 Xiaomi passToken,也可以按项目说明配置 MI_TOKEN。这里有几个容易踩坑的地方:

  • MI_USER 使用小米数字账号 ID,不要优先填手机号;
  • MI_DID 必须对应这台音箱;
  • .env 是临时敏感文件,不要提交 Git;
  • MI_PASS、MI_TOKEN、SSH_PASSWORD 不要贴到群里、文章或截图中;
  • 本文不放真实账号、密码和 Token。

实际测试中,手机号作为 MI_USER 可能导致 MiNA 返回 401,所以建议使用小米数字账号 ID。

3. 推荐用 Docker 制作

项目推荐 Docker,尤其是 macOS Apple Silicon,因为固件构建环境需要兼容 x86_64/amd64:

cd xiaoai-agent/deploy/client-patch

docker build --platform linux/amd64 \
  -t xiaoai-agent-client-patch .

docker run -it --rm \
  --platform linux/amd64 \
  --env-file "$(pwd)/.env" \
  -v "$(pwd)/assets:/app/assets" \
  -v "$(pwd)/patches:/app/patches" \
  xiaoai-agent-client-patch

如果是 Apple Silicon,需要在 Docker Desktop 中允许 amd64 模拟,并启用 Apple Virtualization framework/相关 Rosetta 选项。

构建脚本会自动完成:

登录小米接口
    ↓
获取匹配当前设备的 OTA
    ↓
保存原始 OTA 和原始 root.squashfs
    ↓
把 Dropbear SSH、/data/init.sh 等补丁写入 rootfs
    ↓
生成 root-patched.squashfs

产物通常位于:

assets/<OTA目录>/root.squashfs
assets/<OTA目录>/root-patched.squashfs

4. 也可以本地构建

macOS/Linux 本地构建需要 Python 3、Node.js 22 和 squashfs-tools。macOS 可以先安装:

brew install squashfs

然后执行:

cd xiaoai-agent/deploy/client-patch
npm install
npm run build

本地构建完成后,原始固件和补丁固件同样会保存在 assets 目录。

5. 刷机包怎么备份

这里要分清两类备份。

**第一类:刷机文件备份。**构建完成后,先把整个 assets 目录复制到外部磁盘或单独目录:

cd xiaoai-agent/deploy/client-patch
cp -a assets "$HOME/Desktop/OH2P-assets-backup-$(date +%Y%m%d-%H%M%S)"

至少要确保下面两个文件都保留:

root.squashfs
root-patched.squashfs

不要只保留补丁版,原始 root.squashfs 是恢复和对照依据。

**第二类:音箱运行数据备份。**刷完 SSH 后,在修改 /data 前把启动脚本和自定义目录导出到 Mac:

mkdir -p "$HOME/Desktop/OH2P-device-backup"
ssh root@<speaker-ip> \
  'tar czf - -C /data init.sh open-xiaoai 2>/dev/null' \
  > "$HOME/Desktop/OH2P-device-backup/data-init-open-xiaoai.tgz"

在音箱上改任何文件前,再单独做一份时间戳备份:

cp -p /data/init.sh \
  /data/init.sh.bak.$(date +%Y%m%d-%H%M%S)
cp -p /data/open-xiaoai/xiaoai-airplay \
  /data/open-xiaoai/xiaoai-airplay.bak.$(date +%Y%m%d-%H%M%S) 2>/dev/null || true

另外,OH2P 有启动分区回退路径:如果补丁系统启动异常,可以把启动分区切回 boot1 恢复原系统。不要把 boot1 当成不用备份的理由,仍然要保存本地 root.squashfs 和设备 /data 备份。

四、怎么把补丁固件刷进音箱

1. 准备硬件

需要:

  • OH2P 音箱;
  • 能传输数据的 USB Type-C 数据线,不能只是充电线;
  • macOS 电脑;
  • 已生成的 root-patched.squashfs。

OH2P 可以通过音箱底部的 Type-C 接口连接电脑。

2. macOS 安装刷机依赖

项目自带的 deploy/flash-tool 是 macOS 专用刷机工具,基于 Amlogic 工具链。先安装依赖:

brew install libusb-compat

然后进入刷机工具目录:

cd xiaoai-agent/deploy/flash-tool
chmod +x ./flash
./flash help

3. 连接并刷写

把音箱通过 USB 数据线连接到 Mac,然后按下面顺序执行:

# 第 1 步:等待设备连接
./flash connect

# 第 2 步:设置启动延时
./flash delay 15

# 第 3 步:选择启动分区
./flash switch boot0

# 第 4 步:刷入补丁 rootfs
./flash system system0 \
  /完整路径/root-patched.squashfs

执行 ./flash connect 后,通常需要拔掉音箱电源,再重新插电,让设备重新进入可连接状态。工具识别到设备后,再继续后面的命令。

如果提示刷写错误,不要马上断定设备变砖。先让音箱重新进入刷机模式,再重复连接和写入流程;本次实际刷写中就遇到过 USB I/O 错误,重新进入刷机模式后完成了写入。

刷写成功后:

  1. 拔掉 USB 数据线和音箱电源;
  2. 等待几秒后重新插电;
  3. 等音箱启动并接入局域网;
  4. 从路由器或小米 App 找到新的局域网 IP。

如果启动后没有反应,先不要连续反复刷写,可以尝试切回 boot1 恢复原系统,再重新核对型号、固件版本和补丁文件。

4. Windows 刷机替代方式

如果使用 Windows,可以使用 Amlogic Flash Tool,在 bin 目录中执行等价操作:

update.exe identify
update.exe bulkcmd "setenv bootdelay 15"
update.exe bulkcmd "setenv boot_part boot0"
update.exe bulkcmd "saveenv"
update.exe partition system0 root-patched.squashfs

Windows 不要在 WSL 中执行这套 USB/UBoot 通信流程,使用原生 CMD、PowerShell 或 Git Bash 更稳妥。

五、刷完后怎么打开和使用 SSH

SSH 不是在小爱 App 里点开的,而是补丁 rootfs 中的 Dropbear 服务随系统启动。

先在 Mac 上测试 22 端口:

nc -v <speaker-ip> 22

看到下面的 banner,说明 SSH 已经启动:

SSH-2.0-dropbear

使用项目 .env 中设置的 SSH_PASSWORD 登录:

ssh -4 -o IPQoS=none \
  -o HostKeyAlgorithms=ssh-rsa \
  -o PubkeyAcceptedAlgorithms=ssh-rsa \
  -o KexAlgorithms=curve25519-sha256@libssh.org,diffie-hellman-group14-sha1,diffie-hellman-group1-sha1 \
  root@<speaker-ip>

登录后先做只读检查:

uname -a
cat /proc/cpuinfo
ls -l /data/init.sh
ps

如果新版本 OpenSSH 报算法不兼容,使用上面的兼容参数;如果 22 端口没有响应,先确认音箱和 Mac 在同一局域网,再检查是否刷错型号或系统是否尚未启动完成。

六、SSH 登录后如何安装自定义 Agent

项目目录中的 xiaoai-agent/ 是音箱端 Rust Agent,和 deploy/client-patch/ 是两回事。

1. 为 OH2P 编译

OH2P 是 ARMv7 Linux 用户态,不能直接把 Mac 的 x86_64 二进制复制过去。准备工具链:

rustup target add armv7-unknown-linux-gnueabihf
cargo install cargo-zigbuild
brew install zig

构建时使用兼容 glibc 2.25 的目标:

cd xiaoai-agent
cargo zigbuild --release \
  --target armv7-unknown-linux-gnueabihf.2.25

2. 上传到音箱

Dropbear 环境不一定提供 sftp-server,所以 OpenSSH 的 scp 要加 -O,强制使用旧 SCP 协议:

ssh root@<speaker-ip> 'mkdir -p /data/open-xiaoai'

scp -O target/armv7-unknown-linux-gnueabihf/release/xiaoai-agent \
  root@<speaker-ip>:/data/open-xiaoai/xiaoai-agent

scp -O agent.yaml \
  root@<speaker-ip>:/data/open-xiaoai/agent.yaml

scp -O start-agent.sh \
  root@<speaker-ip>:/data/open-xiaoai/start-agent.sh

ssh root@<speaker-ip> \
  'chmod +x /data/open-xiaoai/xiaoai-agent /data/open-xiaoai/start-agent.sh'

配置中的 ASR、LLM、MCP webhook 都是敏感信息,只在音箱本地保存。

3. 先手动启动,再考虑开机启动

RUST_LOG=debug /data/open-xiaoai/start-agent.sh start

确认唤醒、录音、ASR、模型回复和 TTS 都正常后,再配置自启。停用 Agent、恢复原生语音:

/data/open-xiaoai/start-agent.sh stop

七、AirPlay 接收的实现

AirPlay 使用 stevenjoezhang/xiaoai-agent 中的独立实现思路,底层使用 Rust shairplay:

iPhone / Mac
    ↓ AirPlay / RAOP
shairplay RaopServer
    ↓ PCM
AudioHandler
    ↓ f32 转 S16_LE
aplay -D default
    ↓ ALSA
OH2P 扬声器

最终配置为:

name: XiaoAI AirPlay
device: default
mode: ap2

这里必须是 device: default。小爱原生的 default ALSA 链路还包含:

default → vis → ledd → tocopy / Playback / dmixer → hw:0,2

如果改成 Playback,可能有声音但绕过 vis,音乐灯条就不会呼吸。

八、AirPlay 程序怎么部署

由于 Dropbear 设备不一定有 SFTP,本次用 Mac 临时 HTTP 服务传文件。

Mac 上:

cd outputs/xiaoai-airplay-ap2-duck-led
python3 -m http.server 8765 --bind 0.0.0.0

音箱 SSH 上:

cd /data/open-xiaoai
cp -p xiaoai-airplay \
  "xiaoai-airplay.bak-$(date +%Y%m%d-%H%M%S)"
./start-airplay.sh stop
curl -f -sS -o xiaoai-airplay.new \
  http://<mac-ip>:8765/xiaoai-airplay
sha256sum xiaoai-airplay.new
chmod +x xiaoai-airplay.new
mv xiaoai-airplay.new xiaoai-airplay
./start-airplay.sh start
./start-airplay.sh status

先核对 SHA-256,再替换正式文件。不要直接执行来源不明的二进制。

九、语音抢音、灯条和开机自启

AirPlay 播放时喊“小爱同学”,程序只在 AirPlay PCM 输出层做 ducking:

正常播放:PCM × 1.0
语音活动:PCM × 0.2
语音结束:恢复 PCM × 1.0

监听的事件包括 SpeechWakeup、DuplexRecognizeStarted、Dialog.Finish 和 ExitContinuousDialog。音乐音量降低不会改变系统总音量,也不会改变原生 TTS 音量。

灯条通过 default → vis → ledd 链路随音频呼吸。语音结束后,程序会重新确认音乐灯状态,避免原生语音灯退出时把呼吸灯一起关掉。

开机自启不要覆盖原来的 /data/init.sh。先备份:

cp -p /data/init.sh \
  /data/init.sh.bak.$(date +%Y%m%d-%H%M%S)

然后只增加一条独立后台启动命令:

[ -x /data/open-xiaoai/start-airplay.sh ] && \
  sh /data/open-xiaoai/start-airplay.sh start > /dev/null 2>&1 &

日常管理:

/data/open-xiaoai/start-airplay.sh start
/data/open-xiaoai/start-airplay.sh stop
/data/open-xiaoai/start-airplay.sh status
tail -80 /data/open-xiaoai/airplay.log

十、完整验收清单

服务状态

/data/open-xiaoai/start-airplay.sh status
ps w | grep '[x]iaoai-airplay'
netstat -lntup | grep 5000

Mac 发现 AirPlay

dns-sd -B _raop._tcp local

真机流程

  1. iPhone/Mac 播放音乐;
  2. 选择 XiaoAI AirPlay;
  3. 确认音箱出声;
  4. 确认灯条呼吸;
  5. 播放过程中喊“小爱同学,几点了”;
  6. 确认 AirPlay 自动压低;
  7. 语音结束后确认音量和灯效恢复;
  8. 重启音箱,确认 AirPlay 自启且原生小爱仍正常。

十一、最终文件和回滚关系

项目本地:
xiaoai-agent/deploy/client-patch/assets/<版本>/root.squashfs
xiaoai-agent/deploy/client-patch/assets/<版本>/root-patched.squashfs
xiaoai-agent/deploy/flash-tool/flash

音箱持久化目录:
/data/open-xiaoai/xiaoai-agent
/data/open-xiaoai/xiaoai-airplay
/data/open-xiaoai/airplay.yaml
/data/open-xiaoai/start-airplay.sh
/data/open-xiaoai/airplay-pairing.json
/data/init.sh

回滚时先停 AirPlay,再恢复备份二进制和配置;如果是补丁系统无法启动,则把启动分区切回 boot1,恢复原系统启动。刷机前后的原始 root.squashfs、补丁 root-patched.squashfs 和 /data 备份都不要删除。

十二、总结

这次真正的链路是:

stevenjoezhang/xiaoai-agent
    ↓
deploy/client-patch 获取匹配 OTA 并制作 root-patched.squashfs
    ↓
deploy/flash-tool 刷入 OH2P system0
    ↓
Dropbear SSH + /data/init.sh
    ↓
SSH 登录并部署自定义 Agent
    ↓
加入 shairplay AirPlay 接收
    ↓
ALSA default 保留原生 vis/ledd 灯效
    ↓
语音活动时自动压低 AirPlay
    ↓
开机自启并完成真机验收

小米手机原来的投放方式仍然可以使用;iPhone/Mac 通过 AirPlay 也能投到音箱;AirPlay 播放时有音乐呼吸灯;喊小爱时音乐自动压低,语音结束后恢复。

免责声明

本文是个人设备上的技术折腾记录。刷机、修改 rootfs、禁用自动更新都可能导致变砖、失去保修、服务异常或数据丢失。请只在自己拥有或获得授权的设备上操作,先确认型号和固件版本,完整保留原始 OTA、root.squashfs、补丁 rootfs 和设备数据备份。不要公开小米账号、密码、Token、SSH 密码或 MCP webhook。