# Wi-Fi Provisioning 规划

## 1. 文档目标

本文定义 M5Stack StopWatch / ESP32 Device Lab 后续 Wi-Fi 配置能力的产品规则、交互规则、数据边界和实现验收要求。

本文当前仅作为 **implementation-ready design document** 保存。

在现有核心功能，包括：

- Wearable Shell
- Agent 状态
- Monitor
- Approval
- Speech
- Settings
- Touch / gesture 等主要交互

基本稳定之前，**不要因为本文主动开始开发 Wi-Fi Provisioning**。

未来 Coding Agent 接到明确实现任务后，应先完整阅读本文，再检查当前代码状态，并在尽量少改变现有架构的前提下实施。

---

# 2. 核心目标

当前 firmware 中 Wi-Fi SSID / Password 通过构建配置写入固件。

这种方式适合早期开发，但不适合作为长期设备行为。

目标是把：

```text
Wi-Fi SSID
Wi-Fi Password
```

从 firmware build-time configuration 中解耦出来，使设备可以在不重新编译、不重新刷写 firmware 的情况下切换网络。

需要提供两种配置入口：

1. **设备 Settings → Network → Wi-Fi Setup → SoftAP + Web 配网**
2. **USB Serial 配网脚本**

两种入口最终必须写入相同的持久化 Wi-Fi Profile Store。

---

# 3. 明确的产品原则

## 3.1 不自动进入配网模式

这是本设计最重要的约束。

设备：

- 找不到已知 Wi-Fi；
- Wi-Fi 密码错误；
- Gateway 无法连接；
- 网络暂时断开；
- 重启后 Wi-Fi 不可用；

都**不得自动启动 SoftAP**。

设备只进入正常的 Offline / Reconnecting 状态。

SoftAP Provisioning **只能由用户主动触发**：

```text
Settings
→ Network
→ Wi-Fi Setup
→ Start Setup
```

这样避免：

- 路由器暂时掉线时突然出现 AP；
- 携带设备移动过程中频繁进入 Provisioning；
- 不必要的功耗；
- 无意中扩大无线攻击面；
- 用户无法理解设备为什么突然切换网络状态。

---

# 4. Settings 信息架构

Settings Root 后续增加：

```text
SETTINGS

Agent
Device
Display
Network
System
Back
```

Network 一级页面建议：

```text
NETWORK

Home-WiFi
CONNECTED · -48 dBm

Wi-Fi Setup
Known Networks
Back
```

具体排版可以根据当时的 Wearable Shell 版本调整，但语义保持一致。

第一版不要求在设备小屏幕上完成：

- SSID 手工输入；
- Password 输入；
- 虚拟键盘；
- Wi-Fi 扫描列表选择。

这些输入全部交给手机或电脑的 Web Provisioning 页面。

---

# 5. Wi-Fi Setup 启动流程

用户主动选择：

```text
Settings
→ Network
→ Wi-Fi Setup
```

再执行一次明确确认：

```text
START WIFI SETUP?

Start
Cancel
```

用户确认以后才启动 SoftAP。

建议状态：

```text
NORMAL
  ↓ user request
STARTING_AP
  ↓
PROVISIONING
  ↓ success / cancel
NORMAL
```

Provisioning 是明确的 Modal / Mode，不应该偷偷在后台启动。

---

# 6. SoftAP 规则

## 6.1 SSID

SSID 必须稳定且可识别设备。

推荐：

```text
AgentPager-XXXX
```

其中：

```text
XXXX
```

来自稳定的 device_id / MAC 后四位或类似唯一后缀。

例如：

```text
AgentPager-A2F3
```

不要每次 Provisioning 随机生成 SSID。

原因：

- 手机可以记住网络；
- 用户以后更容易辨认自己的设备；
- 多块设备同时存在时不冲突。

---

# 7. 固定 AP Password

Provisioning AP 使用一个**固定密码**。

目标是：

> 手机或电脑第一次连接以后可以保存密码，后续再次进入 Wi-Fi Setup 时能够自动连接。

密码至少满足 WPA2 minimum password requirement。

具体密码应定义为一个集中配置常量，而不是散落在多个源文件中。

例如概念上：

