# 设备设置、NVS、RTC、Battery、Touch

结论按本地项目、已安装依赖和另外下载的官方 reference 区分。**没有实体设备读取结果，没有 physical PASS。** 官方资料抓取日期 2026-10-04，M5Unified reference 固定到 `fd40d58b8405ad1e7ed7afab548dab5e7cec5bbb`，它不是本项目的新依赖。

## 证据入口

| 证据 | 本轮实际读取的位置 |
|---|---|
| 亮度与当前面板 | `boards/m5stack_stopwatch/board.cpp:139,143,470`；原 production 目录的 `firmware/managed_components/M5GFX/src/lgfx/v1/panel/Panel_AMOLED.cpp:167` |
| 音量 | `boards/m5stack_stopwatch/audio_backend.cpp:34,418`；managed `espressif__esp_codec_dev/esp_codec_dev.c:90,309`、`include/esp_codec_dev.h:137`、`device/es8311/es8311.c:400` |
| 电源/震动 | managed `M5PM1/src/M5PM1.h:1728,1765,1337`、`M5PM1.cpp:3184..3244`；`board.cpp:431,1373,1390,1688,1708` |
| 当前 RTC / Touch | `board.cpp:476` 只 reset/probe；`idf_component.yml` 无 RX8130 驱动；managed `M5GFX/src/M5GFX.cpp:1714` 和 `lgfx/v1/touch/Touch_CSTxxx.cpp` |
| NVS | IDF v5.5.4 `components/nvs_flash/include/nvs.h` 的 `nvs_set_blob/get_blob/commit/erase_key/erase_all`；`firmware/main/app_main.cpp:14`；`firmware/partitions.csv` |
| 字体 | `transcript_font.h`、既有 `firmware/build/device_lab.elf`（只读）和 `evidence/glyphs.txt` |

离线 worktree 没有复制 managed_components；以上依赖从 `/home/lighthouse/esp32-dev/firmware/managed_components/` 只读检查。路径、哈希见 [evidence/sources.json](evidence/sources.json)。

## Brightness / Volume / Vibration

| 项目 | 源码确认 | 应用建议与限制 |
|---|---|---|
| Brightness | `uint8_t` 全范围 0..255；CO5300 的 0x51 命令发送 8bit 值，注释 MAX=0xFF；当前两处设置128 | UI 百分比10..100，10%步进，80%映射204；0用于受控灭屏，不放进普通菜单以免误黑屏。80%是本产品建议，不是 M5 官方默认 |
| 即时亮度 | setter 用 startWrite/write/wait/endWrite，无重启需求 | 在既有 display owner/锁下 Apply，不能 ISR、网络 callback 与 framebuffer 刷新并发写 SPI；软件具备即时写接口，亮度观感、耗电/热、闪烁和最低可读性仍需实测 |
| Volume | 当前 `AUDIO_VOLUME=90`。默认曲线1→−49.5dB、100→0dB；0特殊映射−96dB；高于终点饱和，负数没有用户级安全拒绝。ES8311 写 DAC32 register | UI 严格限定整数0..100，10步进；80是产品建议，曲线约−10dB，不能说是80%声压。0为曲线静音值，是否需硬件 mute 另测 |
| 即时音量 | 必须 output codec ready；否则 WRONG_STATE。setter 内部不提供跨 capture/playback 的应用锁，且此版本调用 codec->set_vol 后返回 OK，未透传底层寄存器失败 | 不承诺“播放中随时调节安全”。0.6a 先保存 desired volume，在 audio owner 的安全空闲点/下一次 output open 应用。保持 I2S/codec/streaming 主流程，待 Speech 收口后添加薄 setter 并测 pop、静音、重新 open 后一致性 |
| Vibration | M5IOE1 PIN9 / PWM channel0，5kHz；generic duty75、notify90；DONE/FAILED/APPROVAL 各自有调用路径 | 一个 preference gate 覆盖普通和通知 pulse；关掉后仍显示视觉通知。现有电机 owner 必须完成 stop，不能通过跳过 stop 造成持续震动；不新建另一套 task/模式。无硬件时按 unavailable 展示 |

当前 Long B 的非模态入口受 cloud_connected 门控，未来本地 Device/Display Settings 必须移到网络门控之前；只把 Agent remote 操作置灰。本轮没有改该判断。

## DevicePreferences：一个持久化模型

