前置知识:了解 Activity 页面框架(第 8 章),理解 JSON 基本格式,了解 Web API 的概念。 本章目标:理解 CrossPoint Reader 如何用”一张表”驱动设备 UI、Web API 和持久化三套系统,如何安全地存储密码,以及如何让一个嵌入式设备说 12 种语言。
15.1 一个设置项的”一生”
在手机 App 或网页项目中,添加一个设置项通常意味着改好几个地方:UI 界面加一个控件、后端 API 加一个字段、数据库加一列、前端表单加一项校验……改漏一处就出 bug。 CrossPoint Reader 面临同样的问题——一个”屏幕边距”设置需要同时出现在三个地方:- 设备端 UI:用户在墨水屏上操作的设置页面
- Web API:通过浏览器远程修改设置
- JSON 文件:持久化到 SD 卡,断电后不丢失
15.2 声明式设置表
核心思想:一处定义,多处使用
所有设置项集中定义在一个函数里:SettingInfo 都包含了完整的”自我描述”:
| 字段 | 含义 | 例子 |
|---|---|---|
| 显示名(StrId) | UI 上显示的文字,支持多语言 | STR_SCREEN_MARGIN -> “屏幕边距” |
| 成员指针 | 指向 CrossPointSettings 结构体中的哪个字段 | &CrossPointSettings::screenMargin |
| JSON key | 持久化到 JSON 文件时的键名 | "screenMargin" |
| 分类(StrId) | 在设置页面中归入哪个分组 | STR_CAT_READER -> “阅读” |
| 约束条件 | 枚举的可选值、数值的范围和步长 | {5, 40, 5} 表示 5 到 40,步长 5 |
四种设置类型
CrossPoint Reader 定义了四种设置类型,覆盖了绝大多数场景:- Enum(枚举):有限的几个选项中选一个。比如休眠画面选”暗色”还是”亮色”。
- Toggle(开关):只有”开”和”关”两种状态。比如阳光褪色修复功能。
- Value(数值):在一个范围内选择数值。比如屏幕边距 5~40 像素,每次调整 5 像素。
- DynamicString(动态字符串):不直接绑定到结构体字段,而是通过自定义的 getter/setter 函数读写。比如 KOReader 的用户名存储在独立的 Store 中,读写逻辑比较特殊。
三处使用同一张表
有了这张表,三套系统就可以各取所需:分类 字段对设置项分组,根据 类型 选择合适的 UI 控件(枚举用选择框、数值用滑块、开关用复选框),从 StrId 获取当前语言的显示文本。
Web API:/api/settings 端点遍历同一张列表,GET 请求时用 JSON key 和 成员指针 自动构造 JSON 响应,POST 请求时从 JSON 中按 JSON key 读取值并写回结构体。
JSON 持久化:保存时遍历列表,用 JSON key 和 成员指针 生成 JSON 文件;加载时反向操作。
类比:这张表就像餐厅的菜单。服务员(UI)拿着菜单给客人点菜,厨房(持久化)按菜单做菜,外卖平台(Web API)也展示同一份菜单。新增一道菜只需要在菜单上加一行——不用分别通知服务员、厨房和外卖平台。
新增设置的工作量
假设你要给阅读器添加一个”翻页动画速度”的设置。你只需要做两件事:- 在
CrossPointSettings结构体里加一个字段uint8_t pageFlipSpeed; - 在
getSettingsList()里加一行:
15.3 设置持久化:JSON + 格式迁移
为什么选 JSON
CrossPoint Reader 的早期版本用二进制格式存储设置——每个字段按固定偏移量写入文件,读取时也按偏移量读出。这种方式速度快、体积小,但有三个致命缺点:- 不可读:用文本编辑器打开是一堆乱码,调试时无法直观看到设置值
- 不兼容:新版本增加了一个字段,所有偏移量都变了,旧文件直接报废
- 不可调试:用户反馈”我的设置丢了”,开发者无法查看 SD 卡上的设置文件来定位问题
加载逻辑与自动迁移
- 优先读 JSON:如果 JSON 格式的设置文件存在,直接加载
- 兜底读二进制:如果 JSON 不存在但旧的二进制文件存在,读取旧格式
- 自动迁移:把二进制的内容用 JSON 格式重新保存
- 备份旧文件:把旧的二进制文件改名为
.bak,而不是直接删除——万一迁移出了问题,还有恢复的机会
15.4 密码存储:MAC 地址 XOR 混淆
CrossPoint Reader 支持 WebDAV 同步和 KOReader 同步,这意味着设备需要存储用户的账号密码。但设置文件保存在 SD 卡上——SD 卡可以被拔出来,插到电脑上直接读取。如果密码以明文存储,安全性为零。混淆方案
这是加密吗?
不是。 这是混淆(Obfuscation),和加密有本质区别:- 加密需要密钥管理、安全随机数、经过验证的算法(如 AES)
- 混淆只是让数据”不那么容易被看到”,理论上可以被逆向
- 绑定物理设备:XOR 的”密钥”是 MAC 地址,每台设备不同。即使有人拔走 SD 卡,插到另一台设备或电脑上,直接读取配置文件看到的只是乱码。
- 防止明文泄露:至少不会出现在文本编辑器里一眼看到
"password": "123456"的尴尬场面。 - 成本极低:XOR 运算在 ESP32-C3 上几乎不消耗资源,不需要引入加密库。
经验法则:安全方案的选择要匹配威胁模型。银行系统需要硬件加密模块,而一台 10 块钱芯片的阅读器只需要”别把密码写在脸上”就够了。
15.5 国际化(I18n)系统
CrossPoint Reader 支持 12 种以上的语言界面,从中文、英文到日文、韩文、阿拉伯文。在嵌入式设备上实现多语言支持,面临的挑战和 Web 项目很不一样。核心设计:数组索引法
tr() 宏是代码中获取翻译文本的唯一入口。StrId 是一个枚举,每个翻译项有唯一的 ID。
查找逻辑极其简单:
const char* 数组,翻译项的 ID 就是数组下标。查找翻译的时间复杂度是 O(1)——一次数组索引操作,比 Web 项目中常见的 JSON 文件查找(哈希表 O(1) 但有额外开销)或 gettext 方案(字符串比较 O(n))都更快。
这也意味着每种语言的数组长度必须完全一致,每个位置对应同一个 StrId。如果某种语言少了一个翻译项,编译时就会报错——而不是运行时才发现”这个按钮没翻译”。
翻译数据的管理
- 零运行时解析:所有字符串在编译时就确定了,不需要运行时读 JSON 文件或解析翻译数据
- 类型安全:如果翻译项数量不对,编译时就能发现错误
- Flash 存储:字符串常量存放在 Flash 中(
const修饰),不占用宝贵的 RAM
与 Web 的 i18n 方案对比
| 方面 | Web(react-i18next 等) | CrossPoint Reader |
|---|---|---|
| 翻译文件格式 | JSON / PO / YAML | CSV -> 编译时生成 C 数组 |
| 运行时查找 | 哈希表 / 对象属性访问 | 数组下标 O(1) |
| 语言切换 | 动态加载新的 JSON 文件 | 切换数组指针,即时生效 |
| 缺失翻译处理 | 运行时 fallback 到默认语言 | 编译时报错,不允许缺失 |
| 内存开销 | 所有翻译加载到内存 | 所有翻译存放在 Flash,零 RAM 占用 |
15.6 设置界面与 Web API 双通道
CrossPoint Reader 的设置可以通过两种方式修改:在设备上操作墨水屏 UI,或者连接 WiFi 后通过浏览器访问 Web 页面。两种方式操作的是同一份数据。设备端:SettingsActivity
SettingsActivity 是一个标准的 Activity(第 8 章介绍的页面框架)。它的render() 方法会遍历 getSettingsList() 返回的列表:
CrossPointSettings 结构体中对应的字段(通过成员指针),然后调用 saveToFile() 持久化。
Web 端:/api/settings
当设备连接 WiFi 后,内置的 Web 服务器(第 12 章)会暴露/api/settings 端点:
- GET /api/settings:遍历设置列表,读取每个字段的当前值,构造 JSON 响应返回
- POST /api/settings:解析请求体中的 JSON,遍历设置列表,按 JSON key 匹配并更新对应字段
修改同步
由于两种通道操作的是同一个CrossPointSettings 结构体实例,Web 端修改的设置会立即反映到设备端,反过来也一样。这种”单一数据源”(Single Source of Truth)的设计避免了数据不一致的问题。
本章要点
- 声明式设置表(
getSettingsList())实现了”一处定义,多处使用”——设备 UI、Web API 和 JSON 持久化共用同一张设置描述表。新增设置只需加一行。 - 设置支持四种类型:Enum(枚举)、Toggle(开关)、Value(数值范围)、DynamicString(自定义读写逻辑)。
- 设置从二进制格式迁移到 JSON,获得了可读性、兼容性和可调试性。旧格式自动迁移,用户无感知。
- 密码混淆使用 MAC 地址 XOR + Base64 编码。不是加密,但对嵌入式场景足够——密码绑定到物理设备,SD 卡被拔走后无法直接读取明文。
- 国际化系统基于编译时生成的
const char*数组,翻译查找是 O(1) 的数组索引操作。12+ 种语言的翻译数据存放在 Flash 中,零 RAM 占用。 - 设备端 UI 和 Web API 是操作设置的两个通道,它们共享同一个数据源(
CrossPointSettings结构体),修改即时同步。 - 声明式设计的核心价值:代码改动越少,引入 bug 的风险越低——在无法”热修复”的嵌入式环境中,这一点至关重要。
附录预告:附录 A 将以速查表的形式,汇总整本书中出现的所有关键设计模式——从 Activity 栈到 MAC XOR 混淆,从分块帧缓冲到链接器劫持,看看 380KB 的小芯片上藏了多少精妙的工程智慧。