```text
PROVISIONING_AP_PASSWORD
```

实际值由项目维护者确定。

不要：

- 每次随机生成密码；
- 把 device token 当 AP password；
- 从 Gateway credential 派生 AP password；
- 在日志中打印其他 credential。

固定密码意味着：

> 任何知道密码的人，在设备主动开启 Provisioning AP 的时间窗口内，都有机会连接。

这一风险当前接受，因为：

1. AP 永远不会自动开启；
2. 必须用户在实体设备上主动进入 Settings；
3. Provisioning 页面只允许配置 Wi-Fi；
4. Device Token / Gateway Token 等敏感配置绝不能通过 Portal 暴露。

未来如果产品化，可重新评估安全模型。

---

# 8. Provisioning 屏幕

进入成功后，StopWatch 显示：

```text
WIFI SETUP

[ QR CODE ]

AgentPager-A2F3

Password
********

192.168.4.1
```

实际排版根据 466×466 round-safe layout 调整。

必须同时存在：

1. QR Code
2. SSID
3. 固定 Password
4. Setup Address

不能只显示二维码。

原因是：

- Mac / PC 使用者可能更习惯手工连接；
- 摄像头扫码可能失败；
- QR 是 convenience，不应该成为唯一入口。

---

# 9. QR Code

二维码优先编码为标准 Wi-Fi QR 格式：

```text
WIFI:T:WPA;S:<ssid>;P:<password>;;
```

例如：

```text
WIFI:T:WPA;S:AgentPager-A2F3;P:<fixed-password>;;
```

这样手机扫码后优先完成：

> 加入 Provisioning AP

而不是尝试编码过多应用层状态。

Web Setup URL 在连接以后通过：

```text
http://192.168.4.1
```

访问。

如果未来 Captive Portal 能可靠自动弹出，则二维码仍然保留作为快速连接入口。

---

# 10. SoftAP 网络参数

建议第一版保持传统 ESP SoftAP 地址：

```text
Device IP:
192.168.4.1
```

Provisioning Web：

```text
http://192.168.4.1/
```

不要为了这个功能增加：

- HTTPS certificate management；
- Cloud service dependency；
- Cloudflare；
- 外部 DNS；
- 用户账号系统。

Provisioning 是设备本地临时网络。

---

# 11. Captive Portal

第一版可以实现轻量 Captive Portal。

建议：

```text
SoftAP
 + DHCP
 + DNS catch-all
 + HTTP server
```

手机连接以后：

```text
any domain
→ 192.168.4.1
```

从而尽可能触发系统 Captive Portal UI。

但是：

**Captive Portal 自动弹出不是功能正确性的必要条件。**

必须始终支持：

```text
http://192.168.4.1
```

手工访问。

不同 Android / iOS / macOS / Windows 的 captive portal 行为不应成为 firmware correctness dependency。

---

# 12. Provisioning Web UI

Web 页面保持极简。

第一页：

```text
AgentPager Wi-Fi Setup

Available Networks

○ Home-WiFi        -42 dBm
○ Office           -58 dBm
○ iPhone           -67 dBm

Other Network...

Password
[________________]

[ Connect ]
```

允许：

- 扫描附近 2.4GHz SSID；
- 选择 SSID；
- 输入密码；
- 输入隐藏 SSID。

明确提示：

```text
This device supports 2.4 GHz Wi-Fi.
```

ESP32-S3 不应让用户误以为可以连接 5GHz-only AP。

---

# 13. Credential 提交

Web Client 提交：

```text
SSID
Password
```

以后，设备执行：

```text
receive credentials
↓
temporarily attempt connection
↓
success?
```

不要一收到表单就立即覆盖现有已知可用网络。

推荐：

```text
candidate credentials
↓
connection test
↓
success
↓
persist profile
```

失败则：

```text
candidate discarded
existing profiles preserved
SoftAP remains active
```

Web 页面返回：

```text
Connection failed

Check SSID/password and try again.
```

---

# 14. 配网成功

连接成功以后：

1. 写入 Wi-Fi Profile Store；
2. 标记为最近成功网络；
3. Web 页面显示成功；
4. StopWatch 显示：

```text
CONNECTED

Home-WiFi

Setup complete
```

