固件 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]>
这个提交包含在:
zhou yang
2026-09-29 08:16:21 +08:00
共同撰写人 Claude Opus 5.5
父节点 385d2f9c47
当前提交 9fb5bba367
共修改 35 个文件,包含 5120 行新增和 3675 行删除
+147 -1
查看文件
@@ -11,6 +11,7 @@
#include <string.h>
#include "board_pins.h"
#include "chgmode.h"
#include "cmd.h"
#include "dac63204.h"
#include "evlog.h"
@@ -112,6 +113,7 @@ void setUp(void)
t_thermal_set_floor(0);
TEST_ASSERT_EQUAL_INT(HAL_OK, evlog_init());
t_actuator_init(NULL, NULL);
chgmode_reset();
cmd_init(NULL);
/* 启动期独占,之后任何一次 I²C 事务没持锁就记违规 —— `set_voltage` 那一笔
@@ -186,7 +188,8 @@ static void test_cmd_numbers_are_frozen(void)
TEST_ASSERT_EQUAL_INT(11, CMD_MQTT);
TEST_ASSERT_EQUAL_INT(12, CMD_NET);
TEST_ASSERT_EQUAL_INT(13, CMD_OTA); /* C2 */
TEST_ASSERT_EQUAL_INT(14, CMD_COUNT);
TEST_ASSERT_EQUAL_INT(14, CMD_FLASH_MODE); /* 0.1.5 刷机模式 */
TEST_ASSERT_EQUAL_INT(15, CMD_COUNT);
TEST_ASSERT_EQUAL_INT(0, CMD_OK);
TEST_ASSERT_EQUAL_INT(1, CMD_ERR_PARSE);
@@ -690,6 +693,144 @@ static void test_small_output_buffer(void)
TEST_ASSERT_EQUAL_UINT16(300, t_thermal_floor());
}
/* ---- 0.1.5 刷机模式(flash_mode) ----------------------------------------- */
static void test_flash_mode_command(void)
{
/* 关着电的路不许开(与 vbus 同一条规矩),记拒绝事件。 */
ASSERT_HAS(send("{\"seq\":1,\"cmd\":\"flash_mode\",\"rail\":1,\"on\":true}"), "\"err\":\"state\"");
TEST_ASSERT_EQUAL_INT(1, count_events(LOG_EV_CMD_REJECT));
ASSERT_HAS(send("{\"seq\":2,\"cmd\":\"power\",\"rail\":1,\"on\":true}"), "\"ok\":true");
ASSERT_HAS(send("{\"seq\":3,\"cmd\":\"flash_mode\",\"rail\":1,\"on\":true}"), "\"left\":3600"); /* 默认 60 分钟 */
TEST_ASSERT_TRUE(chgmode_active(1, hal_time_now_ms()));
evlog_rec_t r;
TEST_ASSERT_TRUE(find_event(LOG_EV_CHG_MODE, 0, &r));
TEST_ASSERT_EQUAL_UINT8(1, r.rail);
TEST_ASSERT_EQUAL_UINT8(CHG_MODE_DEFAULT_MIN, r.arg);
/* status 每一路报剩余秒数 */
ASSERT_HAS(send("{\"cmd\":\"status\"}"), "\"chg\":3600");
/* 越界 / 参数错 */
ASSERT_HAS(send("{\"seq\":4,\"cmd\":\"flash_mode\",\"rail\":1,\"on\":true,\"min\":0}"), "\"err\":\"range\"");
ASSERT_HAS(send("{\"seq\":5,\"cmd\":\"flash_mode\",\"rail\":1,\"on\":true,\"min\":121}"), "\"err\":\"range\"");
ASSERT_HAS(send("{\"seq\":6,\"cmd\":\"flash_mode\",\"rail\":1,\"on\":false,\"min\":5}"), "\"err\":\"arg\"");
ASSERT_HAS(send("{\"seq\":7,\"cmd\":\"flash_mode\",\"rail\":1}"), "\"err\":\"arg\"");
/* 续期:本路再开一次不占新名额 */
ASSERT_HAS(send("{\"seq\":8,\"cmd\":\"flash_mode\",\"rail\":1,\"on\":true,\"min\":120}"), "\"left\":7200");
/* 第三路被拒 */
ASSERT_HAS(send("{\"seq\":9,\"cmd\":\"power\",\"rail\":2,\"on\":true}"), "\"ok\":true");
ASSERT_HAS(send("{\"seq\":10,\"cmd\":\"power\",\"rail\":3,\"on\":true}"), "\"ok\":true");
ASSERT_HAS(send("{\"seq\":11,\"cmd\":\"flash_mode\",\"rail\":2,\"on\":true,\"min\":10}"), "\"ok\":true");
ASSERT_HAS(send("{\"seq\":12,\"cmd\":\"flash_mode\",\"rail\":3,\"on\":true}"), "\"err\":\"state\"");
TEST_ASSERT_FALSE(chgmode_active(3, hal_time_now_ms()));
/* 关:回 left 0、记一条 arg=0;腾出的名额第三路就能用了 */
ASSERT_HAS(send("{\"seq\":13,\"cmd\":\"flash_mode\",\"rail\":1,\"on\":false}"), "\"left\":0");
TEST_ASSERT_TRUE(find_event(LOG_EV_CHG_MODE, 3, &r)); /* 开1、续期、开2、关1 */
TEST_ASSERT_EQUAL_UINT8(CHG_EV_OFF, r.arg);
ASSERT_HAS(send("{\"seq\":14,\"cmd\":\"flash_mode\",\"rail\":3,\"on\":true}"), "\"ok\":true");
}
/* 关这一路电 = 刷机模式一并退出(不白占名额),记一条 arg=0。 */
static void test_power_off_ends_flash_mode(void)
{
ASSERT_HAS(send("{\"seq\":1,\"cmd\":\"power\",\"rail\":4,\"on\":true}"), "\"ok\":true");
ASSERT_HAS(send("{\"seq\":2,\"cmd\":\"flash_mode\",\"rail\":4,\"on\":true}"), "\"ok\":true");
ASSERT_HAS(send("{\"seq\":3,\"cmd\":\"power\",\"rail\":4,\"on\":false}"), "\"ok\":true");
TEST_ASSERT_FALSE(chgmode_active(4, hal_time_now_ms()));
TEST_ASSERT_EQUAL_INT(2, count_events(LOG_EV_CHG_MODE));
evlog_rec_t r;
TEST_ASSERT_TRUE(find_event(LOG_EV_CHG_MODE, 1, &r));
TEST_ASSERT_EQUAL_UINT8(CHG_EV_OFF, r.arg);
}
/* 自然到期:执行器循环里的 cmd_chgmode_tick 记**一条** arg=255,之后不再重复记。 */
static void test_flash_mode_expiry_is_logged_once(void)
{
ASSERT_HAS(send("{\"seq\":1,\"cmd\":\"power\",\"rail\":5,\"on\":true}"), "\"ok\":true");
ASSERT_HAS(send("{\"seq\":2,\"cmd\":\"flash_mode\",\"rail\":5,\"on\":true,\"min\":1}"), "\"left\":60");
cmd_chgmode_tick(hal_time_now_ms());
TEST_ASSERT_EQUAL_INT(1, count_events(LOG_EV_CHG_MODE));
sim_time_advance_ms(60000 + CHG_MODE_GRACE_MS);
cmd_chgmode_tick(hal_time_now_ms());
cmd_chgmode_tick(hal_time_now_ms() + 100);
TEST_ASSERT_EQUAL_INT(2, count_events(LOG_EV_CHG_MODE));
evlog_rec_t r;
TEST_ASSERT_TRUE(find_event(LOG_EV_CHG_MODE, 1, &r));
TEST_ASSERT_EQUAL_UINT8(5, r.rail);
TEST_ASSERT_EQUAL_UINT8(CHG_EV_EXPIRED, r.arg);
ASSERT_HAS(send("{\"cmd\":\"status\"}"), "\"chg\":0");
}
/* 0.1.13:安全判据 / 拔刀片把这一路关了(不经过 power 命令)⇒ 下一次 tick 退出刷机模式、记 arg=254、
让出名额;硬重启中间 eFuse 关着的那几秒不算。 */
static void test_flash_mode_ends_when_the_rail_loses_power(void)
{
ASSERT_HAS(send("{\"seq\":1,\"cmd\":\"power\",\"rail\":1,\"on\":true}"), "\"ok\":true");
ASSERT_HAS(send("{\"seq\":2,\"cmd\":\"power\",\"rail\":2,\"on\":true}"), "\"ok\":true");
ASSERT_HAS(send("{\"seq\":3,\"cmd\":\"power\",\"rail\":3,\"on\":true}"), "\"ok\":true");
ASSERT_HAS(send("{\"seq\":4,\"cmd\":\"flash_mode\",\"rail\":1,\"on\":true}"), "\"ok\":true");
ASSERT_HAS(send("{\"seq\":5,\"cmd\":\"flash_mode\",\"rail\":2,\"on\":true}"), "\"ok\":true");
ASSERT_HAS(send("{\"seq\":6,\"cmd\":\"flash_mode\",\"rail\":3,\"on\":true}"), "\"err\":\"state\"");
/* 硬重启:HR_OFF 期间 eFuse 关着,但这是序列的一部分 —— 不退。 */
ASSERT_HAS(send("{\"seq\":7,\"cmd\":\"power_cycle\",\"rail\":1}"), "\"ok\":true");
TEST_ASSERT_FALSE(rail_efuse_is_on(1));
cmd_chgmode_tick(hal_time_now_ms());
TEST_ASSERT_TRUE(chgmode_active(1, hal_time_now_ms()));
/* 第 2 路被 t_safety 关了(这里直接拉低 EN 扮演):下一次 tick 退出。 */
TEST_ASSERT_EQUAL_INT(HAL_OK, rail_efuse_enable(2, false));
const int before = count_events(LOG_EV_CHG_MODE);
cmd_chgmode_tick(hal_time_now_ms());
TEST_ASSERT_FALSE(chgmode_active(2, hal_time_now_ms()));
TEST_ASSERT_EQUAL_INT(before + 1, count_events(LOG_EV_CHG_MODE));
evlog_rec_t r;
TEST_ASSERT_TRUE(find_event(LOG_EV_CHG_MODE, before, &r));
TEST_ASSERT_EQUAL_UINT8(2, r.rail);
TEST_ASSERT_EQUAL_UINT8(CHG_EV_DROPPED, r.arg);
cmd_chgmode_tick(hal_time_now_ms() + 100);
TEST_ASSERT_EQUAL_INT(before + 1, count_events(LOG_EV_CHG_MODE)); /* 只记一次 */
/* 名额让出来了。 */
ASSERT_HAS(send("{\"seq\":8,\"cmd\":\"flash_mode\",\"rail\":3,\"on\":true}"), "\"ok\":true");
}
/* 0.1.6 加钟:只给开着的窗口加;剩余最多 120 分钟(超出截断);与 on / min 互斥;事件 arg = 0x80 | 分钟。 */
static void test_flash_mode_extend(void)
{
ASSERT_HAS(send("{\"seq\":1,\"cmd\":\"power\",\"rail\":2,\"on\":true}"), "\"ok\":true");
/* 没开着 ⇒ state(要重新开,不是加钟) */
ASSERT_HAS(send("{\"seq\":2,\"cmd\":\"flash_mode\",\"rail\":2,\"add\":30}"), "\"err\":\"state\"");
ASSERT_HAS(send("{\"seq\":3,\"cmd\":\"flash_mode\",\"rail\":2,\"on\":true,\"min\":20}"), "\"left\":1200");
sim_time_advance_ms(5 * 60000); /* 过了 5 分钟,还剩 15 */
ASSERT_HAS(send("{\"seq\":4,\"cmd\":\"flash_mode\",\"rail\":2,\"add\":30}"), "\"left\":2700"); /* 15 + 30 */
evlog_rec_t r;
TEST_ASSERT_TRUE(find_event(LOG_EV_CHG_MODE, 1, &r));
TEST_ASSERT_EQUAL_UINT8(CHG_EV_EXTEND | 30u, r.arg);
/* 截断到 120 分钟 */
ASSERT_HAS(send("{\"seq\":5,\"cmd\":\"flash_mode\",\"rail\":2,\"add\":120}"), "\"left\":7200");
/* 越界 / 混用 */
ASSERT_HAS(send("{\"seq\":6,\"cmd\":\"flash_mode\",\"rail\":2,\"add\":0}"), "\"err\":\"range\"");
ASSERT_HAS(send("{\"seq\":7,\"cmd\":\"flash_mode\",\"rail\":2,\"add\":121}"), "\"err\":\"range\"");
ASSERT_HAS(send("{\"seq\":8,\"cmd\":\"flash_mode\",\"rail\":2,\"add\":10,\"on\":true}"), "\"err\":\"arg\"");
ASSERT_HAS(send("{\"seq\":9,\"cmd\":\"flash_mode\",\"rail\":2,\"add\":10,\"min\":10}"), "\"err\":\"arg\"");
ASSERT_HAS(send("{\"seq\":10,\"cmd\":\"flash_mode\",\"rail\":2,\"add\":true}"), "\"err\":\"arg\"");
/* 加钟不占新名额:两路都开着时照样能加 */
ASSERT_HAS(send("{\"seq\":11,\"cmd\":\"power\",\"rail\":3,\"on\":true}"), "\"ok\":true");
ASSERT_HAS(send("{\"seq\":12,\"cmd\":\"flash_mode\",\"rail\":3,\"on\":true}"), "\"ok\":true");
ASSERT_HAS(send("{\"seq\":13,\"cmd\":\"flash_mode\",\"rail\":3,\"add\":10}"), "\"left\":4200");
}
int main(void)
{
UNITY_BEGIN();
@@ -709,6 +850,11 @@ int main(void)
RUN_TEST(test_busy_rail_refuses_a_second_sequence);
RUN_TEST(test_reboots_are_logged_with_kind_and_link);
RUN_TEST(test_vbus_needs_the_rail_powered);
RUN_TEST(test_flash_mode_command);
RUN_TEST(test_power_off_ends_flash_mode);
RUN_TEST(test_flash_mode_expiry_is_logged_once);
RUN_TEST(test_flash_mode_ends_when_the_rail_loses_power);
RUN_TEST(test_flash_mode_extend);
RUN_TEST(test_power_off_drops_vbus_and_always_drops_the_efuse);
RUN_TEST(test_power_records_intent_only_when_it_took_effect);
RUN_TEST(test_fan_floor);
+2 -1
查看文件
@@ -88,7 +88,8 @@ static void test_flash_format_is_frozen(void)
TEST_ASSERT_EQUAL_INT(18, LOG_EV_OTA); /* C2 */
TEST_ASSERT_EQUAL_INT(19, LOG_EV_BOOT_CHECK); /* 0.1.2 开机校验 */
TEST_ASSERT_EQUAL_INT(20, LOG_EV_BLADE_MUTE); /* 0.1.4 刀片不应答 */
TEST_ASSERT_EQUAL_INT(21, LOG_EV_COUNT);
TEST_ASSERT_EQUAL_INT(21, LOG_EV_CHG_MODE); /* 0.1.5 刷机模式 */
TEST_ASSERT_EQUAL_INT(22, LOG_EV_COUNT);
}
/* §10.7 的「必记事件」有八类,A10 只产生其中两类。剩下的号先占住,
+31
查看文件
@@ -65,6 +65,19 @@ static hal_err_t cb_recover_inside(void *arg)
return HAL_OK;
}
/* 事务进行中,**另一个任务**也来要总线(sim 的假任务号 1 扮演执行器)。 */
static hal_err_t cb_other_task(void *arg)
{
ctx_t *c = (ctx_t *)arg;
c->calls++;
static ctx_t inner_ctx;
inner_ctx.addr = BLADE_INA;
sim_lock_set_task(1);
c->inner = i2cbus_xact(1, 5, cb_read_mv, &inner_ctx);
sim_lock_set_task(0);
return HAL_OK;
}
/* 在事务里再调一次 `i2cbus_xact()` —— 头文件末尾那条警告。 */
static hal_err_t cb_nested(void *arg)
{
@@ -444,6 +457,23 @@ static void test_recover_is_rejected_inside_a_transaction(void)
不拦的话会在同一个任务里第二次 take 同一把锁 —— sim 上是 HAL_ERR_STATE,
FreeRTOS 上(非递归的 mutex)就是那个任务当场停住,
现象是 `t_safety` 整个不跑了,而看门狗之外没有任何日志。 */
/* ⭐ 0.1.11:别的任务正持着锁 ≠ 嵌套。它该**等锁**(超时回 TIMEOUT),而不是回 STATE。
原来的全局标志把两者混为一谈,执行器撞上巡检就回 STATE ⇒ `key` 随机 busy、`vbus` 随机 io。 */
static void test_another_tasks_xact_waits_instead_of_looking_nested(void)
{
mount_ready();
ctx_t c = { BLADE_INA, 0, -1, HAL_OK };
TEST_ASSERT_EQUAL_INT(HAL_OK, i2cbus_xact(0, 10, cb_other_task, &c));
TEST_ASSERT_EQUAL_UINT32(1, c.calls);
TEST_ASSERT_EQUAL_INT(HAL_ERR_TIMEOUT, c.inner);
TEST_ASSERT_FALSE(hal_lock_held(HAL_LOCK_I2C));
/* 锁放了之后,那个任务再来就拿得到。 */
sim_lock_set_task(1);
ctx_t d = { BLADE_INA, 0, -1, HAL_OK };
TEST_ASSERT_EQUAL_INT(HAL_OK, i2cbus_xact(1, 5, cb_read_mv, &d));
sim_lock_set_task(0);
}
static void test_a_nested_xact_is_rejected_and_does_not_deadlock(void)
{
mount_ready();
@@ -604,6 +634,7 @@ int main(void)
RUN_TEST(test_recover_pulses_mux_rst_and_forgets_the_channel);
RUN_TEST(test_recover_is_rejected_inside_a_transaction);
RUN_TEST(test_a_nested_xact_is_rejected_and_does_not_deadlock);
RUN_TEST(test_another_tasks_xact_waits_instead_of_looking_nested);
RUN_TEST(test_a_lock_timeout_changes_nothing_at_all);
RUN_TEST(test_xact_rejects_bad_arguments_before_taking_the_lock);
RUN_TEST(test_forget_channel_only_drops_the_shadow);
+104 -6
查看文件
@@ -97,8 +97,8 @@ static void test_config_over_usb_is_saved_and_applied(void)
"\"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("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);
@@ -172,7 +172,7 @@ static void test_config_validation(void)
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_STRING(pass, s_saved.wifi[1].pass);
TEST_ASSERT_EQUAL_INT(3, s_saves);
}
@@ -186,7 +186,7 @@ static void test_unsaved_config_does_not_take_effect(void)
"\"err\":\"io\"");
netcfg_t c;
netcfg_get(&c);
TEST_ASSERT_EQUAL_STRING("old", c.ssid);
TEST_ASSERT_EQUAL_STRING("old", c.wifi[0].ssid);
TEST_ASSERT_EQUAL_INT(1, s_changed);
}
@@ -197,11 +197,11 @@ 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);
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.ssid));
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);
@@ -210,6 +210,101 @@ static void test_loaded_config_is_sanitised(void)
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)
@@ -287,6 +382,9 @@ int main(void)
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);
+110
查看文件
@@ -1231,6 +1231,113 @@ static void test_boot_check_first_press_boots(void)
TEST_ASSERT_EQUAL_UINT32(0, sim_lock_violations(HAL_LOCK_I2C));
}
/* ⭐ 0.1.11(板 B 实测):VBUS 通着做硬重启 —— 手机靠 USB 撑着没断电、直供电流 ≈ 0 ⇒
校验判「没开起来」再按两下、断电时带着 USB 进关机充电。现在:断电前先断 VBUS,
整个开机 + 校验期间都断着,开机校验通过才接回。 */
static void test_hard_reset_cuts_vbus_first_and_restores_it_after_boot(void)
{
const int rail = 4;
s_pg_val[rail - 1] = true;
phone_up(1, false);
TEST_ASSERT_EQUAL_INT(HAL_OK, slot_vbus_enable(rail, true));
TEST_ASSERT_EQUAL_INT(HAL_OK, t_actuator_hard_reset(rail));
TEST_ASSERT_EQUAL_INT(0, pin(rail, PCA9555_BIT_VBUS_EN)); /* 断电之前就断了 */
run_phone_ms(rail, HR_TO_PRESS_MS + ACT_HR_BOOT_KEY_MS + 10);
TEST_ASSERT_EQUAL_INT(ACT_ST_HR_VERIFY, t_actuator_state(rail));
TEST_ASSERT_EQUAL_INT(0, pin(rail, PCA9555_BIT_VBUS_EN)); /* 校验期间也断着 */
for (int i = 0; i < 1000 && t_actuator_busy(rail); i++) {
run_phone_ms(rail, 10); /* 走到校验刚结束的那一刻,从这里起算 60 s */
}
TEST_ASSERT_EQUAL_INT(ACT_RES_OK, t_actuator_last_result(rail));
TEST_ASSERT_TRUE(s_boot_booted);
/* 开起来了,但**还不接回**:这时手机在 bootloader / 内核早期,关充电补丁没生效,
接回 USB 就被充到 4.4 V(板 B 实测 13 s 顶高断 VBUS)。执行器已空闲、不挡命令。 */
TEST_ASSERT_FALSE(t_actuator_busy(rail));
TEST_ASSERT_TRUE(t_actuator_vbus_back_pending(rail));
TEST_ASSERT_EQUAL_INT(0, pin(rail, PCA9555_BIT_VBUS_EN));
run_phone_ms(rail, ACT_HR_VBUS_BACK_MS - 1000);
TEST_ASSERT_EQUAL_INT(0, pin(rail, PCA9555_BIT_VBUS_EN));
run_phone_ms(rail, 1500);
TEST_ASSERT_EQUAL_INT(1, pin(rail, PCA9555_BIT_VBUS_EN)); /* 到点:原样接回 */
TEST_ASSERT_TRUE(slot_vbus_is_on(rail));
TEST_ASSERT_FALSE(t_actuator_vbus_back_pending(rail));
}
/* 等接回的这 60 s 里:有人手动动了 VBUS / 电,或者这一路被关了电 ⇒ 不再接回。 */
static void test_a_pending_vbus_restore_is_dropped_when_someone_else_acts(void)
{
const int rail = 1;
s_pg_val[rail - 1] = true;
/* ① 人手动断 VBUS(cmd 层会调 cancel)。 */
phone_up(1, false);
TEST_ASSERT_EQUAL_INT(HAL_OK, slot_vbus_enable(rail, true));
TEST_ASSERT_EQUAL_INT(HAL_OK, t_actuator_hard_reset(rail));
run_phone_ms(rail, HR_TO_PRESS_MS + ONE_TRY_MS + 100);
TEST_ASSERT_TRUE(t_actuator_vbus_back_pending(rail));
t_actuator_cancel_vbus_back(rail);
run_phone_ms(rail, ACT_HR_VBUS_BACK_MS + 1000);
TEST_ASSERT_EQUAL_INT(0, pin(rail, PCA9555_BIT_VBUS_EN));
/* ② 这一路在等的时候被关了电(安全判据 / 拔刀片):到点也不接。 */
phone_up(1, false);
TEST_ASSERT_EQUAL_INT(HAL_OK, slot_vbus_enable(rail, true));
TEST_ASSERT_EQUAL_INT(HAL_OK, t_actuator_hard_reset(rail));
run_phone_ms(rail, HR_TO_PRESS_MS + ONE_TRY_MS + 100);
TEST_ASSERT_TRUE(t_actuator_vbus_back_pending(rail));
TEST_ASSERT_EQUAL_INT(HAL_OK, rail_efuse_enable(rail, false));
run_phone_ms(rail, ACT_HR_VBUS_BACK_MS + 1000);
TEST_ASSERT_EQUAL_INT(0, pin(rail, PCA9555_BIT_VBUS_EN));
TEST_ASSERT_FALSE(t_actuator_vbus_back_pending(rail));
/* ③ 再来一次硬重启:上一次排的接回作废(新的一次从头算)。 */
TEST_ASSERT_EQUAL_INT(HAL_OK, rail_efuse_enable(rail, true));
phone_up(1, false);
TEST_ASSERT_EQUAL_INT(HAL_OK, slot_vbus_enable(rail, true));
TEST_ASSERT_EQUAL_INT(HAL_OK, t_actuator_hard_reset(rail));
run_phone_ms(rail, HR_TO_PRESS_MS + ONE_TRY_MS + 100);
TEST_ASSERT_TRUE(t_actuator_vbus_back_pending(rail));
phone_up(1, false);
TEST_ASSERT_EQUAL_INT(HAL_OK, t_actuator_hard_reset(rail)); /* VBUS 这时是断的 */
TEST_ASSERT_FALSE(t_actuator_vbus_back_pending(rail));
run_phone_ms(rail, HR_TO_PRESS_MS + ONE_TRY_MS + ACT_HR_VBUS_BACK_MS + 1000);
TEST_ASSERT_EQUAL_INT(0, pin(rail, PCA9555_BIT_VBUS_EN));
}
/* 没开起来 / 被打断:VBUS **不接回** —— 接回去就是让一台没开机的手机进关机充电。
开始时本来就没通的,成功了也不许凭空打开。 */
static void test_hard_reset_leaves_vbus_off_unless_the_phone_booted(void)
{
const int rail = 2;
s_pg_val[rail - 1] = true;
phone_up(0, false);
TEST_ASSERT_EQUAL_INT(HAL_OK, slot_vbus_enable(rail, true));
TEST_ASSERT_EQUAL_INT(HAL_OK, t_actuator_hard_reset(rail));
run_phone_ms(rail, HR_TO_PRESS_MS + 3 * ONE_TRY_MS + 1000);
TEST_ASSERT_EQUAL_INT(ACT_RES_NO_BOOT, t_actuator_last_result(rail));
TEST_ASSERT_EQUAL_INT(0, pin(rail, PCA9555_BIT_VBUS_EN));
TEST_ASSERT_FALSE(slot_vbus_is_on(rail));
TEST_ASSERT_EQUAL_INT(HAL_OK, slot_vbus_enable(rail, true));
TEST_ASSERT_EQUAL_INT(HAL_OK, t_actuator_hard_reset(rail));
run_phone_ms(rail, ACT_HR_OFF_MS / 2);
t_actuator_abort(rail);
run_phone_ms(rail, ONE_TRY_MS);
TEST_ASSERT_EQUAL_INT(ACT_RES_ABORTED, t_actuator_last_result(rail));
TEST_ASSERT_EQUAL_INT(0, pin(rail, PCA9555_BIT_VBUS_EN));
const int r5 = 5;
s_pg_val[r5 - 1] = true;
phone_up(1, false);
TEST_ASSERT_FALSE(slot_vbus_is_on(r5));
TEST_ASSERT_EQUAL_INT(HAL_OK, t_actuator_hard_reset(r5));
run_phone_ms(r5, HR_TO_PRESS_MS + ONE_TRY_MS + 1000);
TEST_ASSERT_EQUAL_INT(ACT_RES_OK, t_actuator_last_result(r5));
TEST_ASSERT_EQUAL_INT(0, pin(r5, PCA9555_BIT_VBUS_EN));
}
/* ⭐ 这次的主症状:第一下没压上,第二下开了。 */
static void test_boot_check_presses_again_when_the_phone_stays_off(void)
{
@@ -1375,6 +1482,9 @@ int main(void)
RUN_TEST(test_actuator_and_safety_share_one_lock_without_starving);
RUN_TEST(test_boot_check_first_press_boots);
RUN_TEST(test_hard_reset_cuts_vbus_first_and_restores_it_after_boot);
RUN_TEST(test_hard_reset_leaves_vbus_off_unless_the_phone_booted);
RUN_TEST(test_a_pending_vbus_restore_is_dropped_when_someone_else_acts);
RUN_TEST(test_boot_check_presses_again_when_the_phone_stays_off);
RUN_TEST(test_boot_check_gives_up_after_three_presses_and_keeps_power);
RUN_TEST(test_boot_check_does_not_press_again_when_current_is_unreadable);
文件差异内容过多而无法显示 加载差异
文件差异内容过多而无法显示 加载差异