文件
phonerail/firmware/test/test_net.c
T
zhou yang和Claude Opus 5.5 9fb5bba367 固件 0.1.5~0.1.13-c5:刷机模式、多组 WiFi、热插拔初始化、总线锁误判、硬重启断 VBUS
- 0.1.5/0.1.6:刷机模式(flash_mode,最多 2 路,60 分钟可加钟)——窗口内顶高按 4450 mV 绝对值判、不断 VBUS
- 0.1.7/0.1.8:反灌断 VBUS 阈值放宽到 -300 mA(adb reboot bootloader 过渡实测 -208 mA)
- 0.1.9:WiFi 最多存 4 组,连不上轮换;NVS v1 自动迁移并双写(OTA 回滚仍能上网)
- 0.1.10:热插拔进来的刀片重新初始化 PCA9555 + INA226(否则 VBUS 常开、电流恒读 0)
- 0.1.11:i2cbus 嵌套检测改看本任务持锁(原全局标志让 key 随机回 busy、vbus 随机回 io)
- 0.1.12:硬重启先断 VBUS,开机校验通过 60 s 后再接回(否则假重启 / 误判没开机 / 进关机充电)
- 0.1.13:这一路被安全判据断电时一并退出刷机模式(事件 21 arg 254)

26 ctest 全绿;新增用例均先在旧代码上确认变红。板 B 已 OTA 到 0.1.13-c5 并实测。

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
2026-09-29 08:16:21 +08:00

394 行
17 KiB
C
原始文件 Blame 文件历史

