# Flash Station 运行手册（Office iMac）

本文是 **Office iMac（烧录机）** 的权威运行手册，说明 iMac 与远端构建/生产主机的分工、
标准烧录流程、真机验收边界，以及"某个状态只存在于哪台机器"。

**在任何关于 build / artifact / deployment / Gateway / 真机行为的判断之前，先读本文。**
本文的结论来自两台机器的实测，不是对单个 checkout 的静态阅读。

---

## 1. 架构与角色分工

工程刻意分成**两台物理机器**。只看其中任何一台，都会得出错误结论。

| | Office iMac（本机） | Remote host |
|---|---|---|
| 主机 | 本机 | `lighthouse@claw.pangtiao.xyz` |
| 仓库路径 | `<clone>/esp32-dev` | `/home/lighthouse/esp32-dev` |
| 角色 | **USB 烧录 + 串口监视 + Wi-Fi 配网 + 真机验收** | **ESP-IDF 5.5.4 构建 + production Gateway + 部署** |
| ESP-IDF | **不需要安装** | `.cache/esp-idf-v5.5.4` |
| 工具 | `tools/mac/.venv`（esptool 4.8.1 + pyserial 3.5） | `.cache/espressif` + Node 22 |
| artifact | 只消费 `dist/`，不产生 | `scripts/build.sh` 产生 `dist/m5stack-stopwatch/` |
| Wi-Fi 凭据 | `.env`（git-ignored，本机独有） | 不需要，也不应存在 |
| Gateway | 不跑 production | production `gw.pangtiao.men` → `127.0.0.1:18081` |

```text
Remote host (claw.pangtiao.xyz)
  ESP-IDF 5.5.4 build ──► dist/m5stack-stopwatch/merged-flash.bin
                                │
                                │  scp / ssh（tools/mac/update-flash.sh）
                                ▼
Office iMac
  dist/m5stack-stopwatch/ ──► device.sh flash ──► ESP32-S3 via USB
                                                   │
                                                   ▼  wss://gw.pangtiao.men/device/ws
                                          Remote host production Gateway
```

关键推论：

- iMac 上**没有** `.cache/esp-idf-v5.5.4`、没有 `firmware/build/`，
  这是**正常的**，不代表工程从未构建。
- iMac 上 `dist/` 为空或缺失，通常只说明**还没有下载 artifact**，
  不代表远端构建失败。
- production Gateway 只存在于远端主机；iMac 上的 `server/` 是源码副本。

---

## 2. 首次 iMac 环境搭建

只需要 Python 3.8+（本机实测 3.9.6），不需要 ESP-IDF，也不依赖 Homebrew：

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

脚本在 `tools/mac/.venv/` 安装固定版本的 `esptool 4.8.1` 和 `pyserial 3.5`。
`update-flash.sh` 会在工具缺失或损坏时自动运行它。

创建本机独有的 Wi-Fi 凭据文件（**绝不提交**，`.gitignore` 已排除 `.env`）：

```bash
cat > .env <<'EOF'
DEVICE_WIFI_SSID=your-2.4ghz-ssid
DEVICE_WIFI_PASSWORD=your-password
EOF
chmod 600 .env
```

字段名为 `DEVICE_WIFI_SSID` / `DEVICE_WIFI_PASSWORD`。
凭据由 `tools/mac/provision-wifi.sh` **只从文件读取，从不经 argv**，
因此不会泄漏进进程列表或 shell history。**不要把真实凭据写进任何文档、日志或提交。**

> `.env` 只存在于 iMac。远端主机不需要也无法提供它；把仓库复制到第三台机器做真机测试，
> 必须重建 `.env`。

确认设备可见：

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

---

## 3. 访问远端主机（claw）

SSH 免密可用时（实测 `BatchMode=yes` 可直接登录）：

```bash
ssh lighthouse@claw.pangtiao.xyz
```

常用的远端检查命令：

