/* 网络的三条命令(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" /* 配置命令只收 USB(cmd.h 链路那一段)。宿主机测试用的 CMD_LINK_TEST 视同 USB。 */ static bool usb_only_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 (!usb_only_ok(c)) { c->bad = CMD_WIFI; return CMD_ERR_LINK; } const char *ssid = NULL; const char *pass = NULL; if (get_str(c, "ssid", true, &ssid) != CMD_OK || get_str(c, "pass", false, &pass) != CMD_OK) { return CMD_ERR_ARG; } if (!netcfg_wifi_ok(ssid, pass)) { c->bad = (int64_t)strlen(pass); /* 最常见的错是口令长度(<8),记长度不记内容 */ return CMD_ERR_RANGE; } return netcfg_set_wifi(ssid, pass) == HAL_OK ? CMD_OK : CMD_ERR_IO; } cmd_err_t do_mqtt(ctx_t *c) { if (!usb_only_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; 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) { return CMD_ERR_ARG; } 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); cj_w(w, ",\"wifi\":\"%s\",\"ssid\":", net_state_str(st.wifi)); w_str(w, cfg.ssid); cj_w(w, ",\"pass_set\":%s,\"ip\":", cfg.pass[0] ? "true" : "false"); 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); 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; }