/* 配网热点(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 #include #include #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-`。 */ void prov_ssid(const char *id, char out[PROV_SSID_MAX]); /* ---- 网页表单 → 一行命令 --------------------------------------------------- 网页 POST 的正文是按行分的纯文本(免得在固件里做 URL 解码): wifi\n\n<口令> 加一组 / 改同名那组的口令 del\n 删一组 mqtt\n\n<账号>\n<口令>\n 口令留空 = 不改(`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 */