此文件含有模棱两可的 Unicode 字符
此文件含有可能会与其他字符混淆的 Unicode 字符。 如果您是想特意这样的,可以安全地忽略该警告。 使用 Escape 按钮显示他们。
/* 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.wifi[0].ssid);
TEST_ASSERT_EQUAL_STRING("s3cret-pass", s_saved.wifi[0].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.wifi[1].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.wifi[0].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.wifi[0].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.wifi[0].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]);
}
/* ---- 多组 WiFi(0.1.9)--------------------------------------------------- */
/* ⭐ 加第二组不挤掉第一组;同名再发 = 改口令、不多占一格;回应带组数。 */
static void test_wifi_adds_and_updates_by_name(void)
{
ASSERT_HAS(send_on(CMD_LINK_USB, "{\"seq\":1,\"cmd\":\"wifi\",\"ssid\":\"home\",\"pass\":\"pass-home-1\"}"),
"\"n\":1");
ASSERT_HAS(send_on(CMD_LINK_USB, "{\"seq\":2,\"cmd\":\"wifi\",\"ssid\":\"hotspot\",\"pass\":\"pass-hot-1\"}"),
"\"n\":2");
ASSERT_HAS(send_on(CMD_LINK_USB, "{\"seq\":3,\"cmd\":\"wifi\",\"ssid\":\"home\",\"pass\":\"pass-home-2\"}"),
"\"n\":2");
netcfg_t c;
netcfg_get(&c);
TEST_ASSERT_EQUAL_INT(2, netcfg_wifi_count(&c));
TEST_ASSERT_EQUAL_STRING("home", c.wifi[0].ssid);
TEST_ASSERT_EQUAL_STRING("pass-home-2", c.wifi[0].pass);
TEST_ASSERT_EQUAL_STRING("hotspot", c.wifi[1].ssid);
TEST_ASSERT_EQUAL_STRING("pass-hot-1", c.wifi[1].pass);
TEST_ASSERT_EQUAL_MEMORY(&s_saved, &c, sizeof(c)); /* 存的是整张表 */
/* net:ssid = 正在连的那组(状态里 wifi_idx),ssids = 整张表;任何一组的口令都不出现。 */
net_status_t st;
memset(&st, 0, sizeof(st));
st.wifi_idx = 1;
net_status_set(&st);
const char *r = send_on(CMD_LINK_WIFI, "{\"cmd\":\"net\"}");
ASSERT_HAS(r, "\"ssid\":\"hotspot\",\"pass_set\":true,\"ssids\":[\"home\",\"hotspot\"]");
ASSERT_LACKS(r, "pass-home");
ASSERT_LACKS(r, "pass-hot");
}
/* ⭐ 删一组后面的往前补;删没有的 = range;满了再加新名字 = state、表不变;满了改已有的照样行。 */
static void test_wifi_delete_and_full_table(void)
{
char line[128];
for (int i = 0; i < NETCFG_WIFI_N; i++) {
snprintf(line, sizeof(line), "{\"seq\":%d,\"cmd\":\"wifi\",\"ssid\":\"n%d\",\"pass\":\"12345678\"}",
i + 1, i);
ASSERT_HAS(send_on(CMD_LINK_USB, line), "\"ok\":true");
}
ASSERT_HAS(send_on(CMD_LINK_USB, "{\"seq\":20,\"cmd\":\"wifi\",\"ssid\":\"extra\",\"pass\":\"12345678\"}"),
"\"err\":\"state\"");
ASSERT_HAS(send_on(CMD_LINK_USB, "{\"seq\":21,\"cmd\":\"wifi\",\"ssid\":\"n3\",\"pass\":\"87654321\"}"),
"\"ok\":true");
TEST_ASSERT_EQUAL_INT(NETCFG_WIFI_N + 1, s_saves);
ASSERT_HAS(send_on(CMD_LINK_USB, "{\"seq\":22,\"cmd\":\"wifi\",\"ssid\":\"n1\",\"del\":true}"),
"\"n\":3");
netcfg_t c;
netcfg_get(&c);
TEST_ASSERT_EQUAL_INT(NETCFG_WIFI_N - 1, netcfg_wifi_count(&c));
TEST_ASSERT_EQUAL_STRING("n0", c.wifi[0].ssid);
TEST_ASSERT_EQUAL_STRING("n2", c.wifi[1].ssid);
TEST_ASSERT_EQUAL_STRING("n3", c.wifi[2].ssid);
TEST_ASSERT_EQUAL_STRING("87654321", c.wifi[2].pass);
TEST_ASSERT_EQUAL_STRING("", c.wifi[3].ssid);
TEST_ASSERT_EQUAL_STRING("", c.wifi[3].pass); /* 挪走之后末尾清干净,不留口令 */
ASSERT_HAS(send_on(CMD_LINK_USB, "{\"seq\":23,\"cmd\":\"wifi\",\"ssid\":\"n1\",\"del\":true}"),
"\"err\":\"range\"");
ASSERT_HAS(send_on(CMD_LINK_USB, "{\"seq\":24,\"cmd\":\"wifi\",\"ssid\":\"n1\",\"del\":1}"),
"\"err\":\"arg\"");
/* 删也只收 USB。 */
ASSERT_HAS(send_on(CMD_LINK_WIFI, "{\"seq\":25,\"cmd\":\"wifi\",\"ssid\":\"n0\",\"del\":true}"),
"\"err\":\"link\"");
/* 删到空:net 回应仍是合法 JSON,ssids 是空数组。 */
for (int i = 0; i < 3; i++) {
snprintf(line, sizeof(line), "{\"seq\":%d,\"cmd\":\"wifi\",\"ssid\":\"%s\",\"del\":true}", 30 + i,
i == 0 ? "n0" : i == 1 ? "n2" : "n3");
ASSERT_HAS(send_on(CMD_LINK_USB, line), "\"ok\":true");
}
ASSERT_HAS(send_on(CMD_LINK_USB, "{\"cmd\":\"net\"}"), "\"ssid\":\"\",\"pass_set\":false,\"ssids\":[]");
}
/* NVS 读回来的表中间有空位:挤到前面,count 才算得对、轮换才不会停在空位上。 */
static void test_loaded_table_is_compacted(void)
{
netcfg_t in;
memset(&in, 0, sizeof(in));
strcpy(in.wifi[1].ssid, "b");
strcpy(in.wifi[1].pass, "12345678");
strcpy(in.wifi[3].ssid, "d");
strcpy(in.wifi[2].pass, "orphan-pass"); /* 空 SSID 带着口令:整格丢掉 */
netcfg_init(&in, NULL, NULL, NULL);
netcfg_t c;
netcfg_get(&c);
TEST_ASSERT_EQUAL_INT(2, netcfg_wifi_count(&c));
TEST_ASSERT_EQUAL_STRING("b", c.wifi[0].ssid);
TEST_ASSERT_EQUAL_STRING("12345678", c.wifi[0].pass);
TEST_ASSERT_EQUAL_STRING("d", c.wifi[1].ssid);
TEST_ASSERT_EQUAL_INT(1, netcfg_wifi_find(&c, "d"));
TEST_ASSERT_EQUAL_INT(-1, netcfg_wifi_find(&c, ""));
TEST_ASSERT_EQUAL_STRING("", c.wifi[2].pass);
}
/* ---- 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_wifi_adds_and_updates_by_name);
RUN_TEST(test_wifi_delete_and_full_table);
RUN_TEST(test_loaded_table_is_compacted);
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();
}