文件
zhou yang和Claude Opus 5.5 a32fa06c43 固件:A12 命令处理器 + A13 USB 分帧 + A14 WiFi/MQTT(含 C1 配网)
- A12 单一命令处理器(cmd*.c):一行 JSON 进出、seq 全局判重(32 条按值)、
  限幅 / 空槽一律拒 / 拒绝记事件;命令号与错误码冻结(进 flash)。新增 power 命令,
  fan 改为只抬不压的下限(t_thermal_set_floor)。rail_set_mv 注明不拿锁,调用方包 xact。
- A13 USB-Serial-JTAG 分帧(ulink*.c):@C / @R 前缀 + CRC-16,日志行首 @ 垫空格,
  任意切块可切;关掉 USB 副控制台(它绕开驱动写 FIFO)。PC 端客户端 tools/prlink.py。
- A14 + C1(netcfg / mlink / cmd_net):topic phonerail/<mac>/{cmd,rsp,tele,state},
  mqtts(evse-ca 编进固件)、1 s 遥测、遗嘱离线;wifi / mqtt 配置只收 USB、存 NVS,
  net 永不回显密码。
- 协议定稿写进需求文档 §10.8(A12 / A13 / A14 三节);N7 撤销改 D7(机架管理前后端)。

宿主机 23 个 ctest / 400 例全绿;板 B 已烧 0.1.0-a14,经 WiFi → 公网 → 8883 TLS 真连网验过。
固件 0xE2220 / 1 MB 分区只剩 12%,C2 前要重排分区表。

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-09-27 12:33:18 +08:00

124 行
5.9 KiB
C