```bash
# 远端仓库状态与版本
ssh lighthouse@claw.pangtiao.xyz 'cd /home/lighthouse/esp32-dev && \
  git branch --show-current && git rev-parse HEAD && git status --porcelain && cat VERSION'

# 远端构建产物是否存在及其校验值
ssh lighthouse@claw.pangtiao.xyz 'cd /home/lighthouse/esp32-dev && \
  ls -la dist/m5stack-stopwatch && cat dist/m5stack-stopwatch/flash.json'

# production Gateway 健康
curl -s https://gw.pangtiao.men/health
ssh lighthouse@claw.pangtiao.xyz 'curl -s http://127.0.0.1:18081/health'

# Gateway 服务状态与日志
ssh lighthouse@claw.pangtiao.xyz 'systemctl --user status device-gateway.service --no-pager | head -20'
ssh lighthouse@claw.pangtiao.xyz 'journalctl --user -u device-gateway.service -n 50 --no-pager'
```

`/health` 返回形如 `{"status":"ok","control_version":"0.5.0","online_devices":1}`。
`online_devices` 反映当前有几台真机连着 production Gateway。

远端构建（**只在远端执行**，iMac 上没有 ESP-IDF）：

```bash
ssh lighthouse@claw.pangtiao.xyz 'cd /home/lighthouse/esp32-dev && ./scripts/build.sh m5stack-stopwatch'
```

---

## 4. 标准流程：Build → Download → Verify → Flash → Provision → Monitor

完整流程由 `tools/mac/update-flash.sh` 一条命令编排。它会：拉取远端代码、
归档本地旧 artifact、从远端下载构建、校验、检测设备、刷写、并进入串口监视器。

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

刷写前会显示本地旧版本与下载到的新版本并要求确认；任一关键步骤失败立即停止。
下载默认来源（可用环境变量覆盖）：

| 变量 | 默认值 |
|---|---|
| `REMOTE_HOST` | `lighthouse@claw.pangtiao.xyz` |
| `REMOTE_REPO` | `/home/lighthouse/esp32-dev` |
| `BOARD` | `m5stack-stopwatch` |

即 artifact 目录 = `$REMOTE_REPO/dist/$BOARD`。

### 分步执行

只在需要单独重试某一步时才拆开：

```bash
# 1) 下载 artifact（不刷写）
scp -r lighthouse@claw.pangtiao.xyz:/home/lighthouse/esp32-dev/dist/m5stack-stopwatch dist/

# 2) 只校验，不烧写
./tools/mac/device.sh verify dist/m5stack-stopwatch

# 3) 烧写（会自动检测芯片、提醒备份、要求确认）
./tools/mac/device.sh flash dist/m5stack-stopwatch

# 4) 重新下发 Wi-Fi 凭据（见第 6 节，刷机后必须做）
./tools/mac/provision-wifi.sh --yes

# 5) 串口监视器（115200，Ctrl-] 退出）
./tools/mac/device.sh monitor
```

`update-flash.sh --no-monitor` 可跳过最后的串口监视器。

### 首次刷写前先备份原厂固件

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

备份保存在 git-ignored 的 `backups/`，含 `.bin` 与记录了 MAC、Flash 大小、串口与 UTC 时间的 `.json`。
恢复：`./tools/mac/device.sh restore backups/m5stack-stopwatch-<UTC-date>.bin`。

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

多串口时用 `ESPPORT` 显式指定：

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

---

## 5. Artifact provenance 检查

**判断"设备上跑的是什么"必须看 artifact 元数据，不要看目录时间戳或目录名。**

权威文件是 `dist/m5stack-stopwatch/flash.json`，由 `scripts/package-firmware.py` 生成：

```bash
cat dist/m5stack-stopwatch/flash.json
```

它给出 `version`、`chip`、`flash_size` 以及每个镜像和 `merged-flash.bin` 的 `sha256`。
`device.sh flash` / `verify` 只信任它：芯片类型与 `merged_image_sha256` 不符即 `exit 1`。
`VERSION` 是独立的明文版本文件。

三方核对设备上究竟跑了什么：

```bash
# a) iMac 本地 artifact
shasum -a 256 dist/m5stack-stopwatch/merged-flash.bin

# b) 远端构建产物（应一致）
ssh lighthouse@claw.pangtiao.xyz 'cd /home/lighthouse/esp32-dev && \
  sha256sum dist/m5stack-stopwatch/merged-flash.bin'

# c) 真机内部 app descriptor（构建时间 / IDF 版本）
#    烧录后从串口启动日志，或读 0x10020 偏移，核对 build_date/build_time/idf_ver
```

