固件 0.1.19~0.1.21-c5:配网热点(SoftAP + 网页)、风扇 40 ℃ 以下断电

- 0.1.19 配网热点:没配 WiFi / 连续 60 s 连不上时自己开 SoftAP,192.168.4.1 网页里加删 WiFi、改 MQTT;
  配置拼成 wifi / mqtt 命令走唯一的命令处理器(新链路号 3 CMD_LINK_AP),MQTT 仍不收配置命令
- 0.1.20 热点口令固定 88888888
- 0.1.21 风扇:板温 >=40 ℃ 上电、<38 ℃ 断电,40~65 ℃ 线性调速;fan 命令加可选 sec(下限到点自动交回)
- mqtt 命令加 keep_pass;net 回应加 ap;status 的 fan 加 left
- 新增 test_prov(13 例),test_t_thermal +3;29 ctest 全绿

Co-Authored-By: Claude Opus 5.5 <[email protected]>
这个提交包含在:
zhou yang
2026-10-06 14:35:44 +08:00
共同撰写人 Claude Opus 5.5
父节点 3bf4548c62
当前提交 ece65f7c19
共修改 24 个文件,包含 1925 行新增和 324 行删除
+149 -5
查看文件
@@ -83,7 +83,7 @@ void setUp(void)
{
sim_hal_reset();
fans_up();
set_temp(25000); /* 25 °C:常温开机 */
set_temp(T_THERMAL_TEMP_LO_MC); /* 曲线起点:风扇开着、按下限转(再凉就断电了,见最后一组用例) */
t_thermal_set_floor(0); /* 下限是模块级静态量,不随 init 清零(t_thermal.h) */
}
void tearDown(void) {}
@@ -186,7 +186,7 @@ static void test_unreadable_temperature_means_full_speed(void)
{
thermal_snap_t s;
boot(&s);
set_temp(25000);
set_temp(T_THERMAL_TEMP_LO_MC);
run(&s, 1);
TEST_ASSERT_EQUAL_UINT16(T_THERMAL_DUTY_MIN, s.duty);
@@ -205,7 +205,7 @@ static void test_manual_floor_only_raises_the_duty(void)
{
thermal_snap_t s;
boot(&s);
set_temp(25000);
set_temp(T_THERMAL_TEMP_LO_MC);
t_thermal_set_floor(800);
run(&s, 1);
TEST_ASSERT_EQUAL_UINT16(800, s.duty);
@@ -215,7 +215,7 @@ static void test_manual_floor_only_raises_the_duty(void)
TEST_ASSERT_EQUAL_UINT16(1000, s.duty);
t_thermal_set_floor(T_THERMAL_DUTY_MIN / 2); /* 比曲线下限还低:没有效果 */
set_temp(25000);
set_temp(T_THERMAL_TEMP_LO_MC);
run(&s, 1);
TEST_ASSERT_EQUAL_UINT16(T_THERMAL_DUTY_MIN, s.duty);
@@ -253,7 +253,7 @@ static void test_low_duty_windows_are_not_mistaken_for_a_stall(void)
{
thermal_snap_t s;
boot(&s);
set_temp(25000); /* 凉 ⇒ 占空比压到下限 */
set_temp(T_THERMAL_TEMP_LO_MC); /* 凉 ⇒ 占空比压到下限 */
run(&s, 1);
TEST_ASSERT_EQUAL_UINT16(T_THERMAL_DUTY_MIN, s.duty);
@@ -476,6 +476,147 @@ static void test_step_before_init_is_refused(void)
TEST_ASSERT_EQUAL_INT(HAL_ERR_ARG, t_thermal_step(NULL));
}
/* ---- ⑤ 0.1.21:凉了断电 -------------------------------------------------- */
/* ⭐ 40 °C 以下风扇断电:撤的是 `FAN_EN`(四线风扇 0 % PWM 仍在转),并且带迟滞 ——
38 °C 以下才关、回到 40 °C 才开,中间维持原状。
写错了会怎样:没有迟滞 ⇒ 板温在 40 °C 上下晃时风扇几秒启停一次;只压占空比不断电 ⇒ 风扇照转。 */
static void test_cool_board_cuts_fan_power_with_hysteresis(void)
{
thermal_snap_t s;
boot(&s);
TEST_ASSERT_TRUE(s.fan_en);
TEST_ASSERT_EQUAL_UINT16(T_THERMAL_DUTY_MIN, s.duty);
TEST_ASSERT_EQUAL_UINT32(1, s.fan_starts);
set_temp(T_THERMAL_FAN_OFF_MC); /* 正好在关断点上:还开着(低于才关) */
run(&s, 3);
TEST_ASSERT_TRUE(s.fan_en);
TEST_ASSERT_TRUE(sim_fan_rpm(0) > 0);
set_temp(T_THERMAL_FAN_OFF_MC - 1);
run(&s, 1);
TEST_ASSERT_FALSE(s.fan_en);
TEST_ASSERT_EQUAL_INT_MESSAGE(0, sim_gpio_level(BOARD_PIN_FAN_EN), "凉了没撤 FAN_EN");
TEST_ASSERT_EQUAL_UINT32_MESSAGE(0, sim_fan_rpm(0), "风扇还在转");
TEST_ASSERT_EQUAL_UINT32(0, sim_fan_rpm(1));
TEST_ASSERT_EQUAL_UINT16_MESSAGE(0, s.duty, "断着电时遥测里的占空比该是 0");
set_temp(T_THERMAL_FAN_ON_MC - 1); /* 回到两个门限之间:不开 */
run(&s, 5);
TEST_ASSERT_FALSE(s.fan_en);
TEST_ASSERT_EQUAL_UINT32(1, s.fan_starts);
set_temp(T_THERMAL_FAN_ON_MC);
run(&s, 1);
TEST_ASSERT_TRUE(s.fan_en);
TEST_ASSERT_EQUAL_INT(1, sim_gpio_level(BOARD_PIN_FAN_EN));
TEST_ASSERT_TRUE(sim_fan_rpm(0) > 0);
TEST_ASSERT_EQUAL_UINT16(T_THERMAL_DUTY_MIN, s.duty);
TEST_ASSERT_EQUAL_UINT32(2, s.fan_starts);
/* 在门限之间来回晃 20 圈:一次都不许再启停。 */
for (int i = 0; i < 20; i++) {
set_temp((i & 1) ? T_THERMAL_FAN_ON_MC - 100 : T_THERMAL_FAN_OFF_MC + 100);
run(&s, 1);
}
TEST_ASSERT_TRUE(s.fan_en);
TEST_ASSERT_EQUAL_UINT32_MESSAGE(2, s.fan_starts, "门限之间晃动引起了启停");
}
/* ⭐ 断电期间不报停转;重新上电先过启动宽限;转起来之后判据照常工作。
一只**本来就没插**的风扇(一直判停转)不因为每次启停再各记一条事件。 */
static void test_power_cycling_the_fan_does_not_raise_stall_alarms(void)
{
TEST_ASSERT_EQUAL_INT(HAL_OK, evlog_init());
evlog_flush(0);
const uint32_t seq = next_seq();
thermal_snap_t s;
sim_fan_set_stalled(1, true); /* 第二只没插 */
boot(&s);
set_temp(70000);
run(&s, T_THERMAL_TACH_ON_MS / 1000u + 2u);
TEST_ASSERT_FALSE(s.fan[0].stalled);
TEST_ASSERT_TRUE(s.fan[1].stalled);
TEST_ASSERT_EQUAL_UINT32(1, s.fan[1].stalls);
for (int k = 0; k < 3; k++) {
set_temp(30000); /* 凉:断电 */
run(&s, 40); /* 比任何判据窗口都长 */
TEST_ASSERT_FALSE(s.fan_en);
TEST_ASSERT_FALSE_MESSAGE(s.fan[0].stalled, "断着电被判成了停转");
TEST_ASSERT_TRUE(s.fan[0].rpm_valid);
TEST_ASSERT_EQUAL_UINT32_MESSAGE(0, s.fan[0].rpm, "断着电转速没报 0");
set_temp(70000); /* 热:上电满速 */
run(&s, T_THERMAL_SPINUP_MS / 1000u + T_THERMAL_TACH_ON_MS / 1000u + 3u);
TEST_ASSERT_TRUE(s.fan_en);
TEST_ASSERT_FALSE(s.fan[0].stalled);
TEST_ASSERT_TRUE_MESSAGE(s.fan[0].rpm > 5000, "重新上电后转速没恢复");
}
TEST_ASSERT_EQUAL_UINT32(0, s.fan[0].stalls);
TEST_ASSERT_EQUAL_UINT32_MESSAGE(1, s.fan[1].stalls, "没插的那只每次启停都又记了一次");
evlog_flush(0);
TEST_ASSERT_EQUAL_UINT32_MESSAGE(seq + 1, next_seq(), "启停期间多记了事件");
/* 上着电时真停转照样抓得到(判据没有被「断电不判」顺手关掉)。 */
sim_fan_set_stalled(0, true);
run(&s, T_THERMAL_TACH_ON_MS / 1000u + 2u);
TEST_ASSERT_TRUE(s.fan[0].stalled);
}
/* ⭐ 凉着也必须有风的两种情况:温度读不到(§10.10)、上位机给了下限。
带时限的下限到期自动回落 —— 上位机挂了,风扇不会永远停在它最后要的转速上。 */
static void test_cool_fan_still_runs_when_asked_or_blind(void)
{
thermal_snap_t s;
boot(&s);
set_temp(30000);
run(&s, 2);
TEST_ASSERT_FALSE(s.fan_en);
s_temp_ok[0] = false;
s_temp_ok[1] = false;
run(&s, 1);
TEST_ASSERT_TRUE_MESSAGE(s.fan_en, "温度读不到时风扇没开");
TEST_ASSERT_EQUAL_UINT16(1000, s.duty);
TEST_ASSERT_TRUE(sim_fan_rpm(0) > 0);
set_temp(30000);
run(&s, 1);
TEST_ASSERT_FALSE(s.fan_en);
t_thermal_set_floor_for(600, 30000); /* 30 s */
run(&s, 1);
TEST_ASSERT_TRUE_MESSAGE(s.fan_en, "上位机要风时风扇没开");
TEST_ASSERT_EQUAL_UINT16(600, s.duty);
TEST_ASSERT_EQUAL_UINT32(29000, t_thermal_floor_left_ms());
run(&s, 28);
TEST_ASSERT_TRUE(s.fan_en);
TEST_ASSERT_EQUAL_UINT16(600, t_thermal_floor());
run(&s, 1); /* 第 30 秒:到期 */
TEST_ASSERT_EQUAL_UINT16_MESSAGE(0, t_thermal_floor(), "下限到期没回落");
TEST_ASSERT_EQUAL_UINT32(0, t_thermal_floor_left_ms());
TEST_ASSERT_FALSE(s.fan_en);
/* 续期 = 再发一次:时限从这一刻重新算。 */
t_thermal_set_floor_for(500, 10000);
run(&s, 8);
t_thermal_set_floor_for(500, 10000);
run(&s, 8);
TEST_ASSERT_TRUE(s.fan_en);
run(&s, 3);
TEST_ASSERT_FALSE(s.fan_en);
/* 不带时限的下限一直保持(老行为),时钟回绕也不会让它「到期」。 */
t_thermal_set_floor(400);
run(&s, 1);
sim_time_advance_ms(0x80000000u);
run(&s, 1);
TEST_ASSERT_EQUAL_UINT16(400, t_thermal_floor());
TEST_ASSERT_TRUE(s.fan_en);
t_thermal_set_floor(0);
}
int main(void)
{
UNITY_BEGIN();
@@ -492,5 +633,8 @@ int main(void)
RUN_TEST(test_spinup_grace_suppresses_the_first_windows);
RUN_TEST(test_derate_is_edge_triggered_with_hysteresis);
RUN_TEST(test_thermal_never_touches_flash);
RUN_TEST(test_cool_board_cuts_fan_power_with_hysteresis);
RUN_TEST(test_power_cycling_the_fan_does_not_raise_stall_alarms);
RUN_TEST(test_cool_fan_still_runs_when_asked_or_blind);
return UNITY_END();
}