文件
phonerail/firmware/main/cmd.c
T
zhou yang和Claude Opus 5.5 ae1aad3b7c 固件 C2:OTA 双槽 + 失败回滚(已上板实测正常 / 哈希错 / 回滚三条路径)
- 分区重排(16 MB,nvs 0x9000 不动):ota_0 / ota_1 各 3 MB、otadata 0x610000、evlog 挪到 0x612000;
  flash 大小改正为 16 MB;开 CONFIG_BOOTLOADER_APP_ROLLBACK_ENABLE
- ota.{h,c}(宿主机可测):URL / sha256 校验、判定(下载中 busy、待验证拒、同镜像不重下)、健康判据
- ota_esp.{h,c}:t_ota 任务经 HTTP 下载、边下边算 sha256、核 project_name 后切启动分区;
  新固件 120 s 内连不上 broker 就回滚(t_link 判 + esp_timer 兜底);重启前等回应发出、事件落盘
- 新命令 ota = 13(两条链路都收,reboot 默认 false:复位 = 五台手机断电一次);status 加 ota 一块;
  新事件 LOG_EV_OTA = 18;PROJECT_VER 取 PHONERAIL_FW_VERSION
- test_ota.c 13 例,全工程 442 例全绿;版本 0.1.0-c2
- 服务器:nginx 7765 只放行 /fw/<16 hex>.bin(不过门户,路由器转发已加);tools/ota_push.py 推送;
  rack protocol.py 加事件 18 / 命令 13(已部署)
- 板 B:推送到自证健康 19 s;错哈希不切分区;删掉 broker 账号后 120 s 回滚,bad 报出被回滚的版本

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-09-27 14:03:13 +08:00

218 行
7.7 KiB
C
原始文件 Blame 文件历史

