固件: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]>
这个提交包含在:
@@ -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;
|
||||
}
|
||||
在新工单中引用
屏蔽一个用户