固件 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 行删除
+104
查看文件
@@ -0,0 +1,104 @@
/* 配网热点(SoftAP + 网页,0.1.19,2026-10-06 用户定)的平台无关部分。
ESP 胶水(热点、HTTP 服务、DNS 劫持、热点口令的 NVS)在 prov_esp.c。
---- 为什么要有 ------------------------------------------------------------
0.1.18 之前改网络配置只有 USB 一条路(netcfg.h):路由器换了口令 / 板子搬了地方,
就得拆机插线,而 Hub 休眠之后连 COM 口都不在。现在:**连不上 WiFi 就自己开一个热点**,
手机连上去、在 192.168.4.1 的网页里改。
---- 什么时候开、什么时候关(`prov_step`)---------------------------------
| 情形 | 热点 |
|---|---|
| 一组 WiFi 都没配 | 立刻开(`nocfg`) |
| 配了,但连续 `PROV_FAIL_MS` 没拿到地址(含启动后一直连不上、连上过又掉) | 开(`fail`) |
| 开着、STA 仍没连上 | **一直开着**(不设超时:超时关掉 = 又只剩拆机) |
| STA 连上了,热点上没人 | 立刻关 |
| STA 连上了,热点上还有人 | 再留 `PROV_LINGER_MS` 让网页看到「连上了」,然后关(`linger`) |
⚠️ 路由器重启这类短暂断网也会让热点冒出来一会儿,网一恢复它就自己关。这是有意的:
「等多久才算真的连不上」没有对的数,而热点开着不碰任何输出 —— 五台手机照常供电(§10.2)。
---- 热点开着时 STA 还在重连 ----------------------------------------------
STA 每试一次都要离开热点的信道去扫一圈(≈2~3 s),试得太密网页就卡。所以
**热点上有人、且最近 `PROV_HOLD_MS` 内动过网页**时,重连间隔不短于 `PROV_SLOW_MS`
(`hold`,mlink_esp.c 的 on_wifi 里用)。只是放慢、**从不停**:停掉的话,
一台连着热点忘了断的手机就能让板子永远不回网。
---- 安全 ------------------------------------------------------------------
· 热点是 WPA2,口令**所有板子都是 `PROV_PASS`(88888888,0.1.20,2026-10-06 用户定)**,编在固件里。
0.1.19 是每板一份随机口令存 NVS —— 断网时人手边未必有那张纸条,用户选了好记的。
`net` 命令的 `ap.pass` 照样报它;它不是 netcfg.h 说的那「两个密码」:知道它只能**写**配置,
网页与 `net` 一样从不回显 WiFi / MQTT 口令。
· 网页只做三件事:读 `net`、扫描、提交 `wifi` / `mqtt`。别的命令从这条链路进不来
(`prov_build_line` 只会拼出这两条)。
· 能连上热点的人可以把板子指到另一个 broker —— 等同于拿到这块板。口令是公开的固定值,
所以这道门实际只靠两条:热点只在「板子没网」时才开,而且人必须在无线范围内。 */
#ifndef PROV_H
#define PROV_H
#include <stdbool.h>
#include <stddef.h>
#include <stdint.h>
#ifdef __cplusplus
extern "C" {
#endif
#define PROV_FAIL_MS 60000u /* STA 连续这么久没地址 ⇒ 开热点 */
#define PROV_LINGER_MS 60000u /* STA 连上后,热点上还有人时再留多久 */
#define PROV_HOLD_MS 300000u /* 最后一次动网页之后,放慢重连持续多久 */
#define PROV_SLOW_MS 20000u /* 放慢时两次重连之间至少隔多久 */
typedef enum {
PROV_WHY_OFF = 0,
PROV_WHY_NOCFG,
PROV_WHY_FAIL,
PROV_WHY_LINGER,
} prov_why_t;
typedef struct {
int n_wifi; /* 表里有几组 WiFi */
bool wifi_up; /* STA 有地址 */
int ap_sta; /* 热点上连着几台 */
bool touched; /* 上一拍以来网页有过「人的动作」(打开页面 / 扫描 / 提交;状态轮询不算) */
} prov_in_t;
typedef struct {
bool ap; /* 热点该开着 */
bool hold; /* STA 重连要放慢 */
prov_why_t why;
} prov_out_t;
void prov_init(uint32_t now);
prov_out_t prov_step(const prov_in_t *in, uint32_t now);
const char *prov_why_str(prov_why_t w);
/* ---- 热点的名字与口令 ------------------------------------------------------ */
#define PROV_SSID_MAX 20 /* "PhoneRail-" + id 末 6 位 + 0 */
#define PROV_PASS "88888888" /* 热点口令,所有板子一样(见文件头「安全」)。WPA2 要 8~63 位 */
/* `PhoneRail-<id 末 6 位>`。 */
void prov_ssid(const char *id, char out[PROV_SSID_MAX]);
/* ---- 网页表单 → 一行命令 ---------------------------------------------------
网页 POST 的正文是按行分的纯文本(免得在固件里做 URL 解码):
wifi\n<ssid>\n<口令> 加一组 / 改同名那组的口令
del\n<ssid> 删一组
mqtt\n<uri>\n<账号>\n<口令>\n<tele_ms> 口令留空 = 不改(`keep_pass`);tele_ms 留空 = 默认
拼出来的就是 USB 上那两条命令,照样进唯一的命令处理器(cmd.h),校验一处都不重写。
命令的 JSON 解析器不认转义(cmd_json.h),所以字段里有引号 / 反斜杠 / 控制字符一律拒。
返回行长;0 = 拒,`*err` 是原因(`bad_form` / `bad_char` / `too_long`)。 */
size_t prov_build_line(const char *body, uint32_t seq, char *out, size_t cap, const char **err);
/* 一段不可信的字符串(扫描到的 SSID)→ JSON 字符串的内容:引号 / 反斜杠 / 控制字符写成 \u00XX,
其余字节原样(按 UTF-8 理解)。返回写了几个字符;装不下返回 0 且 out 为空串。 */
size_t prov_json_esc(const char *s, char *out, size_t cap);
#ifdef __cplusplus
}
#endif
#endif /* PROV_H */