此文件含有模棱两可的 Unicode 字符
此文件含有可能会与其他字符混淆的 Unicode 字符。 如果您是想特意这样的,可以安全地忽略该警告。 使用 Escape 按钮显示他们。
/* 单一命令处理器:解析 → 序号判重 → 限幅 / 空槽 → 执行 → 回应。契约在 cmd.h。 */
#include "cmd.h"
#include <string.h>
#include "cmd_priv.h"
#include "evlog.h"
/* ---- 名字表(线上用的,全 ASCII) ---------------------------------------- */
static const char *const k_names[CMD_COUNT] = {
[CMD_NONE] = "", [CMD_STATUS] = "status", [CMD_SET_VOLTAGE] = "set_voltage",
[CMD_POWER_CYCLE] = "power_cycle", [CMD_KEY] = "key", [CMD_EDL] = "edl",
[CMD_VBUS] = "vbus", [CMD_FAN] = "fan", [CMD_EVENTS] = "events",
[CMD_POWER] = "power", [CMD_WIFI] = "wifi", [CMD_MQTT] = "mqtt",
[CMD_NET] = "net", [CMD_OTA] = "ota",
};
static const char *const k_errs[CMD_ERR_COUNT] = {
[CMD_OK] = "ok", [CMD_ERR_PARSE] = "parse", [CMD_ERR_UNKNOWN] = "unknown_cmd",
[CMD_ERR_NO_SEQ] = "no_seq", [CMD_ERR_ARG] = "arg", [CMD_ERR_RANGE] = "range",
[CMD_ERR_EMPTY] = "empty", [CMD_ERR_BUSY] = "busy", [CMD_ERR_STATE] = "state",
[CMD_ERR_IO] = "io", [CMD_ERR_OVERFLOW] = "overflow", [CMD_ERR_LINK] = "link",
};
const char *cmd_name(cmd_id_t id) { return (id > CMD_NONE && id < CMD_COUNT) ? k_names[id] : "?"; }
const char *cmd_err_str(cmd_err_t e) { return (e < CMD_ERR_COUNT) ? k_errs[e] : "?"; }
/* 会动东西的命令:必须带 seq、进判重窗口。status / events 幂等,不进。 */
static bool mutates(cmd_id_t id)
{
return id != CMD_STATUS && id != CMD_EVENTS && id != CMD_NET;
}
/* ---- 状态 ---------------------------------------------------------------- */
typedef struct {
uint32_t seq;
uint8_t cmd;
uint8_t err;
} seen_t;
static cmd_env_t s_env;
const cmd_env_t *cmd_env(void) { return &s_env; }
static seen_t s_seen[CMD_SEQ_WINDOW];
static uint32_t s_seen_n; /* 已填几条(≤ 窗口) */
static uint32_t s_seen_head; /* 下一条写哪 */
static cmd_stats_t s_stats;
void cmd_init(const cmd_env_t *env)
{
memset(&s_env, 0, sizeof(s_env));
if (env != NULL) {
s_env = *env;
}
memset(s_seen, 0, sizeof(s_seen));
s_seen_n = 0;
s_seen_head = 0;
memset(&s_stats, 0, sizeof(s_stats));
}
void cmd_stats(cmd_stats_t *st) { *st = s_stats; }
static const seen_t *seen_find(uint32_t seq)
{
for (uint32_t i = 0; i < s_seen_n; i++) {
if (s_seen[i].seq == seq) {
return &s_seen[i];
}
}
return NULL;
}
static void seen_put(uint32_t seq, cmd_id_t id, cmd_err_t err)
{
s_seen[s_seen_head] = (seen_t){ .seq = seq, .cmd = (uint8_t)id, .err = (uint8_t)err };
s_seen_head = (s_seen_head + 1u) % CMD_SEQ_WINDOW;
if (s_seen_n < CMD_SEQ_WINDOW) {
s_seen_n++;
}
}
static cmd_err_t do_status(ctx_t *c)
{
cmd_report_status(c->w, &s_env);
return CMD_OK;
}
static cmd_err_t do_events(ctx_t *c)
{
int64_t from = 0;
const int g = cj_get_int(c->o, "from", &from);
if (g < 0 || (g == 1 && (from < 0 || from > (int64_t)UINT32_MAX))) {
return CMD_ERR_ARG;
}
cmd_report_events(c->w, (uint32_t)from);
return CMD_OK;
}
typedef cmd_err_t (*handler_t)(ctx_t *c);
static const handler_t k_handlers[CMD_COUNT] = {
[CMD_STATUS] = do_status, [CMD_SET_VOLTAGE] = do_set_voltage,
[CMD_POWER_CYCLE] = do_power_cycle, [CMD_KEY] = do_key, [CMD_EDL] = do_edl,
[CMD_VBUS] = do_vbus, [CMD_FAN] = do_fan, [CMD_EVENTS] = do_events,
[CMD_POWER] = do_power, [CMD_WIFI] = do_wifi, [CMD_MQTT] = do_mqtt,
[CMD_NET] = do_net, [CMD_OTA] = do_ota,
};
static cmd_id_t lookup(const char *name)
{
for (int i = CMD_NONE + 1; i < CMD_COUNT; i++) {
if (strcmp(k_names[i], name) == 0) {
return (cmd_id_t)i;
}
}
return CMD_NONE;
}
/* ---- 入口 ---------------------------------------------------------------- */
/* 拒绝记事件的只有「越界 / 空槽 / 状态不许 / 链路不许」四类:它们是操作者(或上位机逻辑)
真的想做一件不该做的事。解析错、缺参数、忙、判重不记 —— USB 线上一串乱码
不该把 256 条的 RAM 缓冲刷掉(evlog.h)。
事件字段:arg = 命令号,bus_mv = 越界的那个值,current_ua = 拒绝原因(cmd_err_t)。 */
static void log_reject(const ctx_t *c, cmd_id_t id, cmd_err_t e)
{
if (e != CMD_ERR_RANGE && e != CMD_ERR_EMPTY && e != CMD_ERR_STATE && e != CMD_ERR_LINK) {
return;
}
const uint8_t rail = (c->rail >= 1 && c->rail <= PHONERAIL_RAIL_COUNT) ? (uint8_t)c->rail : 0;
evlog_put(rail, LOG_EV_CMD_REJECT, (uint8_t)id, (int32_t)c->bad, (int32_t)e);
}
static size_t reply(char *out, size_t len, bool has_seq, uint32_t seq, cmd_err_t e, bool dup,
const char *extra)
{
cj_writer_t w;
cj_w_init(&w, out, len);
cj_w(&w, "{");
if (has_seq) {
cj_w(&w, "\"seq\":%lu,", (unsigned long)seq);
}
if (e == CMD_OK) {
cj_w(&w, "\"ok\":true%s", extra != NULL ? extra : "");
} else {
cj_w(&w, "\"ok\":false,\"err\":\"%s\"", cmd_err_str(e));
}
cj_w(&w, "%s}", dup ? ",\"dup\":true" : "");
return w.overflow ? 0 : w.pos;
}
size_t cmd_handle_line(cmd_link_t link, const char *line, char *out, size_t out_len)
{
s_stats.lines++;
/* 两块大缓冲放静态区:处理器只在执行器任务里跑(cmd.h),不可重入,
而 1.5 KB 放在那个任务的栈上就是把栈顶到头。 */
static cj_obj_t o;
static char extra[CMD_OUT_MAX];
if (!cj_parse(line, &o)) {
s_stats.rejects++;
return reply(out, out_len, false, 0, CMD_ERR_PARSE, false, NULL);
}
int64_t seq64 = 0;
const int gs = cj_get_int(&o, "seq", &seq64);
const bool has_seq = (gs == 1 && seq64 >= 1 && seq64 <= (int64_t)UINT32_MAX);
const uint32_t seq = has_seq ? (uint32_t)seq64 : 0u;
const char *name = NULL;
const cmd_id_t id = (cj_get_str(&o, "cmd", &name) == 1) ? lookup(name) : CMD_NONE;
if (id == CMD_NONE) {
s_stats.rejects++;
return reply(out, out_len, has_seq, seq, CMD_ERR_UNKNOWN, false, NULL);
}
if ((gs != 0 && !has_seq) || (mutates(id) && !has_seq)) {
s_stats.rejects++; /* 带了一个坏 seq,或者该带没带 */
return reply(out, out_len, false, 0, CMD_ERR_NO_SEQ, false, NULL);
}
if (mutates(id)) {
const seen_t *hit = seen_find(seq);
if (hit != NULL) {
/* 重传:回第一次的结论,**不再执行**(§10.8 规矩 3)。
命令号对不上说明上位机把同一个 seq 用在了两条命令上 —— 那是它的 bug,
照样不执行(执行了就是「一个 seq 两个动作」),但回的是 arg 让它看得见。 */
s_stats.dups++;
const cmd_err_t e = (hit->cmd == (uint8_t)id) ? (cmd_err_t)hit->err : CMD_ERR_ARG;
return reply(out, out_len, true, seq, e, true, NULL);
}
}
cj_writer_t w;
cj_w_init(&w, extra, sizeof(extra));
ctx_t c = { .o = &o, .link = link, .w = &w, .rail = 0, .bad = 0 };
cmd_err_t e = k_handlers[id](&c);
if (e == CMD_OK && w.overflow && !mutates(id)) {
e = CMD_ERR_OVERFLOW; /* 只读命令:什么都没做,如实报 */
}
if (e != CMD_OK) {
s_stats.rejects++;
log_reject(&c, id, e);
}
if (mutates(id)) {
seen_put(seq, id, e);
}
size_t n = reply(out, out_len, has_seq, seq, e, false, w.overflow ? NULL : extra);
if (n == 0) {
/* 附加字段装不进调用方的缓冲。会动东西的命令此时**已经执行了**:
回一条不带附加字段的 ok,别让上位机以为没执行而换个 seq 再发一遍。 */
n = reply(out, out_len, has_seq, seq, mutates(id) ? e : CMD_ERR_OVERFLOW, false, NULL);
}
return n;
}