本项目为原生 ESP-IDF，不引入 Arduino Preferences 包装器。直接用已存在的 NVS partition（0x6000，24KiB），统一 namespace `wearable`，两个 blob key `prefs_a` / `prefs_b`；键名/namespace 均不超过 NVS 15 字符限制。

| 字段 | 建议默认 | 验证 / 所属 |
|---|---|---|
| brightness | 80 | 10..100整数，device |
| volume | 80 | 0..100整数，device |
| vibration_enabled | true | 0/1，device |
| home_screen | Watch | Watch/Agent/Monitor，device；演示可设 Agent |
| text_size | Normal | Compact/Normal/Large，device |
| theme | Dark | Dark/Dim，device |
| watchface | Agent Clock (0) | 当前仅0，device |

选中的 Agent target 继续由 Gateway 的每设备 TargetStore 管理，Speech Mode 继续 Gateway device+target scope。上述 blob 不存 session model/effort、Wi-Fi 密码或 gateway token，不为每个 setting 建一个独立文件/存储系统。

原型 `core.hpp` 显式序列化 21 bytes：magic4 + version1 + payload length1 + generation4 LE + payload7 + CRC32(前17bytes)4 LE。不可直接写 C++ struct（padding/enum ABI 不稳定）。未知 enum、非法布尔、越界、错长度、坏CRC均拒绝整条记录，然后尝试另一槽；无有效槽则用默认并显示恢复状态。future schema 的有效 envelope 使旧程序 read-only，不能静默覆盖未来偏好；generation=UINT32_MAX 同样拒绝普通写入，待明确维护重置。CRC不是认证，只用于数据损坏识别。

保存过程（设计；本轮没有 NVS backend）：

1. 读取两个槽，选最大 generation 的已支持有效记录。相同 generation 优先 A；合法 writer 每次只写另一槽，不会正常生成冲突。
2. 编辑阶段只改 draft。B Apply 进行字段验证，硬件 owner 应用；需要延迟应用的 volume 标为 pending，不冒充已生效。
3. 用 `generation+1` 编码到 inactive 槽，`nvs_set_blob` + `nvs_commit` + 读回验证；成功后才更新 last-saved、反馈 Saved。writer 只允许一个。不要在滑动/重复按钮/每帧写 flash。
4. commit/readback 失败保留上一槽；UI 显示未保存并可重试。若硬件已应用但持久化失败，要明确区分 applied 与 saved，重启恢复旧值。进入下一轮实现时测 NVS full/IO failure，不能只测 codec。

v1→v2 是**本 spike 的合成迁移 fixture**（v1前五字段，v2加 theme/watchface），当前 production 没有这样的历史格式。迁移时沿用有效旧值，为新字段补默认，经新槽成功写入才完成；不立即覆盖旧槽。生产 0.5 升 0.6 首次无 namespace，从建议默认开始。

**Reset device preferences** 只擦 `wearable` namespace 的偏好槽并 commit，成功后应用默认，不清 Wi-Fi、设备身份/凭据、Gateway TargetStore 或 Speech Mode。完全 Factory Reset 若以后提供，须明确列出擦除范围并单独两步确认；本轮不实现。当前 app_main 在 NVS 初始化遇 NO_FREE_PAGES/NEW_VERSION_FOUND 时会 `nvs_flash_erase()` 全分区；这不是偏好 reset API，未来需要专门 review 恢复策略，避免新设置功能依赖这一整盘擦除路径。

## Battery / USB / charging

本地 M5PM1 1.0.6 已有 `readVbat(uint16_t*)`（0x22/23、mV）、`readVin()`、`read5VInOut()`、`getPowerSource()`；错误必须返回 unknown，不能让0mV参与百分比计算。没有 fuel gauge/coulomb counter 或容量 percentage API。官方 M5Unified `Power_Class::getBatteryLevel` 的 M5PM1 路径也是电压换算再 clamp，并非硬件 SOC。

第一版建议显示电压，若要百分比用 `~72%` 并标 estimated，采用实测单节电池曲线、滤波和滞回；充电中/大音量负载下暂不以电压估算实时精确 SOC。不能直接从 450mAh 标称容量推剩余分钟。

