文件
phonerail/firmware/test/test_net.c
T
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

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();
}