# ESP32-S3 Device Lab

M5Stack StopWatch **0.6.0 — Wearable Agent Console**：随身查看 Agent 状态、说话、处理 Approval 和阅读最终结果。产品默认启动到 Watch；Agent 是核心工作页，完整服务器指标作为次级 Monitor，Help 独立可见。

短 A 立即前进，短 B 立即后退：`Watch → Agent → Monitor/{Overview, CPU, Memory, Disk, Network} → Help`。没有 launcher、双击窗口或单击延迟。长 A 保留 Voice 输入，长 B 打开 Settings；Approval 的 A APPROVE / B REJECT 和 Voice 的 A SEND / B CANCEL 优先于 Shell。

Settings → Device：Brightness 10..100% / Vibration；Display：Home Screen / Compact、Normal、Large / Dark、Dim / Agent Clock；System：只读电压、Wi-Fi、Gateway、固件。只有 B Apply 才保存双槽 NVS，失败显示 APPLIED / NOT SAVED，本地设置离线可用。Gateway `hello_ack` 下发 UTC 与时区偏移并写入 RX8130；断网后继续走时，RTC 无效且网络未同步时显示 `--:--`。

**本轮不改 Speech Output。** Volume 80 仅保留在偏好模型中，无运行时音量修改。Battery percentage、Touch、Project/Session Picker、Workspace/Session 创建和 Voice replay 延后。Control Layer / CONTROL_VERSION 保持 **0.5.0**。真机门槛与状态见 [0.6a 验收记录](docs/wearable-shell-0.6a.md)，host PASS 不代表 daily-use PASS。

0.4.1 更新 Gateway STT，现有 0.4.0 firmware 无需刷写。真实中文、英文、自然中英混合 STT 已通过。精确机器 identifier 为 best effort，发送前确认；model/session 等精确选择使用 Device Settings。该限制已被接受，不阻塞 STT production readiness。`GATEWAY_STT_INITIAL_PROMPT` 保持未启用。详见 [0.4.1 验收记录](docs/stt-0.4.1-validation.md)。

## Architecture

```text
Lighthouse (Gateway + system collector + optional Pi Extension)
  -> existing Cloudflare Tunnel / wss://gw.pangtiao.men
  -> M5Stack StopWatch (CPU / MEM / DISK / NET / Pi state)

Machine authentication:
  /device/ws              -> WSS + Gateway device token
  /integrations/pi/events -> Gateway Pi token
  /health                 -> unauthenticated health check

Human administration:
  /admin + management APIs/docs -> Cloudflare Access
  -> same allowed identity and IdP model as pi.pangtiao.men

Cloud build (ESP-IDF 5.5.4)
  -> dist/m5stack-stopwatch/merged-flash.bin
  -> scp to Intel Mac
  -> esptool + USB
```

当前完整实现 `m5stack-stopwatch`。`boards/waveshare_1_75c/` 只是明确的后续接入边界，不能构建，也不声称已经支持。

## 云端首次设置

要求：Git、Python 3、可访问 GitHub。脚本会把 ESP-IDF 和工具链安装到仓库内的 `.cache/`，不会要求全局安装，也不会改动服务器现有 Docker 数据。

```bash
git clone <your-repo-url> esp32-dev
cd esp32-dev
./scripts/setup-cloud.sh
```

工程固定 ESP-IDF `v5.5.4`，与 M5Stack 官方 StopWatch ESP-IDF 示例保持一致；M5GFX、M5PM1、M5IOE1 由 ESP-IDF Component Manager 以固定版本/提交获取。

## 配置 Wi-Fi 和网关

真实密码只放在 Git 忽略的本地文件中：

```bash
cp config/sdkconfig.defaults.example config/sdkconfig.defaults.local
```

编辑 `config/sdkconfig.defaults.local`：

```text
CONFIG_DEVICE_LAB_WIFI_SSID="your-2.4GHz-wifi"
CONFIG_DEVICE_LAB_WIFI_PASSWORD="replace-me"
CONFIG_DEVICE_LAB_CLOUD_WS_URL="wss://gw.pangtiao.men/device/ws"
CONFIG_DEVICE_LAB_DEVICE_TOKEN="provisioned-secret"
```

