固件 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]>
这个提交包含在:
@@ -592,6 +592,20 @@ static void test_fan_floor(void)
|
||||
TEST_ASSERT_EQUAL_UINT16(700, t_thermal_floor());
|
||||
ASSERT_HAS(send("{\"seq\":2,\"cmd\":\"fan\",\"duty\":0}"), "\"floor\":0");
|
||||
TEST_ASSERT_EQUAL_UINT16(0, t_thermal_floor());
|
||||
|
||||
/* 0.1.21 `sec`:带时限的下限(到期回落在 test_t_thermal 里测)。没给 sec 的回应里不带它。 */
|
||||
ASSERT_LACKS(r, "\"sec\"");
|
||||
ASSERT_HAS(send("{\"seq\":3,\"cmd\":\"fan\",\"duty\":600,\"sec\":90}"), "\"floor\":600,\"sec\":90");
|
||||
TEST_ASSERT_EQUAL_UINT32(90000, t_thermal_floor_left_ms());
|
||||
ASSERT_HAS(send("{\"seq\":4,\"cmd\":\"fan\",\"duty\":600}"), "\"floor\":600"); /* 不带 sec = 改成一直保持 */
|
||||
TEST_ASSERT_EQUAL_UINT32(0, t_thermal_floor_left_ms());
|
||||
ASSERT_HAS(send("{\"seq\":5,\"cmd\":\"fan\",\"duty\":600,\"sec\":86401}"), "\"err\":\"range\"");
|
||||
ASSERT_HAS(send("{\"seq\":6,\"cmd\":\"fan\",\"duty\":600,\"sec\":-1}"), "\"err\":\"range\"");
|
||||
ASSERT_HAS(send("{\"seq\":7,\"cmd\":\"fan\",\"duty\":600,\"sec\":\"90\"}"), "\"err\":\"arg\"");
|
||||
TEST_ASSERT_EQUAL_UINT16(600, t_thermal_floor());
|
||||
ASSERT_HAS(send("{\"seq\":8,\"cmd\":\"fan\",\"duty\":0,\"sec\":90}"), "\"floor\":0"); /* 0 就是交回自动,时限无意义 */
|
||||
TEST_ASSERT_EQUAL_UINT32(0, t_thermal_floor_left_ms());
|
||||
t_thermal_set_floor(0);
|
||||
}
|
||||
|
||||
/* 0.1.14 hub:改模式 / 保持窗口,两条链路都收;越界记事件、缺参数不记;
|
||||
|
||||
@@ -0,0 +1,350 @@
|
||||
/* 0.1.19:配网热点(prov.h)的平台无关部分,加上「配置命令从哪条链路收」的新口径。
|
||||
|
||||
测的是上板不好查、写错了又很疼的几件事:
|
||||
· 热点**什么时候开、什么时候关** —— 开早了白白露一个入口,关早了人正填着表单就断;
|
||||
· STA 连不上时热点**不许自己消失**(消失 = 又只剩拆机插 USB);
|
||||
· 网页表单拼出来的命令行**拼不出别的命令**,带引号的字段进不来;
|
||||
· 配置命令:热点链路收、MQTT 照旧拒;`keep_pass` 不丢旧口令;`net` 报热点状态。 */
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
|
||||
#include "cmd.h"
|
||||
#include "evlog.h"
|
||||
#include "netcfg.h"
|
||||
#include "prov.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 fake_save(void *ctx, const netcfg_t *cfg)
|
||||
{
|
||||
(void)ctx;
|
||||
s_saved = *cfg;
|
||||
s_saves++;
|
||||
return HAL_OK;
|
||||
}
|
||||
|
||||
void setUp(void)
|
||||
{
|
||||
sim_hal_reset();
|
||||
memset(&s_saved, 0, sizeof(s_saved));
|
||||
s_saves = 0;
|
||||
netcfg_init(NULL, fake_save, NULL, NULL);
|
||||
TEST_ASSERT_EQUAL_INT(HAL_OK, evlog_init());
|
||||
cmd_init(NULL);
|
||||
prov_init(0);
|
||||
}
|
||||
|
||||
void tearDown(void) {}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
#define ASSERT_HAS(r, n) UNITY_TEST_ASSERT_NOT_NULL(strstr((r), (n)), __LINE__, (r))
|
||||
#define ASSERT_LACKS(r, n) UNITY_TEST_ASSERT_NULL(strstr((r), (n)), __LINE__, (r))
|
||||
|
||||
static prov_out_t step(int n_wifi, bool up, int sta, bool touched, uint32_t now)
|
||||
{
|
||||
const prov_in_t in = { .n_wifi = n_wifi, .wifi_up = up, .ap_sta = sta, .touched = touched };
|
||||
return prov_step(&in, now);
|
||||
}
|
||||
|
||||
/* ---- 判据 ------------------------------------------------------------------ */
|
||||
|
||||
/* ⭐ 一组都没配:第一拍就开,不等 60 s。 */
|
||||
static void test_no_config_opens_at_once(void)
|
||||
{
|
||||
const prov_out_t o = step(0, false, 0, false, 0);
|
||||
TEST_ASSERT_TRUE(o.ap);
|
||||
TEST_ASSERT_EQUAL_INT(PROV_WHY_NOCFG, o.why);
|
||||
TEST_ASSERT_FALSE(o.hold);
|
||||
}
|
||||
|
||||
/* ⭐ 配了但连不上:满 60 s 才开;之后一直开着,哪怕再过几天。 */
|
||||
static void test_failure_opens_after_a_minute_and_stays(void)
|
||||
{
|
||||
TEST_ASSERT_FALSE(step(1, false, 0, false, 0).ap);
|
||||
TEST_ASSERT_FALSE(step(1, false, 0, false, PROV_FAIL_MS - 1).ap);
|
||||
prov_out_t o = step(1, false, 0, false, PROV_FAIL_MS);
|
||||
TEST_ASSERT_TRUE(o.ap);
|
||||
TEST_ASSERT_EQUAL_INT(PROV_WHY_FAIL, o.why);
|
||||
/* 时钟回绕(49.7 天)之后「断了多久」会变小 —— 不许因此关掉。 */
|
||||
TEST_ASSERT_TRUE(step(1, false, 0, false, 0xFFFFFF00u).ap);
|
||||
TEST_ASSERT_TRUE(step(1, false, 0, false, 0x00000100u).ap);
|
||||
}
|
||||
|
||||
/* 连上过又掉:从掉的那一刻重新数 60 s(路由器重启一下不该立刻冒热点)。 */
|
||||
static void test_a_drop_restarts_the_minute(void)
|
||||
{
|
||||
TEST_ASSERT_FALSE(step(1, true, 0, false, 1000).ap);
|
||||
TEST_ASSERT_FALSE(step(1, true, 0, false, 500000).ap);
|
||||
TEST_ASSERT_FALSE(step(1, false, 0, false, 500100).ap);
|
||||
TEST_ASSERT_FALSE(step(1, false, 0, false, 500100 + PROV_FAIL_MS - 1).ap);
|
||||
TEST_ASSERT_TRUE(step(1, false, 0, false, 500100 + PROV_FAIL_MS).ap);
|
||||
/* 没满 60 s 就连回来了:计时作废。 */
|
||||
prov_init(0);
|
||||
(void)step(1, false, 0, false, 0);
|
||||
(void)step(1, true, 0, false, 30000);
|
||||
(void)step(1, false, 0, false, 40000);
|
||||
TEST_ASSERT_FALSE(step(1, false, 0, false, 70000).ap); /* 离启动 70 s,但离这次掉线才 30 s */
|
||||
}
|
||||
|
||||
/* ⭐ STA 连上了:热点上没人立刻关;有人再留 60 s(让网页看到「连上了」),到点照关。 */
|
||||
static void test_closes_when_sta_comes_up(void)
|
||||
{
|
||||
TEST_ASSERT_TRUE(step(1, false, 0, false, PROV_FAIL_MS).ap);
|
||||
TEST_ASSERT_FALSE(step(1, true, 0, false, PROV_FAIL_MS + 100).ap);
|
||||
|
||||
prov_init(0);
|
||||
TEST_ASSERT_TRUE(step(1, false, 1, false, PROV_FAIL_MS).ap);
|
||||
prov_out_t o = step(1, true, 1, false, 100000);
|
||||
TEST_ASSERT_TRUE(o.ap);
|
||||
TEST_ASSERT_EQUAL_INT(PROV_WHY_LINGER, o.why);
|
||||
TEST_ASSERT_FALSE(o.hold); /* 已经连上了,没有重连可放慢 */
|
||||
TEST_ASSERT_TRUE(step(1, true, 1, false, 100000 + PROV_LINGER_MS - 1).ap);
|
||||
TEST_ASSERT_FALSE(step(1, true, 1, false, 100000 + PROV_LINGER_MS).ap);
|
||||
/* 留的那一分钟里人走了:马上关,不等到点。 */
|
||||
prov_init(0);
|
||||
(void)step(1, false, 1, false, PROV_FAIL_MS);
|
||||
TEST_ASSERT_TRUE(step(1, true, 1, false, 100000).ap);
|
||||
TEST_ASSERT_FALSE(step(1, true, 0, false, 101000).ap);
|
||||
/* 关了就是关了:连着的时候不会再自己开。 */
|
||||
TEST_ASSERT_FALSE(step(1, true, 1, false, 102000).ap);
|
||||
}
|
||||
|
||||
/* ⭐ 没配网时开的热点:人在网页上刚存了第一组 WiFi(n 0 → 1,离启动还不到 60 s)——
|
||||
不许在这一刻把热点关掉,要等 STA 真的连上。 */
|
||||
static void test_first_config_does_not_close_the_ap(void)
|
||||
{
|
||||
TEST_ASSERT_TRUE(step(0, false, 1, true, 5000).ap);
|
||||
prov_out_t o = step(1, false, 1, true, 6000);
|
||||
TEST_ASSERT_TRUE(o.ap);
|
||||
TEST_ASSERT_EQUAL_INT(PROV_WHY_FAIL, o.why);
|
||||
TEST_ASSERT_TRUE(o.hold);
|
||||
TEST_ASSERT_TRUE(step(1, true, 1, false, 9000).ap); /* 连上了:linger */
|
||||
}
|
||||
|
||||
/* 放慢重连:热点上有人 + 最近 5 分钟动过网页。连上热点本身算一次动作;状态轮询不算。 */
|
||||
static void test_hold_follows_activity(void)
|
||||
{
|
||||
TEST_ASSERT_FALSE(step(1, false, 0, false, PROV_FAIL_MS).hold); /* 没人 */
|
||||
TEST_ASSERT_TRUE(step(1, false, 1, false, 70000).hold); /* 刚连上 */
|
||||
TEST_ASSERT_TRUE(step(1, false, 1, false, 70000 + PROV_HOLD_MS - 1).hold);
|
||||
TEST_ASSERT_FALSE(step(1, false, 1, false, 70000 + PROV_HOLD_MS).hold); /* 连着但 5 分钟没动:恢复正常节奏 */
|
||||
TEST_ASSERT_TRUE(step(1, false, 1, true, 900000).hold); /* 又点了一下 */
|
||||
TEST_ASSERT_FALSE(step(1, false, 1, false, 900000 + PROV_HOLD_MS).hold);
|
||||
/* 过期的窗口不许在时钟回绕后复活(人一直连着没走)。 */
|
||||
TEST_ASSERT_FALSE(step(1, false, 1, false, 900000u + 0xFFFFFF00u).hold);
|
||||
TEST_ASSERT_TRUE(step(1, false, 1, true, 5000).hold);
|
||||
TEST_ASSERT_FALSE(step(1, false, 0, false, 6000).hold); /* 人走了 */
|
||||
TEST_ASSERT_TRUE(step(1, false, 0, false, 7000).ap); /* 热点还在 */
|
||||
TEST_ASSERT_GREATER_OR_EQUAL_UINT32(10000, PROV_SLOW_MS); /* 一次连接尝试 ≈3 s,间隔得明显比它长 */
|
||||
}
|
||||
|
||||
/* ---- 名字与口令 ------------------------------------------------------------ */
|
||||
|
||||
static void test_ssid_and_password(void)
|
||||
{
|
||||
char ssid[PROV_SSID_MAX];
|
||||
prov_ssid("b81f3fc9c8bc", ssid);
|
||||
TEST_ASSERT_EQUAL_STRING("PhoneRail-c9c8bc", ssid);
|
||||
prov_ssid("", ssid);
|
||||
TEST_ASSERT_EQUAL_STRING("PhoneRail-", ssid);
|
||||
|
||||
/* 口令是固定值:WPA2 收 8~63 位;装得进 net_status_t,也不需要 JSON 转义。 */
|
||||
TEST_ASSERT_EQUAL_STRING("88888888", PROV_PASS);
|
||||
TEST_ASSERT_TRUE(strlen(PROV_PASS) >= 8 && strlen(PROV_PASS) <= 63);
|
||||
TEST_ASSERT_NULL(strpbrk(PROV_PASS, "\"\\"));
|
||||
}
|
||||
|
||||
/* ---- 表单 → 命令行 --------------------------------------------------------- */
|
||||
|
||||
static const char *build(const char *body, const char **err)
|
||||
{
|
||||
static char line[CMD_LINE_MAX];
|
||||
line[0] = '\0';
|
||||
const size_t n = prov_build_line(body, 77, line, sizeof(line), err);
|
||||
TEST_ASSERT_EQUAL_UINT(n, strlen(line));
|
||||
return line;
|
||||
}
|
||||
|
||||
/* ⭐ 拼出来的行能被命令处理器原样吃下去、存进后端。 */
|
||||
static void test_form_becomes_the_usb_commands(void)
|
||||
{
|
||||
const char *err = "x";
|
||||
const char *l = build("wifi\nfarm 2g\ns3cret pass", &err);
|
||||
TEST_ASSERT_NULL(err);
|
||||
TEST_ASSERT_EQUAL_STRING("{\"seq\":77,\"cmd\":\"wifi\",\"ssid\":\"farm 2g\",\"pass\":\"s3cret pass\"}", l);
|
||||
ASSERT_HAS(send_on(CMD_LINK_AP, l), "\"ok\":true,\"n\":1");
|
||||
TEST_ASSERT_EQUAL_STRING("s3cret pass", s_saved.wifi[0].pass);
|
||||
|
||||
/* 开放网络:口令那一行可以是空的,也可以整行没有;行尾的 \r 不算内容。 */
|
||||
TEST_ASSERT_EQUAL_STRING("{\"seq\":77,\"cmd\":\"wifi\",\"ssid\":\"open\",\"pass\":\"\"}", build("wifi\nopen\n", &err));
|
||||
TEST_ASSERT_EQUAL_STRING("{\"seq\":77,\"cmd\":\"wifi\",\"ssid\":\"open\",\"pass\":\"\"}", build("wifi\r\nopen\r", &err));
|
||||
/* 中文名字(UTF-8)原样过。 */
|
||||
ASSERT_HAS(build("wifi\n\xE5\xAE\xB6\n12345678", &err), "\"ssid\":\"\xE5\xAE\xB6\"");
|
||||
|
||||
TEST_ASSERT_EQUAL_STRING("{\"seq\":77,\"cmd\":\"wifi\",\"ssid\":\"farm 2g\",\"del\":true}", build("del\nfarm 2g", &err));
|
||||
cmd_init(NULL); /* 清判重窗口:这里每条都用 seq 77 */
|
||||
ASSERT_HAS(send_on(CMD_LINK_AP, build("del\nfarm 2g", &err)), "\"ok\":true,\"n\":0");
|
||||
|
||||
l = build("mqtt\nmqtts://home.example:8883\nboard-b\nBrokerPw77\n2000", &err);
|
||||
TEST_ASSERT_EQUAL_STRING("{\"seq\":77,\"cmd\":\"mqtt\",\"uri\":\"mqtts://home.example:8883\",\"user\":\"board-b\","
|
||||
"\"pass\":\"BrokerPw77\",\"tele_ms\":2000}", l);
|
||||
cmd_init(NULL); /* 清判重窗口:这里每条都用 seq 77 */
|
||||
ASSERT_HAS(send_on(CMD_LINK_AP, l), "\"ok\":true");
|
||||
TEST_ASSERT_EQUAL_UINT32(2000, s_saved.tele_ms);
|
||||
}
|
||||
|
||||
/* ⭐ MQTT 口令留空 = 不改:只换地址时旧口令还在;给了 keep_pass 又给 pass 是自相矛盾。 */
|
||||
static void test_keep_pass_keeps_the_stored_password(void)
|
||||
{
|
||||
send_on(CMD_LINK_USB, "{\"seq\":1,\"cmd\":\"mqtt\",\"uri\":\"mqtt://old:1883\",\"user\":\"u\",\"pass\":\"BrokerPw77\"}");
|
||||
const char *err = NULL;
|
||||
const char *l = build("mqtt\nmqtt://new:1883\nu2\n\n", &err);
|
||||
TEST_ASSERT_EQUAL_STRING("{\"seq\":77,\"cmd\":\"mqtt\",\"uri\":\"mqtt://new:1883\",\"user\":\"u2\","
|
||||
"\"keep_pass\":true,\"tele_ms\":0}", l);
|
||||
const char *r = send_on(CMD_LINK_AP, l);
|
||||
ASSERT_HAS(r, "\"ok\":true");
|
||||
ASSERT_LACKS(r, "BrokerPw77");
|
||||
TEST_ASSERT_EQUAL_STRING("mqtt://new:1883", s_saved.uri);
|
||||
TEST_ASSERT_EQUAL_STRING("u2", s_saved.user);
|
||||
TEST_ASSERT_EQUAL_STRING("BrokerPw77", s_saved.mpass);
|
||||
|
||||
ASSERT_HAS(send_on(CMD_LINK_USB, "{\"seq\":3,\"cmd\":\"mqtt\",\"uri\":\"mqtt://x:1\",\"pass\":\"other\","
|
||||
"\"keep_pass\":true}"), "\"err\":\"arg\"");
|
||||
ASSERT_HAS(send_on(CMD_LINK_USB, "{\"seq\":4,\"cmd\":\"mqtt\",\"uri\":\"mqtt://x:1\",\"keep_pass\":1}"),
|
||||
"\"err\":\"arg\"");
|
||||
TEST_ASSERT_EQUAL_STRING("BrokerPw77", s_saved.mpass);
|
||||
/* keep_pass:false 就是老行为:没给 pass = 清空。 */
|
||||
ASSERT_HAS(send_on(CMD_LINK_USB, "{\"seq\":5,\"cmd\":\"mqtt\",\"uri\":\"mqtt://x:1\",\"keep_pass\":false}"),
|
||||
"\"ok\":true");
|
||||
TEST_ASSERT_EQUAL_STRING("", s_saved.mpass);
|
||||
}
|
||||
|
||||
/* ⭐ 拼不出别的命令;能把 JSON 撑破的字符进不来。 */
|
||||
static void test_form_rejects_what_it_should(void)
|
||||
{
|
||||
static const struct {
|
||||
const char *body;
|
||||
const char *err;
|
||||
} k[] = {
|
||||
{ "", "bad_form" },
|
||||
{ "wifi", "bad_form" },
|
||||
{ "wifi\n\n12345678", "bad_form" }, /* 没名字 */
|
||||
{ "wifi\na\nb\nc", "bad_form" }, /* 多了一行 */
|
||||
{ "del\na\nb", "bad_form" },
|
||||
{ "mqtt\n", "bad_form" },
|
||||
{ "mqtt\nmqtt://h\nu\np\n12x", "bad_form" }, /* tele 不是数字 */
|
||||
{ "mqtt\nmqtt://h\nu\np\n99999999999999", "bad_form" },
|
||||
{ "power\n1", "bad_form" }, /* 别的命令 */
|
||||
{ "status", "bad_form" },
|
||||
{ "wifi\na\"b\n12345678", "bad_char" },
|
||||
{ "wifi\na\n1234\\5678", "bad_char" },
|
||||
{ "wifi\na\tb\n12345678", "bad_char" },
|
||||
{ "wifi\na\n12345678\",\"cmd\":\"power", "bad_char" }, /* 想往行里塞第二个键 */
|
||||
{ "wifi\na\nb\nc\nd\ne", "too_long" }, /* 行数超了 */
|
||||
{ "wifi\n0123456789012345678901234567890123456789012345678901234567890123456789\nx", "too_long" },
|
||||
};
|
||||
for (size_t i = 0; i < sizeof(k) / sizeof(k[0]); i++) {
|
||||
const char *err = NULL;
|
||||
char line[CMD_LINE_MAX] = "junk";
|
||||
TEST_ASSERT_EQUAL_UINT_MESSAGE(0, prov_build_line(k[i].body, 1, line, sizeof(line), &err), k[i].body);
|
||||
TEST_ASSERT_EQUAL_STRING_MESSAGE(k[i].err, err, k[i].body);
|
||||
}
|
||||
/* seq 0 不是合法序号;缓冲不够不越界。 */
|
||||
char small[16];
|
||||
TEST_ASSERT_EQUAL_UINT(0, prov_build_line("wifi\na\n12345678", 0, small, sizeof(small), NULL));
|
||||
TEST_ASSERT_EQUAL_UINT(0, prov_build_line("wifi\na\n12345678", 1, small, sizeof(small), NULL));
|
||||
TEST_ASSERT_EQUAL_STRING("", small);
|
||||
|
||||
/* 三个字段都顶到头(64 + 32 + 64)的 mqtt 行也装得进一行命令。 */
|
||||
char body[256];
|
||||
char f64[65];
|
||||
memset(f64, 'p', 64);
|
||||
f64[64] = '\0';
|
||||
snprintf(body, sizeof(body), "mqtt\nmqtts://%.*s\n%.32s\n%s\n60000", 56, f64, f64, f64);
|
||||
char line[CMD_LINE_MAX];
|
||||
const char *err = "x";
|
||||
TEST_ASSERT_GREATER_THAN_UINT(0, prov_build_line(body, 0xFFFFFFFFu, line, sizeof(line), &err));
|
||||
ASSERT_HAS(send_on(CMD_LINK_AP, line), "\"ok\":true");
|
||||
TEST_ASSERT_EQUAL_STRING(f64, s_saved.mpass);
|
||||
}
|
||||
|
||||
static void test_json_escape(void)
|
||||
{
|
||||
char out[64];
|
||||
TEST_ASSERT_EQUAL_UINT(4, prov_json_esc("farm", out, sizeof(out)));
|
||||
TEST_ASSERT_EQUAL_STRING("farm", out);
|
||||
prov_json_esc("a\"b\\c\n", out, sizeof(out));
|
||||
TEST_ASSERT_EQUAL_STRING("a\\u0022b\\u005cc\\u000a", out);
|
||||
prov_json_esc("\xE5\xAE\xB6", out, sizeof(out));
|
||||
TEST_ASSERT_EQUAL_STRING("\xE5\xAE\xB6", out);
|
||||
char tiny[8];
|
||||
TEST_ASSERT_EQUAL_UINT(0, prov_json_esc("12345678", tiny, sizeof(tiny)));
|
||||
TEST_ASSERT_EQUAL_STRING("", tiny);
|
||||
TEST_ASSERT_EQUAL_UINT(0, prov_json_esc("ab\"", tiny, sizeof(tiny))); /* 转义写到一半不留半截 */
|
||||
TEST_ASSERT_EQUAL_STRING("", tiny);
|
||||
}
|
||||
|
||||
/* ---- 链路口径 -------------------------------------------------------------- */
|
||||
|
||||
/* ⭐ 配置命令:热点链路和 USB 一样收;MQTT 照旧拒(0.1.19 没有放开这一条)。 */
|
||||
static void test_ap_link_may_configure_but_mqtt_still_may_not(void)
|
||||
{
|
||||
ASSERT_HAS(send_on(CMD_LINK_AP, "{\"seq\":1,\"cmd\":\"wifi\",\"ssid\":\"farm\",\"pass\":\"12345678\"}"),
|
||||
"\"ok\":true");
|
||||
ASSERT_HAS(send_on(CMD_LINK_AP, "{\"seq\":2,\"cmd\":\"mqtt\",\"uri\":\"mqtt://h:1883\"}"), "\"ok\":true");
|
||||
TEST_ASSERT_EQUAL_INT(2, s_saves);
|
||||
ASSERT_HAS(send_on(CMD_LINK_WIFI, "{\"seq\":3,\"cmd\":\"wifi\",\"ssid\":\"evil\",\"pass\":\"12345678\"}"),
|
||||
"\"err\":\"link\"");
|
||||
ASSERT_HAS(send_on(CMD_LINK_WIFI, "{\"seq\":4,\"cmd\":\"mqtt\",\"uri\":\"mqtt://1.2.3.4\",\"keep_pass\":true}"),
|
||||
"\"err\":\"link\"");
|
||||
TEST_ASSERT_EQUAL_INT(2, s_saves);
|
||||
TEST_ASSERT_EQUAL_INT(3, CMD_LINK_AP); /* 进事件日志的编号(CMD_REBOOT_ARG),只许追加 */
|
||||
}
|
||||
|
||||
/* `net` 报热点状态;热点口令能读到,WiFi / MQTT 口令照旧读不到。 */
|
||||
static void test_net_reports_the_ap(void)
|
||||
{
|
||||
send_on(CMD_LINK_USB, "{\"seq\":1,\"cmd\":\"wifi\",\"ssid\":\"farm\",\"pass\":\"TopSecret99\"}");
|
||||
ASSERT_HAS(send_on(CMD_LINK_AP, "{\"cmd\":\"net\"}"),
|
||||
"\"ap\":{\"on\":false,\"why\":\"off\",\"sta\":0,\"ssid\":\"\",\"pass\":\"\"}");
|
||||
net_status_t st;
|
||||
net_status_get(&st);
|
||||
st.ap_on = true;
|
||||
st.ap_why = PROV_WHY_FAIL;
|
||||
st.ap_sta = 1;
|
||||
snprintf(st.ap_ssid, sizeof(st.ap_ssid), "PhoneRail-c9c8bc");
|
||||
snprintf(st.ap_pass, sizeof(st.ap_pass), "%s", PROV_PASS);
|
||||
net_status_set(&st);
|
||||
const char *r = send_on(CMD_LINK_WIFI, "{\"cmd\":\"net\"}");
|
||||
ASSERT_HAS(r, "\"ap\":{\"on\":true,\"why\":\"fail\",\"sta\":1,\"ssid\":\"PhoneRail-c9c8bc\",\"pass\":\"88888888\"}");
|
||||
ASSERT_LACKS(r, "TopSecret99");
|
||||
TEST_ASSERT_TRUE(sizeof(st.ap_ssid) >= PROV_SSID_MAX && sizeof(st.ap_pass) > strlen(PROV_PASS));
|
||||
}
|
||||
|
||||
int main(void)
|
||||
{
|
||||
UNITY_BEGIN();
|
||||
RUN_TEST(test_no_config_opens_at_once);
|
||||
RUN_TEST(test_failure_opens_after_a_minute_and_stays);
|
||||
RUN_TEST(test_a_drop_restarts_the_minute);
|
||||
RUN_TEST(test_closes_when_sta_comes_up);
|
||||
RUN_TEST(test_first_config_does_not_close_the_ap);
|
||||
RUN_TEST(test_hold_follows_activity);
|
||||
RUN_TEST(test_ssid_and_password);
|
||||
RUN_TEST(test_form_becomes_the_usb_commands);
|
||||
RUN_TEST(test_keep_pass_keeps_the_stored_password);
|
||||
RUN_TEST(test_form_rejects_what_it_should);
|
||||
RUN_TEST(test_json_escape);
|
||||
RUN_TEST(test_ap_link_may_configure_but_mqtt_still_may_not);
|
||||
RUN_TEST(test_net_reports_the_ap);
|
||||
return UNITY_END();
|
||||
}
|
||||
+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();
|
||||
}
|
||||
|
||||
在新工单中引用
屏蔽一个用户