### 已知陷阱：`flash.json` 无法反查源 commit

`scripts/package-firmware.py` 的 metadata **不含 `source_commit` / `source_tree`**。
因此 artifact 无法自证对应哪个 git commit，只能靠 `version` + `build_time` + 三机 sha256 交叉印证。

如果 `dist/` 中同时存在 `candidate.json`，**不要相信它**：
该文件是 0.6a 时期的手工遗留，没有任何代码生成或消费（`package-firmware.py` 只写 `flash.json`），
且内容已陈旧矛盾（会声称旧版本、旧 commit）。`git ls-files` 无记录、`dist/` 本身 git-ignored。
以 `flash.json` 为准，其余一律忽略。

---

## 6. Wi-Fi provisioning：刷机后必须重新下发

**刷固件不会携带 Wi-Fi 凭据。** 凭据保存在设备自身 NVS 中，
构建期迁移只在**真正的首次启动**（NVS store 不存在）时触发。

所以重刷后若不下发，设备就联不上网——而这**极易被误判为固件 bug**。

```bash
./tools/mac/provision-wifi.sh --yes
```

- 默认读 `<repo>/.env`；也可 `--credentials PATH` 指定其他文件。
- `--ssid` 可只改 SSID；`--yes` 跳过确认。
- 密码为 **hidden input**，从不经 argv。

设备侧还有两条路径，互不替代：

- `./tools/mac/device.sh wifi` — 交互式管理设备上的 Known Networks（增 / 删 / 清 / 查）。
- 设备 UI `Settings → Network → Wi-Fi Setup` — SoftAP + QR + Web Portal，现场配网。

**设备绝不会因为连接失败而自动进入 SoftAP。** 找不到 Wi-Fi、密码错误、
已知 AP 消失、Gateway 不可达、重启无网，都只进入 `OFFLINE / RECONNECTING`。

---

## 7. Branch / candidate 真机验收流程

验收**分支候选**时，不要污染 production 目录，也不要用生产 checkout 的 `dist/`。

1. **在远端准备独立 candidate checkout**，构建到它自己的 `dist/`。
   实测惯例是在 `/home/lighthouse/` 下建独立目录（如
   `esp32-voice-final-candidate`、`esp32-voice-round2-candidate`）。
2. **在 iMac 上以显式 path 下载**，落到 iMac 的 candidate 目录，
   与 `dist/m5stack-stopwatch` 分开：

   ```bash
   scp -r lighthouse@claw.pangtiao.xyz:/home/lighthouse/esp32-voice-final-candidate/dist/m5stack-stopwatch \
     dist/voice-final-candidate/
   ./tools/mac/device.sh verify dist/voice-final-candidate/m5stack-stopwatch
   ./tools/mac/device.sh flash dist/voice-final-candidate/m5stack-stopwatch
   ```

3. **记录 provenance**：candidate 的 `flash.json`、构建时间、串口启动日志。
4. **逐条执行验收表**，每轮保留设备串口日志与 Gateway 侧元数据日志，
   用指纹字段（如 `voice_id`）关联两端时间轴。
5. **验收完成后明确回到 production artifact 或执行 `restore`**，不要留下含义不明的
   本地 candidate 目录当作"当前版本"。

验收清单与判定标准以对应分支的验收文档为准
（例如 `docs/voice-weak-network-round2-acceptance.md`）。

---

## 8. production Gateway 与 staging Gateway 的边界

production Gateway 固定如下（远端主机 user systemd 服务，经 Cloudflare Tunnel 暴露）：

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

安装 / 更新见 README 的 *Lighthouse production deployment* 一节。
配置在**仓库外**的 `~/.config/esp32-device-gateway/gateway.env`（权限 `0600`）。
`/device/ws`、`/integrations/pi/events`、`/health` 是机器路径，**不依赖 Cloudflare Access**；
`/admin`、`/api/*`、`/server*`、`/devices*`、`/docs` 由 Access 保护。

### 不要与 production 争抢同一台真机

同一台真机（MAC `28:84:85:43:a2:00`）**同时绑定 production Gateway**。
若在 iMac 上也起一个 Gateway 指向**同一 `device_id`**：