不要手工复制 token。首次配置或轮换时在 Lighthouse 运行：

```bash
./scripts/provision-device.py --device-id esp32s3-28848543a200
```

脚本生成高熵 token，同步写入 Git 忽略的 `config/sdkconfig.defaults.local` 和权限为 `0600` 的 `~/.config/esp32-device-gateway/device.json`，且不会打印 token。省略 `--device-id` 时，首个持有 token 的设备会完成一次性绑定。

ESP32 只能连接可用的 2.4 GHz 网络。设备必须能访问 Gateway；不要把 `127.0.0.1` 写进固件。公网连接使用公共 CA 的 `wss://`。token 只在 TLS WebSocket 的 `hello` 中发送，日志不会记录它。

## 构建固件

```bash
./scripts/build.sh m5stack-stopwatch
```

成功后生成：

```text
dist/m5stack-stopwatch/
  bootloader.bin
  firmware.bin
  partition-table.bin
  merged-flash.bin
  flash.json
  VERSION
```

`merged-flash.bin` 已包含正确偏移，可以直接从 `0x0` 写入。`flash.json` 同时保留芯片、Flash 大小、各镜像偏移和版本信息。

## 启动云端网关

### 本地临时开发

首次安装：

```bash
./scripts/setup-server.sh
```

启动（默认仅监听 loopback）：

```bash
./scripts/run-server.sh
```

另一个终端检查：

```bash
curl http://127.0.0.1:8000/health
curl http://127.0.0.1:8000/devices
```

### Lighthouse production deployment

生产环境使用仓库中的 `deploy/systemd/device-gateway.service` 作为用户级
systemd 服务，通过 Cloudflare Tunnel 暴露；不要直接监听公网地址。Lighthouse
上的 `127.0.0.1:8000` 已由 FDE Workbench API 使用，`127.0.0.1:18080`
也已有服务，因此 production Gateway 固定使用：

```text
device-gateway.service
http://127.0.0.1:18081
https://gw.pangtiao.men
wss://gw.pangtiao.men/device/ws
```

端口、采样周期和状态文件由仓库外的 `~/.config/esp32-device-gateway/gateway.env` 配置。安装或更新服务：

```bash
install -m 600 deploy/systemd/gateway.env.example ~/.config/esp32-device-gateway/gateway.env
install -m 644 deploy/systemd/device-gateway.service ~/.config/systemd/user/device-gateway.service
systemctl --user daemon-reload
systemctl --user enable --now device-gateway.service
```

production 检查与日志：

```bash
systemctl --user status device-gateway.service
journalctl --user -u device-gateway.service -f
curl http://127.0.0.1:18081/health
curl https://gw.pangtiao.men/health
```

管理页为 <https://gw.pangtiao.men/admin>。它显示设备在线状态、固件版本、最新系统 snapshot、Pi sessions，并允许为设备选择 `Server Overview`、`PI · <session>` 或 `CODEX · <session>`。`/admin`、`/api/*`、`/server*`、`/devices*` 及 API 文档由 Cloudflare Access 保护，复用 `pi.pangtiao.men` 的身份提供方式和允许身份；Gateway 不再实现另一套管理用户名、密码或 session。在线状态仍在内存中；历史 session 从各 Agent 正式 SDK/API 发现，Gateway 重启后可重新查询。Project Registry 与设备 target 持久化在仓库外的小型 JSON 文件。

现有 `cloudflared-dsh.service` 的 `gw.pangtiao.men -> 127.0.0.1:18081` ingress 被复用，不需要第二套 Tunnel。Access 只覆盖人类管理路径：`/device/ws` 继续由 Gateway `device_token` 认证，`/integrations/pi/events` 继续由独立 `pi_token` 认证，`/health` 保持无需浏览器登录的基础探针。这三个机器路径不得依赖 Access Cookie 或交互式登录。

