/* 配网热点的平台无关部分。判据表与表单格式在 prov.h。 */ #include "prov.h" #include #include #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; }