5. 给用户短暂时间看到成功状态；
6. 关闭 SoftAP；
7. 回到正常运行状态；
8. 按正常流程连接 Gateway。

不要重启整个 ESP32，除非当时实现证明重启是必要且更可靠的。

优先支持：

> provisioning → station connection → normal runtime

无重启切换。

---

# 15. Wi-Fi Profile Store

Wi-Fi credential 不再永久依赖：

```text
sdkconfig.defaults.local
```

运行时使用 NVS 保存 Profile。

建议支持多个 Known Network。

第一版上限可以控制在：

```text
5
```

或其他小而固定的数量。

每个 Profile 最少包含：

```text
ssid
password
last_success
```

可选：

```text
priority
```

不要把复杂的 Wi-Fi management database 引入 ESP32。

---

# 16. 自动选择 Known Network

虽然 **不允许自动进入 Provisioning**，但正常启动时应自动连接 Known Networks。

流程：

```text
BOOT
↓
load profiles
↓
scan / attempt known profiles
↓
available known network?
```

找到以后：

```text
connect
```

没有找到：

```text
OFFLINE
```

保持 Offline 并按合理 backoff 重试。

**不进入 SoftAP。**

---

# 17. 网络选择策略

第一版保持简单：

优先级建议：

1. 最近成功连接的 Profile；
2. 其他已知 Profile；
3. 同优先级优先较强信号。

不要追求复杂 roaming algorithm。

如果当前网络正常：

**不要因为另一个已知网络 RSSI 稍强就主动切换。**

避免无意义的网络 flap。

---

# 18. 手机热点

Phone Hotspot 是正式支持场景。

例如：

```text
StopWatch
↓
iPhone / Android Hotspot
↓
Internet
↓
Gateway
```

无需特殊协议。

唯一要求是：

> 手机热点需要提供设备能够连接的 2.4GHz 网络。

文档和 Web UI 可以提醒这一点。

---

# 19. Known Networks 管理

Settings → Network 后续支持：

```text
Known Networks
```

可以查看：

```text
Home-WiFi
Office
Phone-Hotspot
```

至少提供：

```text
Forget
```

第一版不需要设备端显示 Password。

密码永远不在 AMOLED UI 中回显。

固定 Provisioning AP Password 除外，因为它本身设计为用户可知。

---

# 20. Forget 规则

Forget Current：

```text
remove selected profile
```

如果当前连接正使用该 Profile：

```text
disconnect
↓
try other known profiles
↓
otherwise offline
```

依然：

**不得自动进入 Provisioning。**

Forget All：

需要额外确认。

例如：

```text
FORGET ALL WIFI?

Cancel
Confirm
```

完成以后设备变成：

```text
NO KNOWN WIFI
```

但仍然：

**不会自动打开 AP。**

用户需要：

```text
Settings → Network → Wi-Fi Setup
```

重新配置。

---

# 21. Firmware Build-Time Wi-Fi

当前开发配置：

```text
CONFIG_DEVICE_LAB_WIFI_SSID
CONFIG_DEVICE_LAB_WIFI_PASSWORD
```

迁移阶段可以作为：

> first boot / development fallback

但最终运行时优先级应明确：

```text
NVS profiles
↓
optional development fallback
```

长期目标是：

**正式设备运行不依赖重新编译 SSID/Password。**

实现时不要突然删除现有 build-time 配置，导致已有开发流程全部失效。

应采用兼容迁移方式。

---

# 22. USB Provisioning

必须增加第二条独立入口：

```text
USB Serial
```

目标：

> 即使 SoftAP / Web Portal 出现问题，开发者仍然可以从 Mac/PC 恢复 Wi-Fi 配置。

该功能是开发者设备的重要 recovery path。

---

# 23. USB CLI 入口

在现有：

```text
tools/mac/device.sh
```

体系中增加类似：

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

或者如果现有架构更适合：

```bash
./tools/mac/wifi.sh
```

优先复用 `device.sh`。

不要重新创建第二套串口检测、Python 环境和设备选择逻辑。

---

# 24. USB 配网交互

推荐：

