- 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]>
296 行
12 KiB
C
296 行
12 KiB
C
/* A14 / C1:网络配置、MQTT 链路的平台无关部分、三条网络命令。
|
|
|
|
计划里 A14 的验收原话是「协议层用 sim 传输测通;真连网留到 B 段」。
|
|
这里测的是会写错而且上板查不出来的那几件事:
|
|
· 密码**永远**不出现在任何回应里;
|
|
· 配置命令**只收 USB**,经 WiFi 来的一律拒并记事件;
|
|
· 没存进 flash 的配置不生效(重启前后一致);
|
|
· MQTT 入站消息与 USB 同一套坏字节规矩;topic / 退避 / 遥测节拍的数。 */
|
|
#include <stdio.h>
|
|
#include <string.h>
|
|
|
|
#include "cmd.h"
|
|
#include "evlog.h"
|
|
#include "mlink.h"
|
|
#include "netcfg.h"
|
|
#include "sim_hal.h"
|
|
#include "unity.h"
|
|
|
|
static char s_out[CMD_OUT_MAX];
|
|
|
|
/* 假存储后端:记住存了什么、可以注入失败。 */
|
|
static netcfg_t s_saved;
|
|
static int s_saves;
|
|
static hal_err_t s_save_err;
|
|
static int s_changed;
|
|
|
|
static hal_err_t fake_save(void *ctx, const netcfg_t *cfg)
|
|
{
|
|
(void)ctx;
|
|
if (s_save_err != HAL_OK) {
|
|
return s_save_err;
|
|
}
|
|
s_saved = *cfg;
|
|
s_saves++;
|
|
return HAL_OK;
|
|
}
|
|
|
|
static void fake_changed(void *ctx)
|
|
{
|
|
(void)ctx;
|
|
s_changed++;
|
|
}
|
|
|
|
void setUp(void)
|
|
{
|
|
sim_hal_reset();
|
|
memset(&s_saved, 0, sizeof(s_saved));
|
|
s_saves = 0;
|
|
s_save_err = HAL_OK;
|
|
s_changed = 0;
|
|
netcfg_init(NULL, fake_save, fake_changed, NULL);
|
|
TEST_ASSERT_EQUAL_INT(HAL_OK, evlog_init());
|
|
cmd_init(NULL);
|
|
}
|
|
|
|
void tearDown(void) { TEST_ASSERT_EQUAL_INT(0, sim_crit_depth()); }
|
|
|
|
static const char *send_on(cmd_link_t link, const char *line)
|
|
{
|
|
cmd_handle_line(link, line, s_out, sizeof(s_out));
|
|
return s_out;
|
|
}
|
|
|
|
static void assert_has_at(const char *resp, const char *needle, int line)
|
|
{
|
|
UNITY_TEST_ASSERT_NOT_NULL(strstr(resp, needle), line, resp);
|
|
}
|
|
static void assert_lacks_at(const char *resp, const char *needle, int line)
|
|
{
|
|
UNITY_TEST_ASSERT_NULL(strstr(resp, needle), line, resp);
|
|
}
|
|
#define ASSERT_HAS(r, n) assert_has_at((r), (n), __LINE__)
|
|
#define ASSERT_LACKS(r, n) assert_lacks_at((r), (n), __LINE__)
|
|
|
|
static int count_rejects(void)
|
|
{
|
|
evlog_rec_t rec[64];
|
|
(void)evlog_flush(0);
|
|
const uint32_t n = evlog_read(0, rec, 64);
|
|
int k = 0;
|
|
for (uint32_t i = 0; i < n; i++) {
|
|
k += rec[i].type == LOG_EV_CMD_REJECT;
|
|
}
|
|
return k;
|
|
}
|
|
|
|
/* ---- 配置命令 ------------------------------------------------------------ */
|
|
|
|
/* ⭐ USB 来的 wifi / mqtt:存进后端、内存生效、通知网络那一侧重连。 */
|
|
static void test_config_over_usb_is_saved_and_applied(void)
|
|
{
|
|
ASSERT_HAS(send_on(CMD_LINK_USB,
|
|
"{\"seq\":1,\"cmd\":\"wifi\",\"ssid\":\"farm-2g\",\"pass\":\"s3cret-pass\"}"),
|
|
"\"ok\":true");
|
|
ASSERT_HAS(send_on(CMD_LINK_USB, "{\"seq\":2,\"cmd\":\"mqtt\",\"uri\":\"mqtt://10.5.5.13:1883\","
|
|
"\"user\":\"rack\",\"pass\":\"m-pass-1\",\"tele_ms\":2000}"),
|
|
"\"ok\":true");
|
|
TEST_ASSERT_EQUAL_INT(2, s_saves);
|
|
TEST_ASSERT_EQUAL_INT(2, s_changed);
|
|
TEST_ASSERT_EQUAL_STRING("farm-2g", s_saved.ssid);
|
|
TEST_ASSERT_EQUAL_STRING("s3cret-pass", s_saved.pass); /* 第二次存的是整份:WiFi 那部分还在 */
|
|
TEST_ASSERT_EQUAL_STRING("mqtt://10.5.5.13:1883", s_saved.uri);
|
|
TEST_ASSERT_EQUAL_UINT32(2000, s_saved.tele_ms);
|
|
|
|
netcfg_t c;
|
|
netcfg_get(&c);
|
|
TEST_ASSERT_EQUAL_MEMORY(&s_saved, &c, sizeof(c));
|
|
}
|
|
|
|
/* ⭐ 经 WiFi 改 WiFi / MQTT 配置:一律 `link`、记事件、后端一次都不碰。 */
|
|
static void test_config_over_wifi_is_refused_and_logged(void)
|
|
{
|
|
ASSERT_HAS(send_on(CMD_LINK_WIFI,
|
|
"{\"seq\":1,\"cmd\":\"wifi\",\"ssid\":\"evil\",\"pass\":\"12345678\"}"),
|
|
"\"err\":\"link\"");
|
|
ASSERT_HAS(send_on(CMD_LINK_WIFI, "{\"seq\":2,\"cmd\":\"mqtt\",\"uri\":\"mqtt://1.2.3.4\"}"),
|
|
"\"err\":\"link\"");
|
|
TEST_ASSERT_EQUAL_INT(0, s_saves);
|
|
TEST_ASSERT_EQUAL_INT(0, s_changed);
|
|
TEST_ASSERT_EQUAL_INT(2, count_rejects());
|
|
/* 只读的 net 经 WiFi 照常可用。 */
|
|
ASSERT_HAS(send_on(CMD_LINK_WIFI, "{\"cmd\":\"net\"}"), "\"ok\":true");
|
|
}
|
|
|
|
/* ⭐ 密码永不外泄:写进去之后 net 回应里只有 *_set,没有明文;换哪条链路问都一样。 */
|
|
static void test_net_never_echoes_a_password(void)
|
|
{
|
|
send_on(CMD_LINK_USB, "{\"seq\":1,\"cmd\":\"wifi\",\"ssid\":\"farm\",\"pass\":\"TopSecret99\"}");
|
|
send_on(CMD_LINK_USB, "{\"seq\":2,\"cmd\":\"mqtt\",\"uri\":\"mqtt://h:1883\",\"user\":\"u1\","
|
|
"\"pass\":\"BrokerPw77\"}");
|
|
for (int link = CMD_LINK_TEST; link <= CMD_LINK_WIFI; link++) {
|
|
const char *r = send_on((cmd_link_t)link, "{\"cmd\":\"net\"}");
|
|
ASSERT_LACKS(r, "TopSecret99");
|
|
ASSERT_LACKS(r, "BrokerPw77");
|
|
ASSERT_HAS(r, "\"ssid\":\"farm\",\"pass_set\":true");
|
|
ASSERT_HAS(r, "\"user\":\"u1\",\"mpass_set\":true");
|
|
ASSERT_HAS(r, "\"uri\":\"mqtt://h:1883\"");
|
|
ASSERT_HAS(r, "\"tele_ms\":1000"); /* 没给 → 默认 */
|
|
}
|
|
/* status 里也不许有。 */
|
|
ASSERT_LACKS(send_on(CMD_LINK_USB, "{\"cmd\":\"status\"}"), "TopSecret99");
|
|
}
|
|
|
|
static void test_config_validation(void)
|
|
{
|
|
static const char *const bad[] = {
|
|
"{\"seq\":1,\"cmd\":\"wifi\",\"ssid\":\"\",\"pass\":\"12345678\"}", /* 空 SSID */
|
|
"{\"seq\":2,\"cmd\":\"wifi\",\"ssid\":\"x\",\"pass\":\"1234567\"}", /* 7 位口令 */
|
|
"{\"seq\":3,\"cmd\":\"mqtt\",\"uri\":\"http://10.5.5.13\"}", /* 不是 mqtt:// */
|
|
"{\"seq\":4,\"cmd\":\"mqtt\",\"uri\":\"ws://10.5.5.13\"}", /* 不认的 scheme */
|
|
"{\"seq\":5,\"cmd\":\"mqtt\",\"uri\":\"mqtt://h\",\"tele_ms\":100}", /* 太密 */
|
|
"{\"seq\":6,\"cmd\":\"mqtt\",\"uri\":\"mqtt://h\",\"tele_ms\":60001}",
|
|
"{\"seq\":7,\"cmd\":\"mqtt\",\"uri\":\"mqtt://h\",\"tele_ms\":-1}",
|
|
};
|
|
for (size_t i = 0; i < sizeof(bad) / sizeof(bad[0]); i++) {
|
|
ASSERT_HAS(send_on(CMD_LINK_USB, bad[i]), "\"err\":\"range\"");
|
|
}
|
|
ASSERT_HAS(send_on(CMD_LINK_USB, "{\"seq\":8,\"cmd\":\"wifi\",\"pass\":\"12345678\"}"),
|
|
"\"err\":\"arg\"");
|
|
ASSERT_HAS(send_on(CMD_LINK_USB, "{\"seq\":9,\"cmd\":\"wifi\",\"ssid\":\"x\",\"pass\":1}"),
|
|
"\"err\":\"arg\"");
|
|
ASSERT_HAS(send_on(CMD_LINK_USB, "{\"seq\":12,\"cmd\":\"mqtt\",\"uri\":\"mqtts://\"}"),
|
|
"\"err\":\"range\""); /* 只有 scheme 没有主机 */
|
|
ASSERT_HAS(send_on(CMD_LINK_USB, "{\"seq\":13,\"cmd\":\"mqtt\",\"uri\":\"mqtts://home.example:8883\"}"),
|
|
"\"ok\":true");
|
|
/* 开放网络(空口令)是合法的;63 位口令顶到上限也合法。 */
|
|
ASSERT_HAS(send_on(CMD_LINK_USB, "{\"seq\":10,\"cmd\":\"wifi\",\"ssid\":\"open\"}"),
|
|
"\"ok\":true");
|
|
char line[200];
|
|
char pass[64];
|
|
memset(pass, 'p', 63);
|
|
pass[63] = '\0';
|
|
snprintf(line, sizeof(line), "{\"seq\":11,\"cmd\":\"wifi\",\"ssid\":\"x\",\"pass\":\"%s\"}", pass);
|
|
ASSERT_HAS(send_on(CMD_LINK_USB, line), "\"ok\":true");
|
|
TEST_ASSERT_EQUAL_STRING(pass, s_saved.pass);
|
|
TEST_ASSERT_EQUAL_INT(3, s_saves);
|
|
}
|
|
|
|
/* 存不下:回 io,内存里的配置**不变**,也不通知重连(重启前后一致)。 */
|
|
static void test_unsaved_config_does_not_take_effect(void)
|
|
{
|
|
send_on(CMD_LINK_USB, "{\"seq\":1,\"cmd\":\"wifi\",\"ssid\":\"old\",\"pass\":\"12345678\"}");
|
|
s_save_err = HAL_ERR_BUS;
|
|
ASSERT_HAS(send_on(CMD_LINK_USB,
|
|
"{\"seq\":2,\"cmd\":\"wifi\",\"ssid\":\"new\",\"pass\":\"12345678\"}"),
|
|
"\"err\":\"io\"");
|
|
netcfg_t c;
|
|
netcfg_get(&c);
|
|
TEST_ASSERT_EQUAL_STRING("old", c.ssid);
|
|
TEST_ASSERT_EQUAL_INT(1, s_changed);
|
|
}
|
|
|
|
/* NVS 里读回来的东西不可信:没带结尾 0 的字段被强制收尾;遥测周期越界回默认;
|
|
引号 / 反斜杠 / 控制字符在 net 回应里换成 `?`,回应仍是一行合法 JSON。 */
|
|
static void test_loaded_config_is_sanitised(void)
|
|
{
|
|
netcfg_t junk;
|
|
memset(&junk, 'A', sizeof(junk));
|
|
junk.tele_ms = 5;
|
|
memcpy(junk.ssid, "a\"b\\c\n", 6);
|
|
netcfg_init(&junk, NULL, NULL, NULL);
|
|
netcfg_t c;
|
|
netcfg_get(&c);
|
|
TEST_ASSERT_EQUAL_size_t(NETCFG_SSID_MAX, strlen(c.ssid));
|
|
TEST_ASSERT_EQUAL_size_t(NETCFG_URI_MAX, strlen(c.uri));
|
|
TEST_ASSERT_EQUAL_UINT32(NETCFG_TELE_DEFAULT_MS, c.tele_ms);
|
|
|
|
const char *r = send_on(CMD_LINK_USB, "{\"cmd\":\"net\"}");
|
|
ASSERT_HAS(r, "\"ssid\":\"a?b?c?AAAA");
|
|
TEST_ASSERT_EQUAL_CHAR('}', r[strlen(r) - 1]);
|
|
}
|
|
|
|
/* ---- MQTT 链路的平台无关部分 --------------------------------------------- */
|
|
|
|
static void test_topics_follow_the_mac(void)
|
|
{
|
|
const uint8_t mac[6] = { 0xB8, 0x1F, 0x3F, 0xC9, 0xC8, 0xBC };
|
|
mlink_init(mac);
|
|
TEST_ASSERT_EQUAL_STRING("b81f3fc9c8bc", mlink_id());
|
|
TEST_ASSERT_EQUAL_STRING("phonerail/b81f3fc9c8bc/cmd", mlink_topic(MLINK_T_CMD));
|
|
TEST_ASSERT_EQUAL_STRING("phonerail/b81f3fc9c8bc/rsp", mlink_topic(MLINK_T_RSP));
|
|
TEST_ASSERT_EQUAL_STRING("phonerail/b81f3fc9c8bc/tele", mlink_topic(MLINK_T_TELE));
|
|
TEST_ASSERT_EQUAL_STRING("phonerail/b81f3fc9c8bc/state", mlink_topic(MLINK_T_STATE));
|
|
TEST_ASSERT_TRUE(strlen(mlink_topic(MLINK_T_STATE)) < MLINK_TOPIC_MAX);
|
|
|
|
char buf[96];
|
|
TEST_ASSERT_TRUE(mlink_state_online("0.1.0-a14", "10.5.5.77", buf, sizeof(buf)) > 0);
|
|
TEST_ASSERT_EQUAL_STRING("{\"online\":true,\"fw\":\"0.1.0-a14\",\"ip\":\"10.5.5.77\"}", buf);
|
|
}
|
|
|
|
/* 入站消息:与 USB 同一套坏字节规矩;切片的 / 超长的直接拒。 */
|
|
static void test_inbound_messages(void)
|
|
{
|
|
char line[CMD_LINE_MAX];
|
|
const char *ok = "{\"cmd\":\"status\"}";
|
|
TEST_ASSERT_NULL(mlink_inbound(ok, (int)strlen(ok), (int)strlen(ok), line, sizeof(line)));
|
|
TEST_ASSERT_EQUAL_STRING(ok, line); /* 不带结尾 0 的负载也被正确收尾 */
|
|
|
|
const char nul[] = "{\"cmd\":\"status\"}\0junk";
|
|
TEST_ASSERT_EQUAL_STRING("bad_frame",
|
|
mlink_inbound(nul, sizeof(nul) - 1, sizeof(nul) - 1, line, sizeof(line)));
|
|
TEST_ASSERT_EQUAL_STRING("bad_frame", mlink_inbound("{\n}", 3, 3, line, sizeof(line)));
|
|
TEST_ASSERT_EQUAL_STRING("too_long", mlink_inbound(ok, 5, 100, line, sizeof(line))); /* 切片 */
|
|
TEST_ASSERT_EQUAL_STRING("too_long", mlink_inbound(ok, 0, 0, line, sizeof(line)));
|
|
static char big[CMD_LINE_MAX + 8];
|
|
memset(big, 'x', sizeof(big));
|
|
TEST_ASSERT_EQUAL_STRING("too_long", mlink_inbound(big, CMD_LINE_MAX, CMD_LINE_MAX, line,
|
|
sizeof(line)));
|
|
TEST_ASSERT_NULL(mlink_inbound(big, CMD_LINE_MAX - 1, CMD_LINE_MAX - 1, line, sizeof(line)));
|
|
}
|
|
|
|
static void test_backoff_doubles_and_caps(void)
|
|
{
|
|
static const uint32_t want[] = { 1000, 2000, 4000, 8000, 16000, 32000, 60000, 60000 };
|
|
for (uint32_t i = 0; i < sizeof(want) / sizeof(want[0]); i++) {
|
|
TEST_ASSERT_EQUAL_UINT32(want[i], mlink_backoff_ms(i));
|
|
}
|
|
TEST_ASSERT_EQUAL_UINT32(MLINK_BACKOFF_MAX_MS, mlink_backoff_ms(0xFFFFFFFFu));
|
|
}
|
|
|
|
/* 遥测节拍:一连上先报一条;之后按整周期推进(被挤晚了不漂);落后一整周期以上
|
|
就对齐到现在、不补发;32 位毫秒回绕照样对。 */
|
|
static void test_telemetry_cadence(void)
|
|
{
|
|
mlink_tele_t t = { 0 };
|
|
TEST_ASSERT_TRUE(mlink_tele_due(&t, 5000, 1000));
|
|
TEST_ASSERT_FALSE(mlink_tele_due(&t, 5999, 1000));
|
|
TEST_ASSERT_TRUE(mlink_tele_due(&t, 6030, 1000)); /* 晚 30 ms */
|
|
TEST_ASSERT_EQUAL_UINT32(6000, t.last_ms); /* 相位没漂 */
|
|
TEST_ASSERT_FALSE(mlink_tele_due(&t, 6999, 1000));
|
|
TEST_ASSERT_TRUE(mlink_tele_due(&t, 12000, 1000)); /* 卡了 5 s */
|
|
TEST_ASSERT_EQUAL_UINT32(12000, t.last_ms); /* 对齐到现在,不补发 */
|
|
TEST_ASSERT_FALSE(mlink_tele_due(&t, 12500, 1000));
|
|
|
|
mlink_tele_t w = { 0 };
|
|
TEST_ASSERT_TRUE(mlink_tele_due(&w, 0xFFFFFF00u, 1000));
|
|
TEST_ASSERT_FALSE(mlink_tele_due(&w, 0x00000100u, 1000)); /* 回绕后才过了 512 ms */
|
|
TEST_ASSERT_TRUE(mlink_tele_due(&w, 0x00000300u, 1000));
|
|
}
|
|
|
|
int main(void)
|
|
{
|
|
UNITY_BEGIN();
|
|
RUN_TEST(test_config_over_usb_is_saved_and_applied);
|
|
RUN_TEST(test_config_over_wifi_is_refused_and_logged);
|
|
RUN_TEST(test_net_never_echoes_a_password);
|
|
RUN_TEST(test_config_validation);
|
|
RUN_TEST(test_unsaved_config_does_not_take_effect);
|
|
RUN_TEST(test_loaded_config_is_sanitised);
|
|
RUN_TEST(test_topics_follow_the_mac);
|
|
RUN_TEST(test_inbound_messages);
|
|
RUN_TEST(test_backoff_doubles_and_caps);
|
|
RUN_TEST(test_telemetry_cadence);
|
|
return UNITY_END();
|
|
}
|