固件 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]>
这个提交包含在:
+206
@@ -0,0 +1,206 @@
|
||||
/* 配网热点的平台无关部分。判据表与表单格式在 prov.h。 */
|
||||
#include "prov.h"
|
||||
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
|
||||
#include "cmd.h"
|
||||
#include "netcfg.h"
|
||||
|
||||
static bool s_ap;
|
||||
static bool s_was_up;
|
||||
static uint32_t s_since; /* STA 上一次「有地址 ↔ 没地址」翻转的时刻(启动算一次) */
|
||||
static bool s_aged; /* 翻转之后已经过了门限(过了就锁住:32 位时钟 49.7 天回绕,放着不管会「变年轻」) */
|
||||
static bool s_touch; /* hold 窗口还开着(过期即清,同上) */
|
||||
static uint32_t s_touch_at;
|
||||
static int s_prev_sta;
|
||||
|
||||
void prov_init(uint32_t now)
|
||||
{
|
||||
s_ap = false;
|
||||
s_was_up = false;
|
||||
s_since = now;
|
||||
s_aged = false;
|
||||
s_touch = false;
|
||||
s_touch_at = now;
|
||||
s_prev_sta = 0;
|
||||
}
|
||||
|
||||
prov_out_t prov_step(const prov_in_t *in, uint32_t now)
|
||||
{
|
||||
if (in->wifi_up != s_was_up) {
|
||||
s_was_up = in->wifi_up;
|
||||
s_since = now;
|
||||
s_aged = false;
|
||||
}
|
||||
if (!s_aged && (uint32_t)(now - s_since) >= (in->wifi_up ? PROV_LINGER_MS : PROV_FAIL_MS)) {
|
||||
s_aged = true;
|
||||
}
|
||||
if (in->touched || in->ap_sta > s_prev_sta) { /* 有人刚连上热点也算一次 */
|
||||
s_touch = true;
|
||||
s_touch_at = now;
|
||||
}
|
||||
s_prev_sta = in->ap_sta;
|
||||
if (s_touch && (uint32_t)(now - s_touch_at) >= PROV_HOLD_MS) {
|
||||
s_touch = false;
|
||||
}
|
||||
|
||||
prov_out_t o = { .ap = false, .hold = false, .why = PROV_WHY_OFF };
|
||||
if (in->wifi_up) {
|
||||
if (s_ap && in->ap_sta > 0 && !s_aged) {
|
||||
o.ap = true;
|
||||
o.why = PROV_WHY_LINGER;
|
||||
}
|
||||
} else if (in->n_wifi <= 0) {
|
||||
o.ap = true;
|
||||
o.why = PROV_WHY_NOCFG;
|
||||
} else if (s_aged || s_ap) {
|
||||
/* `|| s_ap`:开着就不因为「刚配了一组、还没满 60 s」而关掉 —— 那正是人在网页上点完保存的那一刻。 */
|
||||
o.ap = true;
|
||||
o.why = PROV_WHY_FAIL;
|
||||
}
|
||||
o.hold = o.ap && !in->wifi_up && in->ap_sta > 0 && s_touch;
|
||||
s_ap = o.ap;
|
||||
return o;
|
||||
}
|
||||
|
||||
const char *prov_why_str(prov_why_t w)
|
||||
{
|
||||
switch (w) {
|
||||
case PROV_WHY_OFF: return "off";
|
||||
case PROV_WHY_NOCFG: return "nocfg";
|
||||
case PROV_WHY_FAIL: return "fail";
|
||||
case PROV_WHY_LINGER: return "linger";
|
||||
}
|
||||
return "?";
|
||||
}
|
||||
|
||||
/* ---- 名字与口令 ------------------------------------------------------------ */
|
||||
|
||||
void prov_ssid(const char *id, char out[PROV_SSID_MAX])
|
||||
{
|
||||
const size_t n = strlen(id);
|
||||
snprintf(out, PROV_SSID_MAX, "PhoneRail-%s", n > 6 ? id + n - 6 : id);
|
||||
}
|
||||
|
||||
/* ---- 表单 → 命令行 --------------------------------------------------------- */
|
||||
|
||||
#define FIELD_MAX 64 /* = CMD_JSON_STR_MAX − 1:再长解析器也不收 */
|
||||
#define FIELDS 5
|
||||
|
||||
/* 把正文按 \n 切成至多 FIELDS 段(行尾的 \r 去掉),拷进 f[]。返回段数;−1 = 太长 / 段太多,−2 = 有坏字符。 */
|
||||
static int split(const char *body, char f[FIELDS][FIELD_MAX + 1])
|
||||
{
|
||||
int n = 0;
|
||||
size_t len = 0;
|
||||
f[0][0] = '\0';
|
||||
for (const char *p = body;; p++) {
|
||||
if (*p == '\n' || *p == '\0') {
|
||||
if (len > 0 && f[n][len - 1] == '\r') {
|
||||
len--;
|
||||
}
|
||||
f[n][len] = '\0';
|
||||
for (size_t i = 0; i < len; i++) {
|
||||
const unsigned char c = (unsigned char)f[n][i];
|
||||
if (c < 0x20 || c == 0x7F || c == '"' || c == '\\') {
|
||||
return -2;
|
||||
}
|
||||
}
|
||||
n++;
|
||||
if (*p == '\0') {
|
||||
return n;
|
||||
}
|
||||
if (n >= FIELDS) {
|
||||
return -1;
|
||||
}
|
||||
len = 0;
|
||||
continue;
|
||||
}
|
||||
if (len >= FIELD_MAX) {
|
||||
return -1;
|
||||
}
|
||||
f[n][len++] = *p;
|
||||
}
|
||||
}
|
||||
|
||||
size_t prov_build_line(const char *body, uint32_t seq, char *out, size_t cap, const char **err)
|
||||
{
|
||||
static const char *dummy;
|
||||
if (err == NULL) {
|
||||
err = &dummy;
|
||||
}
|
||||
*err = "bad_form";
|
||||
if (body == NULL || out == NULL || cap == 0 || seq == 0) {
|
||||
return 0;
|
||||
}
|
||||
char f[FIELDS][FIELD_MAX + 1];
|
||||
const int n = split(body, f);
|
||||
if (n < 0) {
|
||||
*err = (n == -2) ? "bad_char" : "too_long";
|
||||
return 0;
|
||||
}
|
||||
for (int i = n; i < FIELDS; i++) {
|
||||
f[i][0] = '\0';
|
||||
}
|
||||
int w;
|
||||
if (strcmp(f[0], "wifi") == 0 && n >= 2 && n <= 3 && f[1][0] != '\0') {
|
||||
w = snprintf(out, cap, "{\"seq\":%lu,\"cmd\":\"wifi\",\"ssid\":\"%s\",\"pass\":\"%s\"}",
|
||||
(unsigned long)seq, f[1], f[2]);
|
||||
} else if (strcmp(f[0], "del") == 0 && n == 2 && f[1][0] != '\0') {
|
||||
w = snprintf(out, cap, "{\"seq\":%lu,\"cmd\":\"wifi\",\"ssid\":\"%s\",\"del\":true}",
|
||||
(unsigned long)seq, f[1]);
|
||||
} else if (strcmp(f[0], "mqtt") == 0 && n >= 2 && f[1][0] != '\0') {
|
||||
unsigned long tele = 0;
|
||||
for (const char *p = f[4]; *p != '\0'; p++) {
|
||||
if (*p < '0' || *p > '9' || tele > NETCFG_TELE_MAX_MS) {
|
||||
return 0;
|
||||
}
|
||||
tele = tele * 10u + (unsigned long)(*p - '0');
|
||||
}
|
||||
/* 口令留空 = 沿用已存的那份:网页上看不到旧口令(永不回显),不该逼人每次重输。 */
|
||||
if (f[3][0] == '\0') {
|
||||
w = snprintf(out, cap,
|
||||
"{\"seq\":%lu,\"cmd\":\"mqtt\",\"uri\":\"%s\",\"user\":\"%s\",\"keep_pass\":true,"
|
||||
"\"tele_ms\":%lu}",
|
||||
(unsigned long)seq, f[1], f[2], tele);
|
||||
} else {
|
||||
w = snprintf(out, cap,
|
||||
"{\"seq\":%lu,\"cmd\":\"mqtt\",\"uri\":\"%s\",\"user\":\"%s\",\"pass\":\"%s\","
|
||||
"\"tele_ms\":%lu}",
|
||||
(unsigned long)seq, f[1], f[2], f[3], tele);
|
||||
}
|
||||
} else {
|
||||
return 0;
|
||||
}
|
||||
if (w <= 0 || (size_t)w >= cap || w >= CMD_LINE_MAX) {
|
||||
*err = "too_long";
|
||||
out[0] = '\0';
|
||||
return 0;
|
||||
}
|
||||
*err = NULL;
|
||||
return (size_t)w;
|
||||
}
|
||||
|
||||
size_t prov_json_esc(const char *s, char *out, size_t cap)
|
||||
{
|
||||
size_t n = 0;
|
||||
for (; *s != '\0'; s++) {
|
||||
const unsigned char c = (unsigned char)*s;
|
||||
const bool esc = c < 0x20 || c == 0x7F || c == '"' || c == '\\';
|
||||
if (n + (esc ? 6u : 1u) >= cap) {
|
||||
if (cap > 0) {
|
||||
out[0] = '\0';
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
if (esc) {
|
||||
n += (size_t)snprintf(out + n, cap - n, "\\u%04x", (unsigned)c);
|
||||
} else {
|
||||
out[n++] = (char)c;
|
||||
}
|
||||
}
|
||||
if (cap > 0) {
|
||||
out[n] = '\0';
|
||||
}
|
||||
return n;
|
||||
}
|
||||
在新工单中引用
屏蔽一个用户