```text
$ ./tools/mac/device.sh wifi

Device:
M5Stack StopWatch
/dev/cu.usbmodem1101

Known networks:
1. Home-WiFi
2. Phone-Hotspot

Actions:
1. Add network
2. Forget network
3. Forget all
4. Show status
5. Exit
```

选择 Add：

```text
SSID: Home-WiFi
Password:
```

Password 输入必须：

**terminal hidden input**

不能 echo。

成功后：

```text
Saved Home-WiFi
Connection test: PASS
```

或者：

```text
Saved
Connection test: unavailable
```

必须诚实区分。

---

# 25. USB Transport

优先使用现有 USB Serial / CDC channel。

不要因为 Wi-Fi Provisioning 增加：

- USB networking；
- custom USB driver；
- proprietary binary tool；
- second firmware image。

可以实现一个简单、版本化的 Serial command protocol。

例如概念上：

```text
wifi.status
wifi.list
wifi.add
wifi.remove
wifi.clear
```

具体 wire format 在实现阶段决定。

要求：

- 有 framing；
- 有 response；
- 有 timeout；
- 不把密码打印回 terminal；
- 不把密码写进普通 log。

---

# 26. SoftAP 与 USB 使用同一后台逻辑

非常重要：

不要形成：

```text
Web Wi-Fi manager
```

和：

```text
USB Wi-Fi manager
```

两套实现。

应该是：

```text
            Web Provisioning
                  │
                  ▼
          Wi-Fi Profile Service
                  ▲
                  │
            USB Provisioning
```

共同调用：

```text
list_profiles()
add_profile()
remove_profile()
clear_profiles()
test_profile()
connect_best()
```

这样以后 BLE 如果真的可用，也只是第三个 frontend：

```text
BLE
 │
 ▼
Wi-Fi Profile Service
```

不改变核心逻辑。

---

# 27. Device Identity 与 Network Identity 分离

Wi-Fi Provisioning 只修改：

```text
SSID
Password
```

不得修改：

```text
device_id
device_token
gateway_url
gateway authentication
Cloudflare configuration
agent target
```

设备身份仍由独立 provisioning / firmware config 管理。

换 Wi-Fi 不等于换 Gateway。

---

# 28. Web Portal Security Boundary

Provisioning Web UI 只能访问 Wi-Fi 设置能力。

禁止提供：

- device token；
- Gateway token；
- API key；
- STT credential；
- Agent session content；
- firmware secrets；
- arbitrary shell；
- filesystem。

Portal 最多暴露：

```text
device display name
device suffix
firmware version
nearby SSIDs
known SSID names
connection state
RSSI
```

Known Network 的 password 永远不回传浏览器。

---

# 29. Credential Logging Rules

任何 log 不得包含：

```text
Wi-Fi password
device token
Gateway token
API key
```

允许：

```text
[WIFI] profile add ssid=Home-WiFi
[WIFI] connection success ssid=Home-WiFi rssi=-48
```

禁止：

```text
password=12345678
```

USB CLI 也遵守相同规则。

---

# 30. SoftAP Lifetime

Provisioning AP 只有以下条件关闭：

### 成功配置并切换到 Station

```text
success
→ AP close
```

### 用户主动退出

例如：

```text
Hold B Close
```

或明确 Cancel。

### 合理的长时间 idle timeout

可以考虑：

```text
10–15 minutes
```

但不是第一版强制要求。

如果实现 timeout：

屏幕需要明确显示：

```text
Setup expired
```

然后回到正常 Offline/Online 状态。

不要出现后台 AP 永久运行而用户不知道。

---

# 31. Provisioning 与已有 Agent 功能关系

进入 Wi-Fi Setup 后：

Provisioning UI 具有较高优先级。

可以暂停：

- 正常页面 navigation；
- Agent visual notification overlay。

但不应该破坏：

- Agent session；
- Gateway server state；
- NVS preferences；
- RTC。

如果切 Wi-Fi 导致 Gateway 连接断开，这是正常行为。

退出 / 成功以后 Gateway 应按现有 reconnect mechanism 恢复。

---

# 32. Speech / Approval

Provisioning Mode 第一版不处理：

- Agent Approval；
- Voice Input；
- Speech Output。

如果进入 Provisioning 时存在未处理 Approval：