Access 边界回归应从未登录的客户端执行。`/admin`、`/api/status`、`/server`、`/devices`、`/docs`、`/redoc` 和 `/openapi.json` 应返回 Access 登录跳转，`/health` 应直接返回 `200`。向 `/integrations/pi/events` 发送结构合法但不带 token 的事件应由 Gateway 返回 `401`，而不是 Access 登录页；真实设备 WSS smoke 应继续依次收到 `hello_ack`、`server_snapshot` 和 `target_state`。

运行全部网关测试：

```bash
./scripts/test-server.sh
```

这会先运行 Pi Extension 批处理/脱敏测试和 Gateway 的 collector、协议、认证、Pi lifecycle/activity 单元测试，再在随机空闲端口启动真实 Uvicorn，验证 Admin HTTP、设备认证、双向 WebSocket、server snapshot 和模拟 Pi target 状态。

## Unified Approval Control 0.4.0

Pi 的 mutation `tool_call` 和 Codex 的 command/file-change approval 共用内存 Registry。
设备不论当前页面/target 都弹出临时审批窗口：松开 A 批准、松开 B 拒绝，完成后恢复原页面。
无审批时 A/B 继续翻页。Admin 的 **Pending Approvals** 提供同一 Registry 的备用决定入口。
所有已认证在线设备按 FIFO 展示最早的一条，第一个合法决定生效；没有人工决定就一直等待：**NO DECISION = KEEP WAITING**。没有任何默认 timer 自动 approve、reject 或 expire；运行时/会话/连接失效只会 CANCELLED。

Pi Extension 的新审批通道使用同用户 `0700` runtime 目录中的 `0600 approvals.sock`，
不轮询 HTTP；Gateway 不可用时 mutation 被阻止。已有交互式 Pi 需要 `/reload` 加载更新后的 extension。
这只是人工工作流 gate，Pi sandbox 和 OS 权限仍独立。只读工具不弹窗，未知工具及复杂 shell 默认要求审批。

```bash
./scripts/test-server.sh
./scripts/build.sh m5stack-stopwatch
# 显式真实模型 smoke：独立临时项目、session 和 Gateway，不动生产服务
server/.venv/bin/python scripts/smoke-approvals.py
```

协议、连接边界与部署说明见 [Approval control](docs/approval-design.md)；真实 smoke 与构建证据见 [0.3.0 验收记录](docs/approval-0.3.0-validation.md)。

## Agent Control Layer 0.2.2

Admin 统一展示 Project → Pi / Codex → 历史与 live sessions。选中 dormant session 后输入文本，点击 `Resume + Send`；HTTP 202 只确认恢复及提交，后续状态/文本继续刷新。聚焦或提交中的设备 target 下拉框不会被轮询重建。

在 Admin 显式添加项目；也可通过受 Access 保护的 `POST /api/projects` 添加 `{id, name, path}`。路径必须是存在的绝对目录，ID 保持稳定，不自动扫描 home。默认配置文件为 `~/.config/esp32-device-gateway/projects.json`，仓库只提供 [example](config/projects.example.json)。删除项目仅取消注册。

Pi SDK host 要求本机 Pi **0.85.1** 与 Node 22。user systemd 的 PATH 可能不包含 Node 安装目录；在仓库外 `gateway.env` 配置 `GATEWAY_NODE_BIN` 和 `PI_SDK_MODULE` 的真实绝对路径。正常 shell 下可根据已安装的 `node` / `pi` 自动定位。Gateway 管理一个 Node SDK host，直接调用正式 SDK，没有启动 Pi TUI。

Codex **0.154.0** spike 已通过。本机复用现有 managed App Server 的 Unix control socket；在 `gateway.env` 设置 `GATEWAY_CODEX_ENABLED=1`，仅在非默认位置时设置 `GATEWAY_CODEX_SOCKET`。Gateway 不新建或重启共享 daemon，不将其暴露到公网。见 [spike 证据与限制](docs/codex-app-server-spike.md)。

统一 API、Pi SDK 调查及生命周期细节见 [Pi session control](docs/pi-session-control.md)。`/api/projects*`、`/api/agents*` 和 Admin 脚本仍在现有 Cloudflare Access `/api*` 边界内。运行中的 Codex turn 暂不接受第二条输入（409）；审批请求进入统一 Registry，并由设备或 Admin 决定。