此文件含有模棱两可的 Unicode 字符
此文件含有可能会与其他字符混淆的 Unicode 字符。 如果您是想特意这样的,可以安全地忽略该警告。 使用 Escape 按钮显示他们。
/* USB-Serial-JTAG 链路的分帧(A13,需求 §10.8 规矩 4 / IF-13 / 计划 D13)。
ESP32-S3 的原生 USB-Serial-JTAG 是**一根线三用**:esptool 烧录、运行日志、命令链路。
日志与命令回应在同一条串流上,所以必须分帧 —— 否则日志一刷就把回应冲散。
本文件是与平台无关的那一半(宿主机可测),ESP 胶水在 `ulink_esp.c`。
---- 线上格式 ------------------------------------------------------------
上位机 → 板 @C {"seq":5,"cmd":"status"}*1A2B\n (*CRC 可省,给人手敲用)
板 → 上位机 @R {"seq":5,"ok":true,...}*3C4D\n (*CRC 必带)
其余任何一行 = 日志,上位机原样丢给日志窗口
· 前缀 `@C ` / `@R `(含一个空格)只在**行首**才算数。
· CRC 是 CRC-16/CCITT-FALSE(多项式 0x1021,初值 0xFFFF),算的是 JSON 那一段
(前缀与 `*` 都不算),4 位大写十六进制。「123456789」的校验值是 0x29B1。
· 行尾 `\n`,前面带一个 `\r` 也认(Windows 终端会发)。
---- 为什么回应必须带 CRC ------------------------------------------------
日志与回应都经 `ulink_tx_*` 串行写出(ESP 侧拿一把互斥锁),**正常情况下不会交错**。
但总有绕开这把锁的写者:panic 处理、ROM 打印、别的组件直接往 USB 寄存器写。
它们插进一条回应中间时,剩下的仍可能是一段「看起来合法」的 JSON ——
比如 `"ok":false` 被截掉后半句。CRC 让这种回应在上位机那一侧**一定**被丢弃
(然后按超时重传同一个 seq,由判重兜住)。
---- 为什么日志永远不可能被当成帧 ----------------------------------------
`ulink_tx_log()` 发现一行日志以 `@` 开头时,在前面补一个空格。
日志内容是任意的(错误信息里可能引用了一条命令原文),这一条保证「行首的 `@R `」
只可能来自 `ulink_tx_frame()`。 */
#ifndef ULINK_H
#define ULINK_H
#include <stdbool.h>
#include <stddef.h>
#include <stdint.h>
#include "cmd.h"
#ifdef __cplusplus
extern "C" {
#endif
#define ULINK_CMD_PREFIX "@C "
#define ULINK_RSP_PREFIX "@R "
#define ULINK_PREFIX_LEN 3
#define ULINK_CRC_LEN 5 /* "*XXXX" */
/* 一行请求最长多少字节(不含 `\r\n`):前缀 + JSON(≤ CMD_LINE_MAX − 1)+ CRC。 */
#define ULINK_RX_LINE_MAX (ULINK_PREFIX_LEN + (CMD_LINE_MAX - 1) + ULINK_CRC_LEN)
uint16_t ulink_crc16(const char *data, size_t len);
/* ---- 接收:字节流 → 命令 ---------------------------------------------- */
typedef enum {
ULINK_RX_CMD = 0, /* 一条完整的命令,payload 是去掉前缀与 CRC 的 JSON */
ULINK_RX_TOO_LONG, /* 以 `@C ` 开头但超长:整行丢弃,payload 为 "" */
ULINK_RX_BAD_CRC, /* 带了 CRC 但对不上:payload 是 JSON 部分(只用来回显 seq) */
ULINK_RX_BAD_BYTES, /* 行里有 NUL 或控制字符:payload 同上 */
} ulink_rx_kind_t;
typedef void (*ulink_rx_fn)(void *ctx, ulink_rx_kind_t kind, const char *payload);
typedef struct {
char buf[ULINK_RX_LINE_MAX + 1];
size_t n;
bool overflow; /* 本行已超长:一直丢到 `\n` */
bool bad; /* 本行出现过 NUL / 控制字符 */
/* 统计 */
uint32_t cmds;
uint32_t ignored; /* 不是命令的行(日志回显、人手敲的回车……) */
uint32_t too_long;
uint32_t bad_crc;
uint32_t bad_bytes;
} ulink_rx_t;
void ulink_rx_init(ulink_rx_t *rx);
/* 喂任意长度的一段字节(可以在任何位置切开,包括 `\r\n` 中间与 CRC 中间)。
每凑齐一行就回调一次 —— 回调在本函数里同步发生。 */
void ulink_rx_feed(ulink_rx_t *rx, const uint8_t *data, size_t len, ulink_rx_fn fn, void *ctx);
/* ---- 发送:日志与帧交错写出 --------------------------------------------- */
/* 往线上写字节。返回**真的写出去**几个(USB 缓冲满时可能少于 len)。 */
typedef size_t (*ulink_sink_fn)(void *ctx, const char *data, size_t len);
typedef struct {
ulink_sink_fn sink;
void *ctx;
bool at_bol; /* 最后真正写出去的字节是 `\n`(或者什么都还没写) */
uint32_t frames;
uint32_t short_writes; /* 没写全的次数:上位机那一侧会看到一条 CRC 错的行 */
} ulink_tx_t;
void ulink_tx_init(ulink_tx_t *tx, ulink_sink_fn sink, void *ctx);
/* 写一段日志。可以是半行、一行或多行;行首 `@` 会被垫一个空格。 */
void ulink_tx_log(ulink_tx_t *tx, const char *data, size_t len);
/* 写一帧回应:必要时先补 `\n` 把半行日志收尾,再写 `@R <json>*XXXX\n`。
`json` 里不许有 `\n`(cmd.h 的回应本来就没有)。返回是否整帧写出。 */
bool ulink_tx_frame(ulink_tx_t *tx, const char *json, size_t len);
/* ---- 链路层自己的错误回应 --------------------------------------------------
这几种错在命令处理器**之前**就发生了(行太长 / CRC 错 / 坏字节 / 队列满),
由链路任务直接回,不进处理器、不记事件(USB 线上的噪声不该刷掉事件缓冲)。
`payload` 里能解析出 seq 就回显,上位机据此知道是哪一条要重发。
err 名字:`too_long` / `bad_crc` / `bad_frame` / `queue_full`。返回 JSON 长度。 */
size_t ulink_link_error(const char *payload, const char *err, char *out, size_t cap);
const char *ulink_rx_err_str(ulink_rx_kind_t kind); /* CMD → NULL */
/* ---- 参考解帧器(上位机那一侧的逻辑,放这里给测试和文档对账) --------------- */
/* 一行输出(不含 `\n`)是不是一条合法回应帧。是则把 JSON 拷进 out 并返回其长度,
不是(日志、CRC 错、没带 CRC)返回 −1。`tools/prlink.py` 是同一逻辑的 Python 版。 */
int ulink_deframe(const char *line, char *out, size_t cap);
#ifdef __cplusplus
}
#endif
#endif /* ULINK_H */