优先选择：

```text
refuse to enter Wi-Fi Setup
```

并提示：

```text
Finish pending approval first
```

而不是让 Wi-Fi 配网覆盖 Human Gate。

如果 Agent 正在普通 RUNNING，则可以允许进入 Wi-Fi Setup，但 Gateway 可能因切网短暂掉线。

实现阶段根据当前 modal priority 机制做最小兼容。

---

# 33. QR Code Renderer

QR renderer 必须：

- 本地生成；
- 不调用云端 QR API；
- 不依赖网络；
- 不动态分配巨大缓冲区；
- 适配 466×466 AMOLED；
- 保持足够 quiet zone；
- 真机可以被主流手机摄像头识别。

二维码内容只包含：

```text
Provisioning SSID
Provisioning AP password
```

不得编码 device token。

---

# 34. Power

SoftAP + Wi-Fi Scan 明显增加功耗。

Provisioning 是人工临时模式，因此接受较高功耗。

不要为了 Provisioning 做复杂 low-power optimization。

但退出 Provisioning 后必须：

```text
stop SoftAP
stop captive portal
free HTTP/DNS resources
return STA runtime
```

不能同时长期保持：

```text
STA + AP
```

除非实现过程中有非常明确的技术理由。

---

# 35. Failure UX

至少区分：

### Wrong credentials

```text
Cannot connect
Check password
```

### Network unavailable

```text
Network not found
```

### Unsupported / no usable 2.4GHz AP

```text
No compatible Wi-Fi found
```

### Storage failure

```text
Connected
Not saved
```

### SoftAP startup failure

```text
Wi-Fi setup unavailable
```

不能统一成：

```text
ERROR
```

---

# 36. 不在第一版范围内

明确不实现：

- BLE Provisioning；
- NFC Provisioning；
- WPS；
- SmartConfig；
- Cloud-based provisioning；
- user account login；
- remote Wi-Fi configuration；
- Wi-Fi password sync；
- WPA Enterprise；
- 5GHz；
- Mesh；
- Thread provisioning；
- QR cloud service；
- mobile App；
- virtual keyboard on StopWatch；
- automatic AP startup；
- automatic reset after N failed connects。

---

# 37. 推荐模块边界

未来实现时优先形成：

```text
wifi_manager
    │
    ├── WiFiProfileStore
    │
    ├── StationManager
    │
    └── ProvisioningController
             │
             ├── SoftAP
             ├── HTTP/Captive Portal
             └── QR/UI
```

USB：

```text
USB CLI
  │
  ▼
WiFiProfileStore / StationManager
```

不要把所有逻辑继续堆进：

```text
board.cpp
```

尤其：

- credential persistence；
- profile selection；
- provisioning state；

应该是可 host-test 的独立逻辑。

Board 层只处理：

- screen；
- buttons；
- AP UI；
- current status rendering。

---

# 38. Host Tests

未来实现必须至少覆盖：

## Profile

```text
add
replace
remove
clear
max profile count
persistence
corrupt data fallback
```

## Selection

```text
last successful preferred
known network absent
multiple known networks
connection failure fallback
```

## Provisioning

```text
manual start only
cancel
successful candidate
failed candidate
existing credential preserved
```

## Security

确认输出/log 中不存在：

```text
password
device_token
gateway_token
```

---

# 39. Firmware Tests

必须真实：

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

成功。

不能只靠 host test。

---

# 40. Physical Acceptance

未来实现完成后必须进行真机验证。

## Test 1 — Manual Entry

正常连接状态：

```text
Settings
→ Network
→ Wi-Fi Setup
```

此前绝不能存在 Provisioning AP。

确认后：

```text
AgentPager-XXXX
```

才出现。

---

## Test 2 — QR

手机扫码：

```text
QR
↓
recognizes SSID/password
↓
joins AP
```

---

## Test 3 — Manual AP

电脑手工连接：

```text
AgentPager-XXXX
```

使用固定密码。

访问：

```text
192.168.4.1
```

Portal 正常。

---

## Test 4 — Configure Wi-Fi

选择一个新的 2.4GHz Wi-Fi：