显式运行真实 Pi 验收（调用模型，只创建专用临时 project/session）：

```bash
server/.venv/bin/python scripts/smoke-pi-control.py
```

流程包括 `SESSION_READY`、关闭 runtime、静态 DORMANT、同 ID 恢复并观察 `SESSION_RESUMED`、live input，以及 Gateway 重启后重新发现。结果见 [0.2.2 验收记录](docs/control-0.2.2-validation.md)。当前审批语义见 [Approval control](docs/approval-design.md)；Voice/Settings 已实现，见 [说明](docs/voice-input-design.md)。

## Voice Input + Session Settings 0.4.0

选择 Pi 或 Codex target 后，长按 A（600 ms）开始说话，松开结束，最多 20 秒。
短按 A/B 仍翻页。录音为 44.1 kHz / 16-bit / mono PCM，每 40 ms 一块，
通过已有认证 WSS 发送，Gateway 在内存中封装 WAV。转录后必须 A SEND / B CANCEL，
只有 SEND 才继续原 session。Approval 会取消录音或未发送的 transcript，并优先显示。

长按 B 打开设置：A NEXT、B SELECT/APPLY，长按 B 退出。Model 来自正式 Adapter；
Codex effort 来自所选模型 metadata，Pi 隐藏 effort。Device 最多六个模型，Admin
显示完整列表。RUNNING/WAITING_APPROVAL 时不允许修改，返回 409。

STT 通过 repo 外 `0600 ~/.config/esp32-device-gateway/gateway.env` 配置。
首选 `GATEWAY_STT_PROVIDER=cloudflare-workers-ai`，搭配 `GATEWAY_CF_ACCOUNT_ID`、
`GATEWAY_CF_API_TOKEN`、`GATEWAY_STT_MODEL=@cf/openai/whisper-large-v3-turbo`。
缺少 Cloudflare 授权时可显式选择 `openai-compatible`，使用
`GATEWAY_STT_BASE_URL=https://api.groq.com/openai/v1`、`GROQ_API_KEY` 和
`GATEWAY_STT_MODEL=whisper-large-v3-turbo`；已有通用 `GATEWAY_STT_API_KEY` 配置仍兼容。
**音频会发送给配置的 STT provider。** Gateway 默认不保存录音或 transcript，
也不记录正文日志。每次仅请求一个 provider，无自动 fallback、无重试。
直接发送现有 44.1 kHz WAV，不指定 language，默认不带 prompt。
授权失败、限流、超时和无有效 transcript 均显示 Transcription failed，不会发送 Agent input。

```bash
server/.venv/bin/python scripts/smoke-settings.py
server/.venv/bin/python scripts/smoke-stt.py  # 未配置时 SKIPPED，exit 2
```

真实 smoke 的公开音频、关键词和 prompt A/B 用法见 [0.4.1 验收记录](docs/stt-0.4.1-validation.md)。

## 可选 Pi Extension

本机 Pi `0.85.1` 使用正式 Extension API。扩展位于 `integrations/pi/device-lab-pager.ts`，除 lifecycle 外还订阅 `message_update` 的 `text_delta` 和 `tool_execution_start/end`。文本以 350 ms 或 120 字符为阈值批量发送；工具只发送脱敏后的短 action/path/command。所有请求均为 750 ms timeout 的 best effort，不读取 TUI/ANSI/session JSONL，也不阻塞 Pi 运行。

安装为全局扩展：

```bash
ln -s "$(pwd)/integrations/pi/device-lab-pager.ts" \
  ~/.pi/agent/extensions/device-lab-pager.ts
```

新 Pi 会话会自动加载；已打开的会话执行 `/reload`。状态统一为 `IDLE / RUNNING / DONE / FAILED / OFFLINE`。Gateway 为每个 session 在内存保留 12 KiB recent tail（不写磁盘），设备切换 target 时立即取得最近 4 KiB；StopWatch 在 PSRAM 继续保留 6 KiB 自动 tail。审批等待时覆盖显示 `WAITING_APPROVAL`；官方 `tool_call` hook 会等待设备或 Admin 的明确决定。

