- 分区重排(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]>
218 行
7.7 KiB
C
218 行
7.7 KiB
C
/* 单一命令处理器:解析 → 序号判重 → 限幅 / 空槽 → 执行 → 回应。契约在 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;
|
||
}
|