```text
input password
↓
Connect
↓
device connects
↓
profile saved
↓
AP closes
↓
Gateway reconnects
```

---

## Test 5 — Reboot

重启。

设备无需再次 Provisioning：

```text
automatically connects known Wi-Fi
```

---

## Test 6 — Multiple Networks

配置两个网络。

分别只开放其中一个。

设备应自动连接当前存在的 Known Network。

---

## Test 7 — No Known Network

关闭所有 Known Network。

设备：

```text
OFFLINE
```

但：

**不得自动启动 SoftAP。**

---

## Test 8 — USB

Mac：

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

完成：

```text
list
add
remove
status
```

然后设备正常连接新网络。

---

# 41. 实现完成条件

Coding Agent 未来只有同时满足以下条件，才能标记：

```text
SOFTWARE VERIFIED
READY FOR PHYSICAL TEST
```

- Profile Store 完成；
- SoftAP 只能手工启动；
- QR 完成；
- 固定 AP SSID/password 完成；
- Web Provisioning 完成；
- 多 Known Network 完成；
- USB CLI 完成；
- secrets 不泄漏；
- host tests PASS；
- firmware build PASS；
- existing Agent / Voice / Approval regression PASS。

只有真机完成上述 Physical Acceptance，才能标记：

```text
PHYSICAL PASS
```

---

# 42. 最终产品行为摘要

正常情况下用户永远只看到：

```text
Wi-Fi online
```

换网络时：

```text
Settings
  ↓
Network
  ↓
Wi-Fi Setup
  ↓
Start
```

然后：

```text
StopWatch
    ↓
SoftAP + QR
    ↓
Phone / Computer
    ↓
Web Portal
    ↓
New Wi-Fi
    ↓
NVS Profile
    ↓
Gateway
```

Developer recovery path：

```text
Mac
 ↓ USB
device.sh wifi
 ↓
same Wi-Fi Profile Store
```

核心原则：

> **设备主动配网，而不是设备自动进入配网。**

> **Wi-Fi 可以改变，设备身份不改变。**

> **SoftAP 和 USB 是两个入口，共享一套 Profile / Network Manager。**

> **StopWatch 负责启动和展示，手机/电脑负责输入复杂 Wi-Fi credential。**

---

# Implementation Status (0.7.0)

This section records the 0.7.0 implementation against the design above. The
design sections 1–42 are unchanged and remain authoritative.

## Module map

```text
boards/common/include/wifi_profiles.h      WiFiProfileStore (pure, host-tested)
boards/common/include/wifi_selection.h     ProvisioningMode, backoff, SSID/QR
boards/common/include/qr_code.h            local byte-mode QR encoder
boards/common/include/wifi_usb_protocol.h  framed USB command protocol
boards/common/include/board_network.h      the only interface board.cpp may call

firmware/main/wifi_store_nvs.{h,cpp}       NVS persistence for the store
firmware/main/wifi_manager.{h,cpp}         StationManager, ProvisioningController,
                                           Portal, USB console task

tools/mac/device.sh wifi                   interactive USB recovery entry
tools/mac/wifi_ctl.py                      host side of the USB protocol
tools/mac/provision-wifi.sh                post-flash credential re-apply
```

## Section-by-section status