充电可通过板级 `CHG_STAT` 得到：[官方 StopWatch pin map](https://docs.m5stack.com/en/core/StopWatch) 连到 M5PM1 GPIO2；[固定版本 Power_Class](https://github.com/m5stack/M5Unified/blob/fd40d58b8405ad1e7ed7afab548dab5e7cec5bbb/src/utility/Power_Class.inl#L2896) 的 StopWatch 路径低电平 charging。当前 standalone 库可用 `gpioGetInput(M5PM1_GPIO_NUM_2,&value)` / `digitalReadWithRes`，先审核配置是否是 GPIO input 和拉电阻，不把 `CHG_EN` 配置位当 charging 状态。不更改充电电流/充电使能来“读取”状态。

**供电来源存在版本矛盾，不能直接照抄：** standalone 1.0.6 把 register0x04 的0/1/2注释为5VIN/5VINOUT/BAT，cpp日志又写成None/USB/Battery；当前 M5Unified 同寄存器使用位图 vin=1、vinout=2、battery=4。两种库语义不一致。0.6a用成功读取的 VIN 电压表示“external power”，以 USB插拔/电池供电时记录 raw register+VIN+BAT 的真机结果解决版本差异。外部5V也不必然是USB，不在 UI 中误称USB数据连接。

建议 DeviceStatusProvider 每2..5s低频读电池，缓存带 valid/source/updated_at；共享既有 I²C owner，错误不抢占音频。Watch/Agent只渲染缓存，不能每帧阻塞读传感器。该调度是提案，未验证与当前 audio I²C 同时工作的时序。

## RX8130CE RTC

当前依赖中没有 RX8130CE headers/source，board 只 ACK 探测0x32；不能声称“现成 RTC API 已可在项目调用”。另外读取了 [M5Unified RX8130_Class.hpp](https://github.com/m5stack/M5Unified/blob/fd40d58b8405ad1e7ed7afab548dab5e7cec5bbb/src/utility/rtc/RX8130_Class.hpp) 与对应 `.inl`：有 `getDateTime`、`setDateTime`、`setAlarmIRQ`、`setTimerIRQ`、IRQ清理、`getVoltLow`。所以芯片/参考实现可读写时间和 alarm；仍需为现有 esp-idf I²C bus 接一个独立小驱动，不为这个功能整体引入 M5Unified。

参考读取7个寄存器从0x10开始，BCD、weekday one-hot和日期均做验证；读成功还不等于时钟可信，必须结合芯片振荡/低电压有效性标志。alarm 为分钟/小时/日或星期匹配；不是任意未来 UNIX 时间的一次性调度器。参考 `begin()` 会写控制寄存器，不能作为无副作用探针直接调用。设备掉电后的保时能力取决于板级 L1/backup 供电，不能凭“有RTC”保证完全断电后也走时。

时间策略：认证的 Gateway connection 提供 UTC epoch + fresh request_id/connection nonce + server timestamp，设备记录接收单调时钟和往返延迟；验证年份范围、时区和过期/异常跳变后设RTC。RTC存UTC，显示层转 Asia/Shanghai；断网用有效RTC，读失败退回最近同步基点+单调时钟并标 unsynced，冷启动无有效时间显示未知。重连/每日有限频次校时即可，不加复杂 NTP、闹钟调度或睡眠唤醒。本轮不发任何 RTC 写操作。

## CST820B Touch

本地 **M5GFX 0.2.21 已在 StopWatch autodetect 分支选择 `Touch_CST816S`**。读取0x02起的数据，points + 12-bit x/y，可得到原始坐标；不是只有外部推测兼容。参考用 IRQ GPIO13、SDA47/SCL48、400kHz、I²C_NUM_1、坐标0..233。本项目则自行构造面板、使用 I²C_NUM_0，并未 attach touch，不能直接复制参考 bus init，更不能按 468px 假定 raw 坐标也是468。要测角点/旋转/范围后映射实际468×466。

此类 `getTouchRaw()` 没有输出 tap/swipe 事件；可用坐标+时间在独立 GestureRecognizer 合成 tap、竖向滚动/横向返回，有限状态仅保留起点/终点/时间。手势阈值（如移位16px、tap<250ms、swipe>60px）只是待测起点。已下载并阅读 [M5 提供的 CST820B datasheet](https://m5stack-doc.oss-cn-shenzhen.aliyuncs.com/1242/CST820B_datasheet.pdf)：概述提到手势，但该PDF没有可据以接入的完整 gesture code 寄存器表，不编造 gesture ID。

建议0.6a默认touch关闭，0.6b末独立增强：tap只选择/聚焦非危险项；swipe对应 A NEXT / Back；不让无意触碰直接批准、发送语音、删除或创建工作区。实体按钮覆盖全部核心流程。需验证触摸唤醒、潮湿/袖口误触、IRQ/I²C争用和音频期间丢点。