## 把固件下载到 Mac

在 Intel Mac 的仓库任意目录中，一键拉取最新代码、归档本地固件、下载 Lighthouse 构建、校验、检测设备、刷写并进入串口监视器：

```bash
./tools/mac/update-flash.sh
```

脚本只编排现有的 `device.sh` 能力。刷写前会显示本地旧版本和下载的新版本并要求确认；任一关键步骤失败都会立即停止。默认从 `lighthouse@claw.pangtiao.xyz:/home/lighthouse/esp32-dev/dist/m5stack-stopwatch` 下载，可通过 `REMOTE_HOST`、`REMOTE_REPO` 和 `BOARD` 环境变量覆盖。当前本地 artifact 会归档到 `dist/archive/`。

首次使用时脚本会在 Mac 工具缺失或损坏时自动运行 `tools/mac/install.sh`；工具正常时不会重复安装。

### 手动下载

默认使用 `scp`，不需要把二进制提交 Git。在 Mac 的仓库根目录执行：

```bash
mkdir -p dist
scp -r <vps-user>@<vps-host>:/absolute/path/esp32-dev/dist/m5stack-stopwatch dist/
```

如果 Mac 上还没有源码，先 `git clone <your-repo-url> esp32-dev && cd esp32-dev`，再执行上面的命令。

## Mac 首次安装

Mac 只需要 Python 3.8+，不需要完整 ESP-IDF，也不依赖 Homebrew：

```bash
./tools/mac/install.sh
```

脚本在 `tools/mac/.venv/` 安装固定的 `esptool 4.8.1` 和 `pyserial 3.5`。同时支持 Intel Mac；若旧系统没有 Python 3，请先安装 python.org 提供的 Intel 安装包。

## 第一次刷写前备份原厂固件

用支持数据传输的 USB 线连接设备，然后先执行：

```bash
./tools/mac/device.sh detect
./tools/mac/device.sh backup m5stack-stopwatch
```

只有一个 `/dev/cu.usbmodem*` 或 `/dev/cu.usbserial*` 时会自动选择；多个端口时会要求选择。也可显式指定：

```bash
ESPPORT=/dev/cu.usbmodem1101 ./tools/mac/device.sh backup m5stack-stopwatch
```

备份会读取设备报告的完整 Flash 大小，并在 Git 忽略的 `backups/` 中保存 `.bin` 和包含 MAC、Flash 大小、串口及 UTC 时间的 `.json`。

如果 esptool 无法自动连接 M5Stack StopWatch，保持 USB 连接，长按电源键约 2 秒，看到绿色指示灯后松开，使其进入下载模式，再重试。

## 刷写与串口日志

```bash
./tools/mac/device.sh flash dist/m5stack-stopwatch
```

脚本会检测 ESP32-S3、再次提醒备份、要求确认、将合并镜像写到 `0x0` 并复位，然后询问是否打开串口监视器。之后也可单独执行：

```bash
./tools/mac/device.sh monitor
```

串口监视器波特率为 `115200`，按 `Ctrl-]` 退出。脚本不会自动擦除整片 Flash；只有明确执行 `./tools/mac/device.sh erase` 才会进入带警告和确认的擦除流程。

## 恢复原厂镜像

```bash
./tools/mac/device.sh restore backups/m5stack-stopwatch-<UTC-date>.bin
```

恢复会再次检测芯片并要求确认，然后从 `0x0` 写回完整备份。

## 端到端验证

设备成功连接到 production Gateway 后，在已通过 Cloudflare Access 登录的浏览器中打开管理页查看 `device_id`：

<https://gw.pangtiao.men/admin>

管理 API 同样受 Access 保护。需要从命令行调用时，应先使用现有 Cloudflare Access 登录态，再向对应设备发消息：

```bash
curl -X POST https://gw.pangtiao.men/devices/<device-id>/message \
  -H 'content-type: application/json' \
  -d '{"title":"Hello","body":"Message from cloud"}'
```