- 两个 Gateway 会争抢同一 WS incarnation；
- 表现为互相 `close(1012)`、设备反复重连；
- **极易被误判为固件 bug**。

因此做故障注入或弱网验收时：

- 使用**独立 staging Gateway**：独立端口、独立 `device_token`；
- 或临时把设备指向 staging，验收结束再切回；
- 做完即停，不要在 production 旁边留下常驻的第二套 Gateway。

区分方法：

| 现象 | production 问题 | iMac staging 问题 |
|---|---|---|
| 设备反复 1012 / 重连 | 通常不是，除非有人在别处起了第二套 Gateway | 先查本机是否在跑 Gateway |
| `capture-queue-full` 等设备侧事件 | **不会**出现在 Gateway 日志中 | 只能从**设备串口**读取 |
| `online_devices` 数量异常 | 在远端 `/health` 查 | 本机 staging 有自己独立的 `/health` |

---

## 9. 常见误判与故障排查

| 观察到的现象 | 错误结论 | 真实解释 |
|---|---|---|
| iMac 上 `.cache/esp-idf-*` 不存在、无 `.elf/.bin` | "工程从未构建过" | 构建在远端主机；iMac 按设计不装 ESP-IDF |
| iMac 上 `dist/` 为空 | "构建失败 / 产物丢失" | 只是还没下载 artifact，跑 `update-flash.sh` 或 `scp` |
| iMac 上 `git log` 落后 / 有本地改动 | "远端也是这个状态" | 两台机器是独立 checkout，必须分别查 |
| iMac 上跑 `scripts/build.sh` 失败 | "构建被破坏" | iMac 没有 ESP-IDF，这是预期失败；构建属远端 |
| 远端 `firmware/tests/run_history_binding_test.py` 在本机失败 | "测试挂了 / 有回归" | 该测试硬编码 `.cache/esp-idf-v5.5.4` 路径，只在构建机可跑 |
| 重刷后设备联不上网 | "固件有 bug" | 刷机不带 Wi-Fi 凭据，需 `provision-wifi.sh`（第 6 节） |
| 设备反复重连 / 1012 | "固件网络栈坏了" | 先排查是否有第二套 Gateway 在抢同一 `device_id`（第 8 节） |
| 远端目录名写 `releases/0.8.0-<sha>` 或进程命令行含旧版本 | "production 跑的旧代码" | 以 drop-in 显式声明的 `WorkingDirectory` + `PYTHONPATH` 为准；解释器路径与代码路径可以不同 |
| Gateway 日志里搜不到设备侧事件 | "问题不存在" | 设备侧事件（如 `capture-queue-full`）**不写进 Gateway 日志**，只能从串口看 |
| `dist/` 中 `candidate.json` 写 `0.6.1` | "artifact 是 0.6.1" | 该文件是死文件，以 `flash.json` 为准（第 5 节） |
| `/api/*` 用 curl 拿不到内容 | "缺 auth，是漏洞" | 返回 302 是 Cloudflare Access 登录跳转，边界正常生效 |

### production Gateway 状态检查

```bash
ssh lighthouse@claw.pangtiao.xyz 'systemctl --user status device-gateway.service --no-pager | head -20'
ssh lighthouse@claw.pangtiao.xyz 'ls ~/.config/systemd/user/device-gateway.service.d/'
```

远端存在**单一权威 drop-in**，显式声明唯一的 `WorkingDirectory` / `ExecStart` / `PYTHONPATH`。
历史上曾出现 6 层按字母序层叠的 drop-in，导致"加一个文件就静默切换生产代码路径"。
**不要新增竞争的 override 文件；要改就改那一个。**

---

## 10. 哪些状态只存在于某一台机器

下结论前先确认自己在看哪台机器。

