- 0.1.5/0.1.6:刷机模式(flash_mode,最多 2 路,60 分钟可加钟)——窗口内顶高按 4450 mV 绝对值判、不断 VBUS - 0.1.7/0.1.8:反灌断 VBUS 阈值放宽到 -300 mA(adb reboot bootloader 过渡实测 -208 mA) - 0.1.9:WiFi 最多存 4 组,连不上轮换;NVS v1 自动迁移并双写(OTA 回滚仍能上网) - 0.1.10:热插拔进来的刀片重新初始化 PCA9555 + INA226(否则 VBUS 常开、电流恒读 0) - 0.1.11:i2cbus 嵌套检测改看本任务持锁(原全局标志让 key 随机回 busy、vbus 随机回 io) - 0.1.12:硬重启先断 VBUS,开机校验通过 60 s 后再接回(否则假重启 / 误判没开机 / 进关机充电) - 0.1.13:这一路被安全判据断电时一并退出刷机模式(事件 21 arg 254) 26 ctest 全绿;新增用例均先在旧代码上确认变红。板 B 已 OTA 到 0.1.13-c5 并实测。 Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
151 行
7.9 KiB
C
151 行
7.9 KiB
C
/* 单一命令处理器(A12,需求 §10.8)。
|
||
|
||
§10.8 的第一条规矩:**命令处理器只有一个**。WiFi(A14)与 USB CDC(A13)只是传输,
|
||
各自把「一行 JSON」交到这里、把「一行 JSON」带回去。鉴权之外的全部约束 ——
|
||
限幅、空槽拒绝、序号判重、拒绝记事件 —— 都只写在本模块里,**绝不写进适配器**:
|
||
以后加第三条链路时漏掉一处,功能测试是测不出来的。
|
||
|
||
---- 在哪个任务里跑 ------------------------------------------------------
|
||
**只许在执行器任务里调 `cmd_handle_line()`**(app_main.c 的 `task_actuator`
|
||
从命令队列里取行、调它、把回应交还给链路)。理由:`t_actuator_*` 的状态机没有锁,
|
||
它们的请求函数与 `t_actuator_step()` 必须在同一个任务里;判重窗口同理。
|
||
把处理器放进链路任务再给执行器加锁,等于为「两条链路」付出「全部状态机加锁」的代价。
|
||
宿主机上没有任务,测试直接调。
|
||
|
||
---- 协议(写回需求 §10.8「A12 定稿的线上格式」)------------------------
|
||
请求:`{"seq":17,"cmd":"key","rail":3,"keys":"pwr","ms":10000}`
|
||
回应:`{"seq":17,"ok":true}` / `{"seq":17,"ok":false,"err":"range"}`
|
||
判重命中时回**第一次的结论**并加 `"dup":true`,不再执行。
|
||
|
||
⚠️ **判重只对「会动东西的命令」**(`CMD_MUTATES`),而且这些命令**必须带 seq**:
|
||
`status` / `events` 本身幂等,重发一遍就该拿到新数据,不进窗口。
|
||
⚠️ 窗口是**全局的、不分链路**(§10.8 规矩 3),按「值相等」判,不按「不大于最大值」判 ——
|
||
后者在上位机重启、序号从头数的那一刻会把全部新命令判成重复。
|
||
上位机因此要保证:**同一个 seq 只用于同一条命令的重传**;想「再试一次」就换新 seq。
|
||
⚠️ 窗口在 RAM 里,板子重启就清空。「上位机重传的那一刻板子恰好重启」会让命令
|
||
执行两遍 —— 但重启本身已经松开了所有键、关掉了所有输出(SR-6),第二遍落在的是
|
||
一块刚启动的板子上,不是「第二遍按在刚开机的手机上」那种情形。
|
||
回应里没有 boot 编号;上位机要识别重启就看 `status` 的 `up`(上电毫秒)有没有变小。 */
|
||
#ifndef CMD_H
|
||
#define CMD_H
|
||
|
||
#include <stdbool.h>
|
||
#include <stddef.h>
|
||
#include <stdint.h>
|
||
|
||
#include "t_safety.h"
|
||
#include "t_thermal.h"
|
||
|
||
#ifdef __cplusplus
|
||
extern "C" {
|
||
#endif
|
||
|
||
/* ---- 线上编号:进 flash 的,冻结 ----------------------------------------
|
||
⚠️ 这两张表的数字会写进事件日志(`LOG_EV_CMD_REJECT` 的 arg / current_ua、
|
||
`LOG_EV_REBOOT` 的 arg),和 `log_ev_t` 一样**只许在末尾追加**。
|
||
`test_cmd_numbers_are_frozen()` 把它们钉死了。 */
|
||
typedef enum {
|
||
CMD_NONE = 0,
|
||
CMD_STATUS = 1,
|
||
CMD_SET_VOLTAGE = 2,
|
||
CMD_POWER_CYCLE = 3,
|
||
CMD_KEY = 4,
|
||
CMD_EDL = 5,
|
||
CMD_VBUS = 6,
|
||
CMD_FAN = 7,
|
||
CMD_EVENTS = 8,
|
||
CMD_POWER = 9, /* A12 新增,§10.8 原表没有:管理界面要能把一台手机停下来 */
|
||
CMD_WIFI = 10, /* A14/C1:写 WiFi 凭据(只收 USB) */
|
||
CMD_MQTT = 11, /* A14/C1:写 broker 地址 / 账号 / 遥测周期(只收 USB) */
|
||
CMD_NET = 12, /* A14:网络状态(只读,不回显密码) */
|
||
CMD_OTA = 13, /* C2:从 URL 下载固件、校验 sha256、切启动分区(ota.h) */
|
||
CMD_FLASH_MODE = 14, /* 0.1.5:刷机模式——VBUS 通着时允许手机充电、不断 USB(chgmode.h) */
|
||
CMD_COUNT,
|
||
} cmd_id_t;
|
||
|
||
typedef enum {
|
||
CMD_OK = 0,
|
||
CMD_ERR_PARSE = 1, /* 不是一行合法的扁平 JSON */
|
||
CMD_ERR_UNKNOWN = 2, /* 没有这条命令 */
|
||
CMD_ERR_NO_SEQ = 3, /* 会动东西的命令没带 seq(或 seq 不在 1~2^32−1) */
|
||
CMD_ERR_ARG = 4, /* 缺参数 / 参数类型不对 / 键名写错 */
|
||
CMD_ERR_RANGE = 5, /* 参数越界(SR-13 就落在这一类) */
|
||
CMD_ERR_EMPTY = 6, /* 该路刀片不在位(`PRSNT#` 高)—— 一律拒 */
|
||
CMD_ERR_BUSY = 7, /* 这一路正在跑另一个序列(§10.6 不可重入) */
|
||
CMD_ERR_STATE = 8, /* 当前状态下不许(例:eFuse 关着时开 VBUS) */
|
||
CMD_ERR_IO = 9, /* 下发到器件失败(I²C) */
|
||
CMD_ERR_OVERFLOW = 10, /* 回应装不进缓冲 —— 本模块的 bug,不该出现 */
|
||
CMD_ERR_LINK = 11, /* 这条命令不许从这条链路来(经 WiFi 改 WiFi 配置) */
|
||
CMD_ERR_COUNT,
|
||
} cmd_err_t;
|
||
|
||
/* 哪条链路来的。进 `LOG_EV_REBOOT` 的 arg 高 4 位。§10.8:两条链路的命令语义完全一致,
|
||
**唯一的例外**是 `wifi` / `mqtt` 两条配置命令只收 USB(netcfg.h:经 WiFi 改 WiFi 配置,
|
||
一次手误就把唯一的远程通道弄断了)。 */
|
||
typedef enum {
|
||
CMD_LINK_TEST = 0,
|
||
CMD_LINK_USB = 1,
|
||
CMD_LINK_WIFI = 2,
|
||
} cmd_link_t;
|
||
|
||
/* `LOG_EV_REBOOT` 的 arg:bit0 = 1 硬(断电)/ 0 软(长按电源键),bit4~7 = 链路。 */
|
||
#define CMD_REBOOT_ARG(hard, link) ((uint8_t)(((hard) ? 1u : 0u) | ((unsigned)(link) << 4)))
|
||
|
||
/* 判重窗口条数。上位机的重传间隔是秒级,窗口 32 条对应的是「几十条命令之内的重传」,
|
||
够用;再大只是多占 RAM,而且窗口越大,上位机序号回绕时撞车的概率越高。 */
|
||
#define CMD_SEQ_WINDOW 32
|
||
|
||
/* 一次 `events` 最多回几条。每条约 50 字节,8 条 ≈ 400 字节,和 status 一个量级。 */
|
||
#define CMD_EVENTS_MAX 8
|
||
|
||
/* 回应缓冲的建议大小。status 满配实测 ≈ 900 字节(`test_status_fits_in_out_max`)。 */
|
||
#define CMD_OUT_MAX 1536
|
||
|
||
/* 请求行的最大长度(含结尾 0)。超长的行由适配器整行丢弃(A13 的分帧规矩)。 */
|
||
#define CMD_LINE_MAX 256
|
||
|
||
/* `power` 命令改了某一路的「意图」(A15)。启动时按这个意图逐路恢复(§10.9 第 4 步
|
||
「上次的每路使能状态」),所以 app_main 在这里把它存进 NVS。
|
||
⚠️ 只有 `power` 命令改意图:判据跳闸、硬重启、mux 失联全断都**不改** ——
|
||
那些是事故,不是操作员的决定;重启之后按意图恢复,跳闸的那一路再由判据管。
|
||
在执行器任务里同步调(可以写 flash,毫秒级,命令本来就稀)。 */
|
||
typedef void (*cmd_intent_fn)(void *ctx, int rail, bool on);
|
||
|
||
typedef struct {
|
||
const safety_snap_t *safety; /* NULL = status 里不报这一块 */
|
||
const thermal_snap_t *thermal; /* NULL = 同上 */
|
||
cmd_intent_fn intent; /* NULL = 不记意图(宿主机测试) */
|
||
void *intent_ctx;
|
||
} cmd_env_t;
|
||
|
||
/* 清判重窗口、记下两份快照的位置。幂等。
|
||
⚠️ 快照属于别的任务(巡检 / 热管理),这里是**跨任务只读**:status 读到的可能是
|
||
半圈新半圈旧的数,对遥测无害;本模块**从不拿快照里的数做判断**
|
||
(判断只用 `rail_efuse_is_on()` / `slot_present()` 这类单字节的影子)。 */
|
||
void cmd_init(const cmd_env_t *env);
|
||
|
||
/* 处理一行、写回一行(不带换行)。返回回应长度(不含结尾 0)。
|
||
`out` 至少 64 字节才保证装得下任何一条错误回应。 */
|
||
size_t cmd_handle_line(cmd_link_t link, const char *line, char *out, size_t out_len);
|
||
|
||
const char *cmd_name(cmd_id_t id); /* 线上用的名字,"set_voltage" 这种 */
|
||
const char *cmd_err_str(cmd_err_t e);
|
||
|
||
/* 观测:累计处理几行 / 判重命中几次 / 拒绝几次(含解析错)。遥测与测试用。 */
|
||
typedef struct {
|
||
uint32_t lines;
|
||
uint32_t dups;
|
||
uint32_t rejects;
|
||
} cmd_stats_t;
|
||
void cmd_stats(cmd_stats_t *st);
|
||
|
||
/* 刷机模式到期清理 + 记事件(LOG_EV_CHG_MODE,arg = CHG_EV_EXPIRED)。**只许在执行器任务里调**
|
||
(chgmode.h:窗口表只有这个任务写),app_main 每转一圈调一次。 */
|
||
void cmd_chgmode_tick(uint32_t now);
|
||
|
||
#ifdef __cplusplus
|
||
}
|
||
#endif
|
||
|
||
#endif /* CMD_H */
|