Files
gaoguangpu/code_review_report.md
2026-08-18 13:34:21 +08:00

214 lines
15 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 代码审查报告 — 波长系数全精度修复及周边问题
- **审查日期**:2026-08-12
- **审查对象**:`src/main.cpp``bochangxishu` 全精度修复(占位符 + 序列化后字符串替换)、ArduinoJson 7.4.2 → 6.21.6 降级,以及 git diff(`HEAD~1..HEAD` + 工作区改动)触及的周边代码
- **行号基准**:当前工作区文件
- **审查方式**:逐行 diff 扫描 + 跨文件数据流追踪 + 行为变更审计 + 多角度并行核查(5 个后台 agent 交叉验证)
---
## 结论摘要
| # | 严重度 | 位置 | 问题 | 类别 |
|---|--------|------|------|------|
| 1 | 🔴 严重 | [main.cpp:2623-2676](src/main.cpp#L2623-L2676) | **精度修复把 SD 卡存储的标定系数覆盖成出厂系数** | 本次改动引入的回归 |
| 2 | 🟠 高 | [main.cpp:416](src/main.cpp#L416) / [422](src/main.cpp#L422) | `=` 写成 `==`,manual 采集窗口永不生效,且把 work_time 篡改为 "gps" | 既有 bug(随 diff 触及) |
| 3 | 🟠 高 | [main.cpp:795](src/main.cpp#L795) | 未持有互斥锁就 `xSemaphoreGive`,可破坏互斥 | 并发 |
| 4 | 🟠 高 | [main.cpp:960-961](src/main.cpp#L960-L961) | OTA 挂起唯一喂狗任务 Task1,>10 分钟 OTA 中途 panic 重启 | WDT |
| 5 | 🟠 高 | [main.cpp:566-570](src/main.cpp#L566-L570) | open_4G_mode 阻塞网络调用导致任务看门狗饥饿,重启循环 | WDT |
| 6 | 🟠 高 | [main.cpp:892](src/main.cpp#L892) | 上传净荷无锁撕裂读(两任务并发读写 ~8KB 结构体) | 并发 |
| 7 | 🟡 中 | [main.cpp:2663-2676](src/main.cpp#L2663-L2676) | 字符串替换方案脆弱(replace 静默失效 → 200000 被写入文件) | 本次改动 |
| 8 | 🟡 中 | [main.cpp:875-880](src/main.cpp#L875-L880) | `http_head.sn/version` 为 char[20],长字符串越界写 | 内存 |
| 9 | 🟡 中 | [main.cpp:734](src/main.cpp#L734) | 每日重启死代码:`day_count` 从不递增,`esp_restart()` 不可达 | 逻辑 |
| 10 | 🟡 中 | [main.cpp:566](src/main.cpp#L566) | 4G 模式每 ~10s 无条件全量 getnetData+get_GPS,网络轮询量 ×12 | 性能 |
| 11 | 🟡 中 | [src/SensorIS11.cpp:85](src/SensorIS11.cpp#L85) | `new char[]` 用标量 `delete` 释放(delete/delete[] 不匹配,UB) | 内存 |
| 12 | 🟡 中 | [src/gsmm_mqtt.cpp:163](src/gsmm_mqtt.cpp#L163) | 4 处复制粘贴的握手轮询,且 180s 超时只守前半段,`portMAX_DELAY` 无界 | 可维护性 |
| 13 | 🟡 中 | [src/gsmm_mqtt.cpp:124](src/gsmm_mqtt.cpp#L124) | 共享 20s 客户端超时,弱网下上传/OTA 单块停滞即断连 | 网络 |
| 14 | 🟢 低 | [main.cpp:2628-2631](src/main.cpp#L2628-L2631) | b0-b3 系数未被修复覆盖,仍 9 位小数截断(修复只做了一半) | 本次改动 |
| 15 | 🟢 低 | [src/gsmm_mqtt.cpp:77](src/gsmm_mqtt.cpp#L77) | reconnect() 无退避,断连时最多阻塞 ~200s | 网络 |
| 16 | 🟢 低 | [main.cpp:366](src/main.cpp#L366) | Task1 栈 15KB 承载了原 23KB 的 4G 调用链,有溢出风险 | 资源 |
| 17 | 🟢 低 | [main.cpp:2663-2676](src/main.cpp#L2663-L2676) / [2718-2731](src/main.cpp#L2718-L2731) | 替换块两个分支逐字重复,后续修改需同步两处 | 可维护性 |
---
## 一、本次精度修复直接引入的问题(最优先)
### 🔴 #1 存储标定被出厂系数覆盖(本次改动的回归)
**位置**:[main.cpp:2623-2626](src/main.cpp#L2623-L2626) 与 [main.cpp:2664-2676](src/main.cpp#L2664-L2676)
**启动时序**:
```
277 initSensor() → SensorInfo.a1..a4 = 传感器出厂系数
294 sys_info_init() ← 此刻 SensorInfo 仍是出厂值!
2619-2626 被注释掉的旧代码原本保留 doc 中的存储值,现被 200000 占位符取代
2638-2641 doc["bochangxishu"]["a0..a3"] 被读入 guangpu_bochang(存储标定加载成功)
2664-2676 替换块却用 SensorInfo.a1..a4(= 出厂值)写回 sys_sd_doc 并序列化
319-344 之后才把 guangpu_bochang → SensorInfo(运行时实际用的是存储标定)
```
**后果**:
- 只要 SD 卡上存在 system_info.json,每次开机都会把 `sys_sd_doc["bochangxishu"]["a0..a3"]` 重置为**出厂系数**,而不是 SD 里的存储标定。
- 下一次任意配置保存(如 [main.cpp:786](src/main.cpp#L786) Task2 写盘)会把出厂值固化到文件,存储标定**永久丢失**。
- `get_bochangxishu`([main.cpp:1793-1808](src/main.cpp#L1793-L1808))读到的是出厂值,而运行时计算波长用的是 `guangpu_bochang`(存储值)→ **主机读到的系数与实际计算用的系数不一致**
被注释掉的旧代码 [main.cpp:2619-2622](src/main.cpp#L2619-L2622)(`sys_sd_doc[...] = doc[...]`)恰恰是保留存储标定的正确做法——它的问题只是序列化时丢到 9 位小数,而不是数值错误。
**修复建议**(文件存在分支):
```cpp
// 替换块的数据源从 SensorInfo(出厂)改为 guangpu_bochang(存储标定)
const double a[4] = { is11Sensor->guangpu_bochang.a0,
is11Sensor->guangpu_bochang.a1,
is11Sensor->guangpu_bochang.a2,
is11Sensor->guangpu_bochang.a3 };
```
首启分支([main.cpp:2700-2703](src/main.cpp#L2700-L2703)、[2718-2731](src/main.cpp#L2718-L2731))用 `SensorInfo` 是**正确**的(此时无存储值,出厂系数即默认标定),无需改动。
### 🟡 #7 字符串替换方案本身脆弱
**位置**:[main.cpp:2663-2676](src/main.cpp#L2663-L2676)
- `String::replace` 是**全局替换**:任何字段若恰好序列化为 `:200000`(数值 200000 的字段),会被一并改成该系数。
- 替换依赖 ArduinoJson 6.21.6 的**精确输出格式** `"a0":200000`。若未来库升级/格式化变化(冒号后空格、`200000.0`、科学计数法),replace 会**静默失效**,占位符 200000 被原样写入文件;下次开机 [2638](src/main.cpp#L2638) 把它读回 `guangpu_bochang.a0`,每个波长偏移 ~200000nm。
- 方案的"全精度"目标本身有悖论:降级到 6.21.6 正是为了让这套 replace 匹配其输出;一旦动库版本,修复即失效。
**建议**:文件存在分支不需要占位符——数据源是内存里的 double,序列化 9 位截断后,直接用 key 锚定 replace 那 4 个 9 位字符串即可;或接受 9 位小数(见下文精度量化)。
### 🟢 #14 b0-b3 未覆盖
文件存在分支里 b 系数([main.cpp:2628-2631](src/main.cpp#L2628-L2631))仍走 ArduinoJson 默认 9 位小数序列化,同样丢精度——修复只做了一半。若要彻底,应对 b0-b3 采用相同手段。
### 关于"丢精度"的量化提醒
本次修改前我已测算:对这些量级的系数,a0 误差 ~1e-8nm(2047 像元处)、a3 误差 ~5e-5nm,远低于传感器自身光谱分辨率。**9 位小数在物理上可能已足够**,如果确认存储标定不被覆盖才是关键,可以考虑干脆不做字符串替换,只修复 #1 的数值来源。
---
## 二、并发 / 任务问题
### 🟠 #2 `work_time` 赋值写成比较
**位置**:[main.cpp:416](src/main.cpp#L416) / [422](src/main.cpp#L422)
```cpp
if(sys_sd_doc["work_time"] = "gps") // 应为 ==
else if(sys_sd_doc["work_time"] = "manual") // 应为 ==
```
`JsonVariant::operator=` 返回非空 variant,`bool` 转换恒为 true → `gps` 分支**无条件**进入,manual 采集窗口(用户配置的 start_time/stop_time)**永不生效**。副作用:每次 Task0 迭代都把 `sys_sd_doc["work_time"]` 写成 "gps",下次保存后用户设的 "manual" 被固化篡改。已在 [430](src/main.cpp#L430) 之前执行,并直接跳过 manual 分支。修复:改 `==`
### 🟠 #3 Task2 在未持有互斥锁时 Give
**位置**:[main.cpp:781-795](src/main.cpp#L781-L795)
`xSemaphoreGive(xMutexInventory)``if(xSemaphoreTake(...) == pdPASS)` **块外**。若 take 超时(其他任务持有锁超过 100s,如 Task1 阻塞在网络调用中),Task2 会 Give 一个自己不持有的锁 → 释放别人的临界区,两个任务并发进入,`sys_sd_doc` / 写盘互相踩踏;在 FreeRTOS 上属于未定义行为,可能触发互斥锁所有权断言。
**修复建议**:
```cpp
if(xSemaphoreTake(xMutexInventory, timeOut) == pdPASS)
{
...序列化写盘...
save = false;
xSemaphoreGive(xMutexInventory);
}
```
### 🟠 #4 OTA 挂起唯一喂狗任务 → 中途重启
**位置**:[main.cpp:960-961](src/main.cpp#L960-L961)、[550](src/main.cpp#L550)、[370-376](src/main.cpp#L370-L376)
Task1 是唯一注册任务看门狗的任务(`esp_task_wdt_add(NULL)`,10 分钟超时,`trigger_panic=true`),而 OTA_task 在下载+烧录期间 `vTaskSuspend(Task1_Handler)`。慢速 4G 下载 >10 分钟 → WDT panic → `CONFIG_ESP_PANIC_HANDLER_REBOOT` 中途重启 → **固件分区半写,设备可能变砖**。同时 open_4G_mode 内的 getnetData/get_GPS/UpdateData 各自最多 180s,叠加可超 10 分钟,造成假阳性重启(见 #5)。
### 🟠 #5 任务看门狗被当成万能补丁
`trigger_panic=true` + 双核 `idle_core_mask` + 10 分钟超时,而唯一的喂狗任务 Task1 恰恰在做全部长阻塞网络 I/O([main.cpp:566-570](src/main.cpp#L566-L570))。设计自相矛盾:网络慢但活着,也会被看门狗判死刑。4G 链路拥堵时设备会进入"每 ~10 分钟重启一次"的循环,4G 模式无法恢复。
### 🟠 #6 上传净荷撕裂读
**位置**:[main.cpp:892](src/main.cpp#L892)、[433](src/main.cpp#L433)
Task0 采集写入 `IS11_datastruct_fanshelv`(无锁,[433](src/main.cpp#L433) `get_fanshelv()`),Task1 在 open_4G_mode 里先做最长 180s 的网络同步再 `memcpy` 这个结构体(也无锁,仅在 `up_data` 标志上加锁)。两个核并发读写 ~8KB 结构体 → 上传的数据包可能是**两次测量的混合**(头部/波长被撕裂),服务器端静默收到错误数据。
---
## 三、GSM / MQTT 子模块
### 🟡 #12 握手轮询 4 处复制 + 超时只守一半
**位置**:[gsmm_mqtt.cpp:163](src/gsmm_mqtt.cpp#L163)、[230](src/gsmm_mqtt.cpp#L230)、[348](src/gsmm_mqtt.cpp#L348)、[550](src/gsmm_mqtt.cpp#L550)
`while(uxBits & mqtt_stop_bit){ ... if(cout==180*1000) return; vTaskDelay(1); }` 被复制 4 次,且已分化(有的返回 false、有的返回 "-1"、有的 `esp_restart`)。180s 超时只守卫 mqtt_stop_bit 前半段,紧接的 `xEventGroupWaitBits(Http_start_bit, ..., portMAX_DELAY)` **无界**——若 mqtt 任务僵死,调用方永久阻塞。建议提取单一 `wait_mqtt_free()` helper。已核实本板 `CONFIG_FREERTOS_HZ=1000`,`vTaskDelay(1)=1ms`,故 180×1000 确实 ≈ 180s(早期"30 分钟"说法不成立,特此更正)。
### 🟡 #13 共享 20s 客户端超时
**位置**:[gsmm_mqtt.cpp:124](src/gsmm_mqtt.cpp#L124)
同一个 Client 对象同时被 PubSubClient(MQTT)和 HttpClient(HTTP)使用,`setTimeout(1000*20)` 一并生效。弱网下 1024 字节单块写停滞 >20s → `http->write()` 返回 -1 → [UpdateData 返回 false](src/gsmm_mqtt.cpp#L217) 而 open_4G_mode 忽略返回值,上传静默丢失;OTA 下载同理中断。
### 🟢 #15 reconnect 无退避
断连时最多 10 次 × 20s = ~200s 阻塞在重连里,期间不服务 mqtt_stop_bit/Http_start_bit 握手,Task1 的 open_4G_mode 干等 180s 后超时,当天数据可能丢弃。
---
## 四、内存 / 资源
### 🟡 #11 `delete` / `delete[]` 不匹配
**位置**:[SensorIS11.cpp:85](src/SensorIS11.cpp#L85)
[74](src/SensorIS11.cpp#L74) `result = new char[retlenth]` 用标量 `delete result` 释放(同一函数另一处 [67](src/SensorIS11.cpp#L67) 正确用了 `delete[]`)。UB,可能损坏堆元数据。改为 `delete[] result;`
### 🟡 #8 char[20] 越界写
**位置**:[main.cpp:875-880](src/main.cpp#L875-L880)
`memcpy(http_head.sn, sn.c_str(), sn.length())` + `http_head.sn[sn.length()]='\0'` 无长度校验。当序列号 ≥ 20 字符时越界写,破坏相邻的 http_head 字段。现有序列号(如 "TFNSP44250004")较短暂安全,但 `version` 取自 [874](src/main.cpp#L874) 的 `sys_sd_doc["version"]`,长版本号即可触发。建议加 `sn.length() < sizeof(http_head.sn)` 保护。
### 🟢 #16 Task1 栈吃紧
Task1 栈 15KB([main.cpp:366](src/main.cpp#L366)),却承载了原 task_4G_mode(23KB 独立任务)的全部调用链:open_4G_mode → getnetData/get_GPS/UpdateData(HttpClient/TinyGsm/String 深层栈帧)。深栈路径下可能溢出,而溢出任务恰好是喂狗任务,表现为随机 panic 重启。
---
## 五、可维护性
- 替换块在两个分支逐字重复([2663-2676](src/main.cpp#L2663-L2676) / [2718-2731](src/main.cpp#L2718-L2731))——修 #1 时需同步两处,建议提取成函数。
- `day_count` / `count` / `running_count` 三重计数冗余,`day_count` 从未递增(#9),已基本失去可推理性。
- [main.cpp:556-562](src/main.cpp#L556-L562) `up_data` 全局标志在 Task1 每 1s 循环被清零,但只在 `running_count==10` 时才消费 → 两窗口之间的触发被静默丢弃。
---
## 六、已验证的"非问题"(避免误改)
| 项 | 结论 |
|----|------|
| ArduinoJson 7.4.2 → 6.21.6 降级 | **正确且必要**:v7 移除了 `DynamicJsonDocument`,HEAD 上 pin 的 7.4.2 本来就编译不过。代码全用 v6 原生 API,无兼容问题。 |
| `reconnect()` 返回值 | 唯一调用方 gsmm_mqtt_loop_task 正确消费 `reconnect_flag`。 |
| `updata_buff` 大小 | `sizeof(http_head)+sizeof(IS11_datastruct)` 计算正确,无溢出。 |
| `save` 标志 | 读写在互斥锁内完成,volatile 裸读在本核上无害。 |
| 180×1000 超时 | 已核实 `CONFIG_FREERTOS_HZ=1000`,即 180s,不是 30 分钟。 |
---
## 七、修复优先级建议
| 优先级 | 项 | 动作 |
|--------|----|------|
| P0 | #1 存储标定被覆盖 | 文件存在分支替换块数据源改 `guangpu_bochang`(或恢复旧代码+全精度替换);**上线前必改** |
| P0 | #2 work_time 赋值 | 改 `==`;否则 manual 模式完全失效且配置被篡改 |
| P0 | #3 互斥锁 Give 越权 | 移入 take 成功分支内 |
| P1 | #4/#5 WDT | OTA 期间喂狗或临时解注册;open_4G_mode 内周期性 `esp_task_wdt_reset`;或缩小超时并把 panic 改为可恢复 |
| P1 | #6 上传撕裂 | 采集与 memcpy 之间加锁,或改为单任务快照拷贝 |
| P2 | #8/#11/#12/#13 | 加长度保护、改 `delete[]`、抽取 wait helper、上传用独立超时 |
| P2 | #7/#14 | 决定替换方案的取舍(见 #1 修复建议与精度量化) |
---
## 附录:本次精度修复的原始问题回顾
- **问题根源**:ArduinoJson(v7.4.2 与 6.21.6 一致)序列化 double 时**硬编码最多 9 位小数**(内部 `maxDecimalPart=10^9``uint32_t` 上限约束,10^15 会溢出),并非赋值丢精度——`ARDUINOJSON_USE_DOUBLE=1` 下 double 在内存中是精确的。
- **当前方案**:序列化前置 `200000` 占位整数 → `serializeJson` → 按 `"aX":200000` 锚定字符串替换为 `snprintf("%.15g")` 的全精度系数 → 恢复 `sys_sd_doc` 真实值。`%.15g`(而非 `String(v,15)`)才能同时保住 ~1e-9 小系数与 ~3e2 大系数的有效位数。
- **遗留**:写入路径 [main.cpp:786](src/main.cpp#L786)(配置保存)与 [main.cpp:1226](src/main.cpp#L1226)(GPS 更新保存)仍直接 `serializeJson(sys_sd_doc,...)`,bochangxishu 依旧 9 位截断——本次只修了 sys_info_init 的启动/首启两条路径,这两处如需全精度需同样处理。