/* 网络的三条命令(A14 / C1,需求 IF-19):`wifi` / `mqtt` 写配置(**只收 USB 与配网热点**),`net` 读状态; 外加 C2 的 `ota`(下载固件,两条链路都收)。 `net` 的回应里**永远没有密码**,只有 `pass_set` / `mpass_set`(netcfg.h「密码永不外泄」)。 */ #include #include "cmd_priv.h" #include "netcfg.h" #include "ota.h" #include "prov.h" /* 配置命令不收 MQTT(cmd.h 链路那一段):USB 与配网热点都是「人在板子旁边」。 宿主机测试用的 CMD_LINK_TEST 视同 USB。 */ static bool local_ok(const ctx_t *c) { return c->link != CMD_LINK_WIFI; } static cmd_err_t get_str(const ctx_t *c, const char *key, bool required, const char **out) { static const char empty[] = ""; const int g = cj_get_str(c->o, key, out); if (g == 1) { return CMD_OK; } if (g == 0 && !required) { *out = empty; return CMD_OK; } return CMD_ERR_ARG; } cmd_err_t do_wifi(ctx_t *c) { if (!local_ok(c)) { c->bad = CMD_WIFI; return CMD_ERR_LINK; } const char *ssid = NULL; const char *pass = NULL; bool del = false; if (get_str(c, "ssid", true, &ssid) != CMD_OK || get_str(c, "pass", false, &pass) != CMD_OK || cj_get_bool(c->o, "del", &del) < 0) { return CMD_ERR_ARG; } netcfg_t cfg; netcfg_get(&cfg); hal_err_t e; if (del) { /* 删:`{"cmd":"wifi","ssid":"X","del":true}`。没有这一组 = range(记现有组数)。 */ if (netcfg_wifi_find(&cfg, ssid) < 0) { c->bad = netcfg_wifi_count(&cfg); return CMD_ERR_RANGE; } e = netcfg_del_wifi(ssid); } else { if (!netcfg_wifi_ok(ssid, pass)) { c->bad = (int64_t)strlen(pass); /* 最常见的错是口令长度(<8),记长度不记内容 */ return CMD_ERR_RANGE; } if (netcfg_wifi_find(&cfg, ssid) < 0 && netcfg_wifi_count(&cfg) >= NETCFG_WIFI_N) { c->bad = NETCFG_WIFI_N; /* 满了:先删一组 */ return CMD_ERR_STATE; } e = netcfg_set_wifi(ssid, pass); } if (e != HAL_OK) { return CMD_ERR_IO; } netcfg_get(&cfg); cj_w(c->w, ",\"n\":%d", netcfg_wifi_count(&cfg)); return CMD_OK; } cmd_err_t do_mqtt(ctx_t *c) { if (!local_ok(c)) { c->bad = CMD_MQTT; return CMD_ERR_LINK; } const char *uri = NULL; const char *user = NULL; const char *mpass = NULL; int64_t tele = 0; bool keep = false; const int gt = cj_get_int(c->o, "tele_ms", &tele); if (get_str(c, "uri", true, &uri) != CMD_OK || get_str(c, "user", false, &user) != CMD_OK || get_str(c, "pass", false, &mpass) != CMD_OK || gt < 0 || cj_get_bool(c->o, "keep_pass", &keep) < 0) { return CMD_ERR_ARG; } /* `"keep_pass":true`(0.1.19)= 口令沿用已存的那份:配网网页看不到旧口令(永不回显), 只想改地址时不该逼人重输。同时又给了 `pass` 是自相矛盾,拒。 */ static netcfg_t cur; /* ≈560 B,不放栈上(只有执行器任务进来) */ if (keep) { if (mpass[0] != '\0') { return CMD_ERR_ARG; } netcfg_get(&cur); mpass = cur.mpass; } if (tele < 0 || tele > NETCFG_TELE_MAX_MS || !netcfg_mqtt_ok(uri, user, mpass, (uint32_t)tele)) { c->bad = tele; return CMD_ERR_RANGE; } return netcfg_set_mqtt(uri, user, mpass, (uint32_t)tele) == HAL_OK ? CMD_OK : CMD_ERR_IO; } /* 把一段来路不可信的字符串写成 JSON 字符串内容:引号、反斜杠、控制字符换成 `?`。 经命令写进来的配置过了解析器,本来就没有这些;但 NVS 里读回来的可能是任何东西, 一个裸引号就能让整条回应变成非法 JSON,或者更糟 —— 变成另一段合法 JSON。 */ static void w_str(cj_writer_t *w, const char *s) { char buf[NETCFG_URI_MAX + 1]; size_t n = 0; for (; *s != '\0' && n < sizeof(buf) - 1; s++) { const unsigned char ch = (unsigned char)*s; buf[n++] = (ch < 0x20 || ch == 0x7F || ch == '"' || ch == '\\' || ch >= 0x80) ? '?' : (char)ch; } buf[n] = '\0'; cj_w(w, "\"%s\"", buf); } cmd_err_t do_net(ctx_t *c) { netcfg_t cfg; net_status_t st; netcfg_get(&cfg); net_status_get(&st); cj_writer_t *w = c->w; cj_w(w, ",\"id\":"); w_str(w, st.id); /* `ssid` / `pass_set` = 正在连 / 已连上的那一组;`ssids` = 整张表(按轮换顺序)。 */ const int n = netcfg_wifi_count(&cfg); const netcfg_wifi_t *cur = (st.wifi_idx < n) ? &cfg.wifi[st.wifi_idx] : &cfg.wifi[0]; cj_w(w, ",\"wifi\":\"%s\",\"ssid\":", net_state_str(st.wifi)); w_str(w, cur->ssid); cj_w(w, ",\"pass_set\":%s,\"ssids\":[", cur->pass[0] ? "true" : "false"); for (int i = 0; i < n; i++) { if (i > 0) { cj_w(w, ","); } w_str(w, cfg.wifi[i].ssid); } cj_w(w, "],\"ip\":"); w_str(w, st.ip); cj_w(w, ",\"rssi\":%d,\"wifi_reason\":%u,\"mqtt\":\"%s\",\"uri\":", (int)st.rssi, (unsigned)st.wifi_reason, net_state_str(st.mqtt)); w_str(w, cfg.uri); cj_w(w, ",\"user\":"); w_str(w, cfg.user); cj_w(w, ",\"mpass_set\":%s,\"tele_ms\":%lu,\"drops\":{\"wifi\":%lu,\"mqtt\":%lu}," "\"tele\":{\"sent\":%lu,\"skipped\":%lu}", cfg.mpass[0] ? "true" : "false", (unsigned long)cfg.tele_ms, (unsigned long)st.wifi_drops, (unsigned long)st.mqtt_drops, (unsigned long)st.tele_sent, (unsigned long)st.tele_skipped); /* 配网热点(prov.h)。`pass` 是**热点自己的**口令,不是上面那两个「永不外泄」的: 人得先知道它才连得上热点,而读到它的前提是已经在 USB / MQTT 上了。 */ cj_w(w, ",\"ap\":{\"on\":%s,\"why\":\"%s\",\"sta\":%u,\"ssid\":", st.ap_on ? "true" : "false", prov_why_str((prov_why_t)st.ap_why), (unsigned)st.ap_sta); w_str(w, st.ap_ssid); cj_w(w, ",\"pass\":"); w_str(w, st.ap_pass); cj_w(w, "}"); return CMD_OK; } /* `ota`(C2):`{"seq":9,"cmd":"ota","url":"http://…","sha256":"<64 位小写>","size":N,"reboot":false}`。 两条链路都收 —— 下载本身走 WiFi,但命令从 USB 来也没有「把唯一的远程通道弄断」的风险: 新固件起不来会自己回滚(ota.h)。 回 ok 时带 `act`:start = 开始下载(进度看 status 的 `ota`)/ reboot = 同一镜像已就绪, 马上重启 / ready = 同一镜像已就绪、这次不要求重启。 */ cmd_err_t do_ota(ctx_t *c) { const char *url = NULL; const char *hex = NULL; int64_t size = 0; ota_req_t r; memset(&r, 0, sizeof(r)); if (cj_get_str(c->o, "url", &url) != 1 || cj_get_str(c->o, "sha256", &hex) != 1 || cj_get_int(c->o, "size", &size) != 1 || cj_get_bool(c->o, "reboot", &r.reboot) < 0) { return CMD_ERR_ARG; } if (!ota_url_ok(url) || !ota_parse_sha256(hex, r.sha256)) { return CMD_ERR_ARG; } if (size <= 0 || size > (int64_t)OTA_SLOT_BYTES) { c->bad = size; return CMD_ERR_RANGE; } memcpy(r.url, url, strlen(url) + 1); r.size = (uint32_t)size; ota_status_t st; ota_status_get(&st); const ota_action_t a = ota_plan(&st, &r); const int rc = ota_request(&r); if (rc == OTA_ACT_BUSY) { return CMD_ERR_BUSY; } if (rc != 0) { return CMD_ERR_STATE; /* 待验证中(PENDING)/ 没有 OTA 能力 */ } cj_w(c->w, ",\"act\":\"%s\"", a == OTA_ACT_START ? "start" : (a == OTA_ACT_REBOOT ? "reboot" : "ready")); return CMD_OK; }