| 状态 | 只存在于 | 如何检查 |
|---|---|---|
| ESP-IDF 5.5.4 与工具链 | 远端 | 远端 `ls .cache/esp-idf-v5.5.4` |
| `scripts/build.sh` 的成功构建输出 | 远端 | 远端 `firmware/build/`、`dist/` |
| production Gateway 进程 / 日志 / 端口 18081 | 远端 | 远端 `systemctl --user status`、`journalctl` |
| production artifact 的源 checkout | 远端 | 远端 `git rev-parse HEAD` |
| Cloudflare Tunnel 配置 | 远端 | 远端 `cloudflared-dsh.service` |
| Wi-Fi 凭据 `.env` | **iMac** | iMac `ls -l .env`（**不要打印内容**） |
| `tools/mac/.venv` | **iMac** | iMac `tools/mac/.venv/bin/python -V` |
| 真机 USB 串口 | **iMac** | iMac `ls /dev/cu.usb*` |
| `backups/` 原厂镜像 | **iMac** | iMac `ls backups/` |
| 设备自身 NVS（Wi-Fi profile、设置、target） | **ESP32 真机** | 串口日志、设备屏幕 UI |
| 设备当前固件版本与构建时间 | **ESP32 真机** | 串口启动日志、app descriptor `0x10020` |
| Gateway 内存中的在线设备状态 | 对应 Gateway 进程 | 该 Gateway 的 `/health`、`/devices` |
| `~/.config/esp32-device-gateway/`（token、gateway.env、projects.json） | 远端 | **仓库外**，权限 `0600`，不打印 |

因此有三个环境必须分开判断：**iMac**、**远端主机**、**ESP32 真机**。

---

## 11. 安全规则

1. **不记录 secret。** 不把 token、password、Wi-Fi 凭据写入文档、日志、commit、
   聊天或截图。凭据只放在 git-ignored 的 `.env` 与仓库外 `0600` 文件中。
   检查时只确认存在性与权限，不打印内容。
2. **不随意重启 production。** `device-gateway.service`、`cloudflared-dsh.service`
   与远端其他服务共享主机。需要重启必须有明确授权，并先说明影响范围。
   只读诊断（`status`、`journalctl`、`/health`）不需要授权。
3. **不 force push，不改写共享历史。** 不使用 `--force` / `--force-with-lease`
   覆盖他人分支；不改写 `main` 的历史。
4. **不把本地缺失状态当作远端缺失。** iMac 上没有 `.cache/esp-idf-*`、没有 `dist/`、
   没有 `firmware/build/`，都不构成"工程未构建"或"构建失败"的证据。
   要判断构建/部署状态，**必须去远端查**。
5. **不提交构建产物与凭据。** `dist/`、`backups/`、`.cache/`、`.env`、
   `firmware/build/`、`firmware/sdkconfig` 均已在 `.gitignore` 内。
6. **不与 production 争抢真机。** 见第 8 节；调试用独立端口与独立 `device_token`。
7. **不擅自擦除整片 Flash。** 只有明确执行 `device.sh erase` 才进入带警告和确认的擦除流程；
   首次接触任何设备前先 `backup`。

---

## 12. 快速参考

```bash
# 远端（构建 + 生产）
ssh lighthouse@claw.pangtiao.xyz
cd /home/lighthouse/esp32-dev && ./scripts/build.sh m5stack-stopwatch
curl -s https://gw.pangtiao.men/health

# iMac（烧录 + 验收）
./tools/mac/install.sh                    # 首次：esptool + pyserial
./tools/mac/device.sh detect              # 确认 ESP32-S3 可见
./tools/mac/device.sh backup m5stack-stopwatch   # 首次刷写前
./tools/mac/update-flash.sh               # 一键：拉代码 → 下载 → 校验 → 刷写 → 监视
./tools/mac/provision-wifi.sh --yes       # 刷机后必须重新下发凭据
./tools/mac/device.sh verify dist/m5stack-stopwatch
./tools/mac/device.sh monitor             # 115200，Ctrl-] 退出
./tools/mac/device.sh restore backups/m5stack-stopwatch-<UTC-date>.bin
```

相关文档：

- Build / artifact 生成：`scripts/build.sh`、`scripts/package-firmware.py`
- 烧录机脚本：`tools/mac/update-flash.sh`、`tools/mac/device.sh`、`tools/mac/provision-wifi.sh`
- production Gateway 部署：README *Lighthouse production deployment*
- Wi-Fi 配网设计：`docs/wifi-provisioning-plan.md`
- 分工与 provenance 审查记录：`docs/review-2026-10-08-local-flash-role.md`