通知实体自测可直接在 Lighthouse 本机执行，不需要真正启动 Pi run：

```bash
curl -X POST http://127.0.0.1:18081/devices/<device-id>/test-notification \
  -H 'content-type: application/json' -d '{"kind":"done"}'
curl -X POST http://127.0.0.1:18081/devices/<device-id>/test-notification \
  -H 'content-type: application/json' -d '{"kind":"failed"}'
```

公网同一路径属于现有 Cloudflare Access 的 `/devices*` 管理保护范围，匿名请求不能触发设备。设备必须在线，否则接口返回 `409`。

默认启动 Watch；A/B 以相反方向即时遍历 Watch、Agent、Monitor 五页和 Help。Gateway 断线显示 `RECONNECTING`，保留 Agent target/state/final_text 并自动恢复。

从管理页选择 Pi session 后，设备更新当前 Agent，保留正在查看的页面；Agent 页以正文为主，终态优先 `final_text`，运行态保留最新 activity、工具活动与 recent tail。连续 activity 最多约 4 FPS 重绘。真实 `RUNNING -> DONE` 转换触发 90% PWM 的 250/180 ms 长短双震及 880/1200 Hz 上扬提示音；`RUNNING -> FAILED` 触发三个 240 ms 强震及 600/400 Hz 下行提示音。同一 run 只提醒一次；Vibration OFF 关闭所有震动但不改变视觉状态或审批。声音由 I2S0 + ES8311 + AW8737A 非阻塞播放，音频初始化失败不影响显示、Wi-Fi、WSS 或 Pager。

检查清单：

- [ ] `./scripts/build.sh m5stack-stopwatch` 成功
- [ ] Mac 检测到 ESP32-S3 和 16 MB Flash
- [ ] 原厂完整 Flash 已备份
- [ ] `merged-flash.bin` 刷写成功
- [ ] 115200 串口启动日志可见
- [ ] 屏幕显示 LIGHTHOUSE、CPU/MEM/DISK/NET 和 ONLINE
- [ ] 日志出现 `[WIFI] connected <ip>`
- [ ] 日志出现 `[CLOUD] connected` 和 `hello acknowledged`
- [ ] `/devices` 中设备为 `online: true`
- [ ] 云端消息显示在设备上并触发轻震
- [ ] 按 A/B 后网关收到 `button_event`
- [ ] 管理页可选择 Pi session，设备显示 RUNNING/DONE/FAILED
- [ ] Pi 页面实时显示 assistant text 和脱敏后的 tool activity
- [ ] DONE/FAILED 自测产生对应声音和强震，且同一 run 不重复提醒
- [ ] 串口出现 `[AUDIO] init OK`（或明确的 nonfatal 失败日志）

首次启动的关键串口输出应类似：

```text
[BOOT] firmware 0.5.0
[BOARD] M5Stack StopWatch
[WIFI] connecting...
[WIFI] connected 192.168.x.x
[CLOUD] connecting to wss://gw.pangtiao.men/device/ws
[CLOUD] connected
[CLOUD] sent hello device_id=esp32s3-...
[CLOUD] hello acknowledged
[CLOUD] target server
```

设备 token 轮换：

```bash
./scripts/provision-device.py --rotate --device-id esp32s3-28848543a200
./scripts/build.sh m5stack-stopwatch
systemctl --user restart device-gateway.service
```

轮换后必须刷入新构建的固件。协议详情见 [`docs/protocol.md`](docs/protocol.md)。

## 工程结构

```text
boards/                 板级显示、按键和振动边界
config/                 不含秘密的配置样例
docs/                   官方硬件/BSP 调研记录
firmware/               ESP-IDF 应用
integrations/pi/         可选 Pi lifecycle Extension
scripts/                云端安装、构建、打包、网关运行和测试
server/                 FastAPI 内存网关及测试
tools/mac/              备份、刷写、恢复和串口监视器
dist/                   可复现构建产物（Git 忽略）
backups/                设备完整 Flash 备份（Git 忽略）
```

板卡与官方资源的具体选择见 [`docs/boards.md`](docs/boards.md)。
