固件: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]>
这个提交包含在:
zhou yang
2026-09-27 12:33:18 +08:00
共同撰写人 Claude Opus 5.5
父节点 cae567dfd8
当前提交 a32fa06c43
共修改 38 个文件,包含 4490 行新增和 85 行删除
+213
查看文件
@@ -0,0 +1,213 @@
/* 扁平 JSON 解析 + 定长写入器。契约与「刻意不支持」的清单在 cmd_json.h。 */
#include "cmd_json.h"
#include <stdio.h>
#include <string.h>
static const char *ws(const char *p)
{
while (*p == ' ' || *p == '\t' || *p == '\r' || *p == '\n') {
p++;
}
return p;
}
/* 读一个 `"..."`,写进 out(容量 cap,含结尾 0)。返回引号之后的位置,失败 NULL。
只收可打印 ASCII 之外不做任何解释:反斜杠与控制字符一律失败。 */
static const char *str(const char *p, char *out, size_t cap)
{
if (*p != '"') {
return NULL;
}
p++;
size_t n = 0;
while (*p != '"') {
const unsigned char c = (unsigned char)*p;
if (c == '\0' || c == '\\' || c < 0x20) {
return NULL;
}
if (n + 1 >= cap) {
return NULL; /* 太长:截断会把 "set_voltage_x" 读成别的东西 */
}
out[n++] = (char)c;
p++;
}
out[n] = '\0';
return p + 1;
}
static const char *num(const char *p, int64_t *v)
{
bool neg = false;
if (*p == '-') {
neg = true;
p++;
}
if (*p < '0' || *p > '9') {
return NULL;
}
if (*p == '0' && p[1] >= '0' && p[1] <= '9') {
return NULL; /* 前导零:JSON 本身就不许,也防 "010" 被当成八进制理解 */
}
int64_t acc = 0;
int digits = 0;
while (*p >= '0' && *p <= '9') {
if (++digits > 18) {
return NULL;
}
acc = acc * 10 + (*p - '0');
p++;
}
if (*p == '.' || *p == 'e' || *p == 'E') {
return NULL; /* 浮点:见头文件 */
}
*v = neg ? -acc : acc;
return p;
}
static bool word(const char **pp, const char *w)
{
const size_t n = strlen(w);
if (strncmp(*pp, w, n) != 0) {
return false;
}
*pp += n;
return true;
}
static const cj_field_t *find(const cj_obj_t *o, const char *key)
{
for (int i = 0; i < o->n; i++) {
if (strcmp(o->f[i].key, key) == 0) {
return &o->f[i];
}
}
return NULL;
}
bool cj_parse(const char *line, cj_obj_t *o)
{
memset(o, 0, sizeof(*o));
if (line == NULL) {
return false;
}
const char *p = ws(line);
if (*p++ != '{') {
return false;
}
p = ws(p);
if (*p == '}') {
p++;
} else {
for (;;) {
if (o->n >= CMD_JSON_MAX_FIELDS) {
return false;
}
cj_field_t *f = &o->f[o->n];
p = str(p, f->key, sizeof(f->key));
if (p == NULL || f->key[0] == '\0' || find(o, f->key) != NULL) {
return false; /* 解析失败 / 空键 / 重复键 */
}
p = ws(p);
if (*p++ != ':') {
return false;
}
p = ws(p);
if (*p == '"') {
f->type = CJ_STR;
p = str(p, f->s, sizeof(f->s));
} else if (word(&p, "true")) {
f->type = CJ_BOOL;
f->i = 1;
} else if (word(&p, "false")) {
f->type = CJ_BOOL;
f->i = 0;
} else {
f->type = CJ_INT;
p = num(p, &f->i);
}
if (p == NULL) {
return false;
}
o->n++;
p = ws(p);
if (*p == ',') {
p = ws(p + 1);
continue;
}
if (*p == '}') {
p++;
break;
}
return false;
}
}
return *ws(p) == '\0'; /* 右括号之后只许有空白:两条命令粘在一行里是分帧的 bug */
}
int cj_get_int(const cj_obj_t *o, const char *key, int64_t *v)
{
const cj_field_t *f = find(o, key);
if (f == NULL) {
return 0;
}
if (f->type != CJ_INT) {
return -1;
}
*v = f->i;
return 1;
}
int cj_get_str(const cj_obj_t *o, const char *key, const char **s)
{
const cj_field_t *f = find(o, key);
if (f == NULL) {
return 0;
}
if (f->type != CJ_STR) {
return -1;
}
*s = f->s;
return 1;
}
int cj_get_bool(const cj_obj_t *o, const char *key, bool *v)
{
const cj_field_t *f = find(o, key);
if (f == NULL) {
return 0;
}
if (f->type != CJ_BOOL) {
return -1; /* `"on":1` 不是布尔。收 1 的话 `"on":2` 该算什么就说不清了 */
}
*v = f->i != 0;
return 1;
}
void cj_w_init(cj_writer_t *w, char *buf, size_t cap)
{
w->buf = buf;
w->cap = cap;
w->pos = 0;
w->overflow = (buf == NULL || cap == 0);
if (!w->overflow) {
buf[0] = '\0';
}
}
void cj_w(cj_writer_t *w, const char *fmt, ...)
{
if (w->overflow) {
return;
}
va_list ap;
va_start(ap, fmt);
const int n = vsnprintf(w->buf + w->pos, w->cap - w->pos, fmt, ap);
va_end(ap);
if (n < 0 || (size_t)n >= w->cap - w->pos) {
w->overflow = true;
w->buf[w->pos] = '\0'; /* 把写了一半的那段撤掉 */
return;
}
w->pos += (size_t)n;
}