| Design section | Status | Notes |
|---|---|---|
| 3 no automatic SoftAP | Implemented | `ProvisioningMode` only leaves Normal via `request_start_access_point(confirmed=true)`; tested in `wifi_profiles_test.cpp` |
| 4 Settings information architecture | Implemented | `Menu::Network`, `Menu::KnownNetworks` |
| 5 setup start flow | Implemented | `STARTING_AP → PROVISIONING → NORMAL`, `Failed` on AP error |
| 6–7 SoftAP SSID / fixed password | Implemented | `AgentPager-XXXX` from the Wi-Fi MAC suffix; `PROVISIONING_AP_PASSWORD` defined once |
| 8–9 provisioning screen / QR | Implemented | QR + SSID + password + `192.168.4.1`; local encoder, quiet zone, round-safe |
| 10–11 SoftAP params / captive portal | Implemented | `192.168.4.1` always works; catch-all redirect is convenience only |
| 12–14 Web UI / candidate-first / success | Implemented | Candidate validated before persist; AP closes on success without reboot |
| 15 profile store | Implemented | max 5, `ssid`/`password`/`last_success`, versioned + CRC; load is three-state Missing/Valid/Corrupt so a corrupt image is never mistaken for a first boot; a durable `initialized` marker distinguishes "never configured" from "deliberately empty" |
| 16–17 boot selection / strategy | Implemented | last-successful first, then insertion order; no proactive roam |
| 18 phone hotspot | Supported | no special protocol |
| 19–20 Known Networks / Forget | Implemented | list, Forget, Forget All (confirmed); never opens the AP |
| 21 build-time Wi-Fi compatibility | Implemented | one-time FIRST-RUN migration of `CONFIG_DEVICE_LAB_WIFI_SSID`, gated by `should_migrate_build_wifi` (Missing only); Forget All and corrupt stores cannot re-trigger it |
| 22–26 USB provisioning | Implemented | framed `WFP/1` protocol over the existing console; percent-encoded values; `wifi.add` is candidate-first and reports `result=VERIFIED/REJECTED/UNAVAILABLE/INVALID/STORAGE_ERROR`; candidate test explicitly disconnects the current STA before connecting; one Profile Store |
| 27 identity separation | Implemented | provisioning touches only SSID/password |
| 28 Portal security boundary | Implemented | no token/API/agent/filesystem/shell; passwords never returned; scanned SSIDs HTML-escaped for attribute and text; POST body capped at 512 B before allocation |
| 29 logging rules | Implemented | `wifi_secrets_test.py` enforces no password logs |
| 30 SoftAP lifetime | Implemented | success, Hold B cancel, 15-minute idle timeout with `Setup expired` |
| 31–32 relation to Agent / Approval | Implemented | Setup refused while an approval is pending; RUNNING is allowed |
| 33 QR renderer | Implemented | local, no cloud, bounded buffer, round-safe |
| 34 power | Implemented | AP + Portal stopped on exit; never a long-lived AP+STA |
| 35 failure UX | Implemented | distinct reason strings for each failure class |
| 36 out of scope | Respected | no BLE/NFC/WPS/SmartConfig/5 GHz/Enterprise/Mesh/cloud |
| 37 module boundaries | Implemented | policy and persistence live outside `board.cpp` |
| 38–39 host / firmware tests | Implemented | see `docs/stopwatch-0.7.0.md` |
| 40–42 physical acceptance | Pending | no device was flashed in this round |

## Post-flash provisioning

Flashing firmware does not carry Wi-Fi credentials. The device keeps them in
its own NVS store, and the build-time migration (section 21) only fires on a
genuine first boot — `should_migrate_build_wifi` returns true for a `Missing`
store only. An already-configured device therefore keeps its stored networks
across a reflash, and a freshly erased one would need the credentials again.

`tools/mac/provision-wifi.sh` closes that gap without baking any secret into
the firmware:

```bash
./tools/mac/device.sh provision-wifi [--ssid NAME] [--credentials PATH] [--yes]
```

It delegates to the same `wifi_ctl.py` `wifi.add` path used by the interactive
menu, so the candidate-first validation and the single Profile Store are
unchanged. Credentials are read from `<repo>/.env` (already git-ignored):

```text
DEVICE_WIFI_SSID=Tencent-LabWiFi
DEVICE_WIFI_PASSWORD=...
```

Security properties, consistent with section 28 (passwords are never returned):

- The password is never a command-line argument, so it cannot appear in `ps`
  output or shell history.
- It is never placed in the environment, so `/proc/<pid>/environ` is clean.
- It reaches the helper on stdin and is base64-framed, so an embedded newline
  cannot break the port/SSID/password split.
- The credentials file is parsed as `KEY=value` lines and never executed.
- Any text printed by the tool passes through `redact`, which strips
  `password=` values.

This is a host-side convenience only: it adds no protocol surface, no firmware
change and no new secret storage on the device.

## Deferred / not implemented

- Captive-portal auto-popup is best-effort; `http://192.168.4.1` is the
  guaranteed entry point.
- The Portal scan list shows raw scan results without BSSID de-duplication.
- No virtual keyboard, SSID entry or password entry on the watch itself.
