文件
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

153 行
5.9 KiB
C

/* A12:命令行的扁平 JSON 解析器(cmd_json.h)。
这一层的测试重点不是「合法的能解出来」,而是**不合法的一律拒**:
请求形状越窄,越界输入的面就越小。每一条「刻意不支持」都在这里钉一条用例 ——
哪天有人为了「兼容」放宽了其中一条,这里先红。 */
#include <stdio.h>
#include <string.h>
#include "cmd_json.h"
#include "unity.h"
void setUp(void) {}
void tearDown(void) {}
static cj_obj_t o;
static void test_parses_a_flat_command(void)
{
TEST_ASSERT_TRUE(cj_parse("{\"seq\":17,\"cmd\":\"key\",\"rail\":3,\"keys\":\"pwr+vdn\","
"\"ms\":2000,\"on\":true,\"x\":false,\"neg\":-5}\r\n",
&o));
int64_t v = 0;
const char *s = NULL;
bool bv = false;
TEST_ASSERT_EQUAL_INT(1, cj_get_int(&o, "seq", &v));
TEST_ASSERT_EQUAL_INT64(17, v);
TEST_ASSERT_EQUAL_INT(1, cj_get_str(&o, "cmd", &s));
TEST_ASSERT_EQUAL_STRING("key", s);
TEST_ASSERT_EQUAL_INT(1, cj_get_str(&o, "keys", &s));
TEST_ASSERT_EQUAL_STRING("pwr+vdn", s);
TEST_ASSERT_EQUAL_INT(1, cj_get_bool(&o, "on", &bv));
TEST_ASSERT_TRUE(bv);
TEST_ASSERT_EQUAL_INT(1, cj_get_bool(&o, "x", &bv));
TEST_ASSERT_FALSE(bv);
TEST_ASSERT_EQUAL_INT(1, cj_get_int(&o, "neg", &v));
TEST_ASSERT_EQUAL_INT64(-5, v);
TEST_ASSERT_EQUAL_INT(0, cj_get_int(&o, "missing", &v));
}
static void test_whitespace_and_empty_object(void)
{
TEST_ASSERT_TRUE(cj_parse(" { \"a\" : 1 ,\t\"b\":\"x\" } ", &o));
TEST_ASSERT_EQUAL_INT(2, o.n);
TEST_ASSERT_TRUE(cj_parse("{}", &o));
TEST_ASSERT_EQUAL_INT(0, o.n);
}
/* 「有但类型不对」与「没有」是两回事:`"mv":"3900"` 是上位机的 bug,不是「没给电压」。 */
static void test_wrong_type_is_distinguished_from_missing(void)
{
TEST_ASSERT_TRUE(cj_parse("{\"mv\":\"3900\",\"on\":1,\"cmd\":5}", &o));
int64_t v = 0;
const char *s = NULL;
bool b = false;
TEST_ASSERT_EQUAL_INT(-1, cj_get_int(&o, "mv", &v));
TEST_ASSERT_EQUAL_INT(-1, cj_get_bool(&o, "on", &b)); /* 1 不是布尔 */
TEST_ASSERT_EQUAL_INT(-1, cj_get_str(&o, "cmd", &s));
}
static void test_rejects_everything_outside_the_flat_shape(void)
{
static const char *const bad[] = {
"", /* 空行 */
"[1,2]", /* 不是对象 */
"{\"a\":{\"b\":1}}", /* 嵌套 */
"{\"a\":[1]}", /* 数组 */
"{\"a\":null}", /* null */
"{\"a\":3.9}", /* 浮点:不许被截成 3 */
"{\"a\":1e3}", /* 指数 */
"{\"a\":010}", /* 前导零 */
"{\"a\":1234567890123456789}", /* 19 位:不回绕 */
"{\"a\":\"x\\\"y\"}", /* 转义:见头文件 */
"{\"a\":\"line\nbreak\"}", /* 控制字符 */
"{\"mv\":3900,\"mv\":4300}", /* 重复键:有人拿来绕校验的那种 */
"{\"a\":1}{\"b\":2}", /* 两条命令粘在一行 */
"{\"a\":1} x", /* 尾巴 */
"{\"a\":1,}", /* 尾逗号 */
"{\"a\" 1}", /* 缺冒号 */
"{\"\":1}", /* 空键 */
"{a:1}", /* 键没引号 */
"{\"a\":tru}", /* 半个 true */
"{\"a\":1", /* 没收尾 */
"{\"a\":-}", /* 光一个负号 */
};
for (size_t i = 0; i < sizeof(bad) / sizeof(bad[0]); i++) {
TEST_ASSERT_FALSE_MESSAGE(cj_parse(bad[i], &o), bad[i]);
}
TEST_ASSERT_FALSE(cj_parse(NULL, &o));
}
/* 超长的键 / 字符串不截断:截断会把 "set_voltage_x" 读成另一条命令。 */
static void test_overlong_tokens_are_rejected_not_truncated(void)
{
char line[128];
memset(line, 0, sizeof(line));
strcpy(line, "{\"cmd\":\"");
memset(line + strlen(line), 'a', CMD_JSON_STR_MAX); /* 32 个字符:装不下结尾 0 */
strcat(line, "\"}");
TEST_ASSERT_FALSE(cj_parse(line, &o));
memset(line, 0, sizeof(line));
strcpy(line, "{\"");
memset(line + 2, 'k', CMD_JSON_KEY_MAX); /* 16 个:装不下结尾 0 */
line[2 + CMD_JSON_KEY_MAX] = '\0';
strcat(line, "\":1}");
TEST_ASSERT_FALSE(cj_parse(line, &o));
memset(line, 0, sizeof(line));
strcpy(line, "{\"cmd\":\"");
memset(line + strlen(line), 'a', CMD_JSON_STR_MAX - 1); /* 31 个 + 结尾 0:刚好装得下 */
strcat(line, "\"}");
TEST_ASSERT_TRUE(cj_parse(line, &o));
}
static void test_field_count_is_bounded(void)
{
char line[256] = "{";
for (int i = 0; i < CMD_JSON_MAX_FIELDS + 1; i++) {
char f[16];
snprintf(f, sizeof(f), "%s\"k%d\":%d", i ? "," : "", i, i);
strcat(line, f);
}
strcat(line, "}");
TEST_ASSERT_FALSE(cj_parse(line, &o));
}
static void test_writer_never_overruns(void)
{
char buf[8];
cj_writer_t w;
cj_w_init(&w, buf, sizeof(buf));
cj_w(&w, "abc");
TEST_ASSERT_FALSE(w.overflow);
cj_w(&w, "defgh"); /* 3 + 5 = 8,装不下结尾 0 */
TEST_ASSERT_TRUE(w.overflow);
TEST_ASSERT_EQUAL_STRING("abc", buf); /* 写了一半的那段撤掉 */
cj_w(&w, "x");
TEST_ASSERT_EQUAL_STRING("abc", buf); /* 溢出之后一律不写 */
}
int main(void)
{
UNITY_BEGIN();
RUN_TEST(test_parses_a_flat_command);
RUN_TEST(test_whitespace_and_empty_object);
RUN_TEST(test_wrong_type_is_distinguished_from_missing);
RUN_TEST(test_rejects_everything_outside_the_flat_shape);
RUN_TEST(test_overlong_tokens_are_rejected_not_truncated);
RUN_TEST(test_field_count_is_bounded);
RUN_TEST(test_writer_never_overruns);
return UNITY_END();
}