文件
phonerail/firmware/main/mlink_esp.c
T
zhou yang和Claude Opus 5.5 9fb5bba367 固件 0.1.5~0.1.13-c5:刷机模式、多组 WiFi、热插拔初始化、总线锁误判、硬重启断 VBUS
- 0.1.5/0.1.6:刷机模式(flash_mode,最多 2 路,60 分钟可加钟)——窗口内顶高按 4450 mV 绝对值判、不断 VBUS
- 0.1.7/0.1.8:反灌断 VBUS 阈值放宽到 -300 mA(adb reboot bootloader 过渡实测 -208 mA)
- 0.1.9:WiFi 最多存 4 组,连不上轮换;NVS v1 自动迁移并双写(OTA 回滚仍能上网)
- 0.1.10:热插拔进来的刀片重新初始化 PCA9555 + INA226(否则 VBUS 常开、电流恒读 0)
- 0.1.11:i2cbus 嵌套检测改看本任务持锁(原全局标志让 key 随机回 busy、vbus 随机回 io)
- 0.1.12:硬重启先断 VBUS,开机校验通过 60 s 后再接回(否则假重启 / 误判没开机 / 进关机充电)
- 0.1.13:这一路被安全判据断电时一并退出刷机模式(事件 21 arg 254)

26 ctest 全绿;新增用例均先在旧代码上确认变红。板 B 已 OTA 到 0.1.13-c5 并实测。

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
2026-09-29 08:16:21 +08:00

515 行
21 KiB
C
原始文件 Blame 文件历史

此文件含有模棱两可的 Unicode 字符
此文件含有可能会与其他字符混淆的 Unicode 字符。 如果您是想特意这样的,可以安全地忽略该警告。 使用 Escape 按钮显示他们。
/* WiFi / MQTT 链路的 ESP 胶水(A14 + C1)。**只进 IDF 目标**;topic 与规矩在 mlink.h,
配置与「密码永不外泄」在 netcfg.h。
---- 谁在哪个任务里跑 ------------------------------------------------------
| 事 | 任务 |
|---|---|
| WiFi / IP 事件(掉线、拿到地址) | 系统事件循环 |
| MQTT 事件(连上、收到命令) | esp-mqtt 自己的任务,优先级压到与 `t_link` 相同(§10.3:链路最低) |
| **MQTT 客户端的创建 / 销毁、重配置、遥测节拍** | `t_link`(`mlink_esp_poll()`)—— 生命周期只在一个任务里管 |
| 回应 / 遥测的发布 | 执行器任务只把一份拷贝塞进 `s_outbox`(不等);`t_link` 取出来 `esp_mqtt_client_enqueue()` |
⚠️ **执行器任务一律不碰 esp-mqtt 的任何 API**,连 `esp_mqtt_client_enqueue()` 也不行:
它要拿客户端的 `api_lock`,而 esp-mqtt 任务在连接 / 写超时 / 分发事件时一直持着这把锁。
0.1.1-c3 之前执行器是「持 `s_mtx` → 等 `api_lock`」,esp-mqtt 任务分发 DISCONNECTED 时是
「持 `api_lock` → `on_mqtt` 里 `ST_EDIT` 等 `s_mtx`」—— 2026-09-28 热点一断就死锁:
USB 命令没人执行、WiFi 不重连、**按到一半的键松不开**,只有 t_safety / t_thermal 还活着。
现在的规矩:**持 `s_mtx` 时不调 esp-mqtt;执行器根本不调 esp-mqtt**。
---- 失联不动作(§10.2 / §10.8) -----------------------------------------
WiFi 断、broker 断,本文件只做一件事:**重连**(WiFi 指数退避,MQTT 由 esp-mqtt 每 10 s 重试)。
没有任何「连不上就……」的分支会碰到输出 —— 五台手机照常供电。 */
#include <string.h>
#include "cmd_queue.h"
#include "esp_event.h"
#include "esp_log.h"
#include "esp_mac.h"
#include "esp_netif.h"
#include "esp_timer.h"
#include "esp_wifi.h"
#include "freertos/FreeRTOS.h"
#include "freertos/queue.h"
#include "freertos/semphr.h"
#include "hal_time.h"
#include "mlink.h"
#include "mlink_esp.h"
#include "mqtt_client.h"
#include "netcfg.h"
#include "nvs.h"
#include "nvs_flash.h"
#include "phonerail.h"
#include "tasks.h"
#include "ulink.h"
static const char *TAG = "mlink";
#define NVS_NS "phonerail"
#define NVS_KEY "net2"
#define NVS_VERSION 2 /* netcfg_t 布局一变就加一:旧版本的 blob 直接当没配置 */
/* ≤0.1.8 的配置(只有一组 WiFi)存在 "net" 键里。0.1.9 起:
· 读:没有 "net2" 就读 "net" 迁过来(上架的板子升级后不用重新配网);
· 写:两个键**都写**,"net" 里放表里第一组 —— OTA 回滚到老固件照样连得上网。 */
#define NVS_KEY_V1 "net"
#define NVS_VERSION_V1 1
typedef struct { /* ≤0.1.8 的 netcfg_t,一个字节都不能动 */
char ssid[NETCFG_SSID_MAX + 1];
char pass[NETCFG_PASS_MAX + 1];
char uri[NETCFG_URI_MAX + 1];
char user[NETCFG_USER_MAX + 1];
char mpass[NETCFG_MPASS_MAX + 1];
uint32_t tele_ms;
} netcfg_v1_t;
typedef struct {
uint8_t ver;
netcfg_v1_t cfg;
} nvs_blob_v1_t;
/* evse-ca(home 服务器上那个 mosquitto 的自签 CA,公钥证书,不是机密)。
`main/CMakeLists.txt` 的 EMBED_TXTFILES 编进来,结尾带 0。只在 mqtts:// 时用。 */
extern const char evse_ca_pem_start[] asm("_binary_evse_ca_pem_start");
#define MQTT_KEEPALIVE_S 15 /* 掉线后 ≈1.5 × 15 s broker 发遗嘱;默认 120 s 要等 3 分钟 */
typedef struct {
uint8_t ver;
netcfg_t cfg;
} nvs_blob_t;
static SemaphoreHandle_t s_mtx; /* 护 s_st 与 s_mqtt 句柄 */
static net_status_t s_st;
static esp_mqtt_client_handle_t s_mqtt;
static netcfg_t s_cur; /* 当前生效的配置(只在 t_link 里读写) */
static volatile uint8_t s_idx; /* 正在连 / 已连上的是 s_cur.wifi 第几组 */
static volatile bool s_leaving; /* wifi_apply 主动断开了一条好好的连接:那次断开事件不算失败 */
static volatile bool s_dirty; /* 配置变了,等 t_link 来重配 */
static esp_timer_handle_t s_retry;
static uint32_t s_attempt;
static mlink_tele_t s_tele;
static volatile bool s_tele_inflight;
static bool s_up;
/* 发件队列(见文件头):执行器 / esp-mqtt 任务往里拷,t_link 取出来交给 esp-mqtt。
深度 4 × 1.5 KB:遥测 1 s 一条、回应偶尔一条,t_link 至少每 100 ms 来取一次;
满了说明 t_link 被 esp-mqtt 堵住了 —— 丢掉这一条,遥测记 skipped,回应靠上位机重传。 */
typedef struct {
uint8_t topic; /* mlink_topic_t */
uint8_t qos;
uint16_t len;
char data[CMD_OUT_MAX];
} out_msg_t;
#define OUTBOX_DEPTH 4
static QueueHandle_t s_outbox;
static SemaphoreHandle_t s_out_mtx; /* 只护 enqueue() 的暂存缓冲 */
/* ---- 状态:改完立刻推给 netcfg,命令处理器随时读得到一份完整的 ------------ */
/* 可变参数:语句里带逗号(snprintf 的实参)也能整个传进来。 */
#define ST_EDIT(...) \
do { \
xSemaphoreTake(s_mtx, portMAX_DELAY); \
__VA_ARGS__; \
net_status_set(&s_st); \
xSemaphoreGive(s_mtx); \
} while (0)
/* ---- NVS ----------------------------------------------------------------- */
static hal_err_t nvs_save(void *ctx, const netcfg_t *cfg)
{
(void)ctx;
/* ⚠️ 在执行器任务里被调(配置命令),flash 写可能要几十毫秒。配置是上架时一次性的事,
这点停顿可以接受;换成异步写就得处理「回了 ok 但没存下」。 */
nvs_handle_t h;
if (nvs_open(NVS_NS, NVS_READWRITE, &h) != ESP_OK) {
return HAL_ERR_BUS;
}
static nvs_blob_t b; /* ≈560 B,不放栈上(只有执行器任务进来) */
static nvs_blob_v1_t v1;
b.ver = NVS_VERSION;
b.cfg = *cfg;
memset(&v1, 0, sizeof(v1));
v1.ver = NVS_VERSION_V1;
memcpy(v1.cfg.ssid, cfg->wifi[0].ssid, sizeof(v1.cfg.ssid));
memcpy(v1.cfg.pass, cfg->wifi[0].pass, sizeof(v1.cfg.pass));
memcpy(v1.cfg.uri, cfg->uri, sizeof(v1.cfg.uri));
memcpy(v1.cfg.user, cfg->user, sizeof(v1.cfg.user));
memcpy(v1.cfg.mpass, cfg->mpass, sizeof(v1.cfg.mpass));
v1.cfg.tele_ms = cfg->tele_ms;
esp_err_t e = nvs_set_blob(h, NVS_KEY, &b, sizeof(b));
if (e == ESP_OK) {
e = nvs_set_blob(h, NVS_KEY_V1, &v1, sizeof(v1));
}
if (e == ESP_OK) {
e = nvs_commit(h);
}
nvs_close(h);
return e == ESP_OK ? HAL_OK : HAL_ERR_BUS;
}
static bool nvs_load(netcfg_t *out)
{
nvs_handle_t h;
if (nvs_open(NVS_NS, NVS_READONLY, &h) != ESP_OK) {
return false;
}
static nvs_blob_t b; /* ≈560 B,不放栈上(只在启动时进来一次) */
size_t n = sizeof(b);
bool ok = nvs_get_blob(h, NVS_KEY, &b, &n) == ESP_OK && n == sizeof(b) &&
b.ver == NVS_VERSION;
if (ok) {
*out = b.cfg;
} else {
static nvs_blob_v1_t v1;
n = sizeof(v1);
ok = nvs_get_blob(h, NVS_KEY_V1, &v1, &n) == ESP_OK && n == sizeof(v1) &&
v1.ver == NVS_VERSION_V1;
if (ok) {
memset(out, 0, sizeof(*out));
memcpy(out->wifi[0].ssid, v1.cfg.ssid, sizeof(v1.cfg.ssid));
memcpy(out->wifi[0].pass, v1.cfg.pass, sizeof(v1.cfg.pass));
memcpy(out->uri, v1.cfg.uri, sizeof(v1.cfg.uri));
memcpy(out->user, v1.cfg.user, sizeof(v1.cfg.user));
memcpy(out->mpass, v1.cfg.mpass, sizeof(v1.cfg.mpass));
out->tele_ms = v1.cfg.tele_ms;
ESP_LOGI(TAG, "旧版网络配置(一组 WiFi)已迁移;下次改配置时写成新版");
}
}
nvs_close(h);
return ok;
}
static void on_cfg_changed(void *ctx)
{
(void)ctx;
s_dirty = true; /* 执行器任务里只置标志;断开重连在 t_link 里做(esp_mqtt_client_stop 会阻塞) */
}
/* ---- 发布:任何任务都只进 s_outbox,不等、不碰 esp-mqtt ------------------- */
static bool enqueue(mlink_topic_t t, const char *data, size_t len, int qos)
{
/* 1.5 KB 不放栈上。执行器和 esp-mqtt 任务(link_error)都会进来 ⇒ s_out_mtx 护着。
这把锁持有期间只有 memcpy 和一次**不等**的入队,不会再去等任何别的东西。 */
static out_msg_t m;
if (s_outbox == NULL || len > sizeof(m.data)) {
return false;
}
/* 没连上就不攒(上线后 on_mqtt 会先补一条遥测)。读一个枚举,不拿 s_mtx。 */
if (s_st.mqtt != NET_UP) {
return false;
}
if (xSemaphoreTake(s_out_mtx, pdMS_TO_TICKS(10)) != pdTRUE) {
return false;
}
m.topic = (uint8_t)t;
m.qos = (uint8_t)qos;
m.len = (uint16_t)len;
memcpy(m.data, data, len);
const bool ok = xQueueSend(s_outbox, &m, 0) == pdTRUE;
xSemaphoreGive(s_out_mtx);
return ok;
}
static void reply_mqtt(void *ctx, const char *line, size_t len)
{
(void)ctx;
(void)enqueue(MLINK_T_RSP, line, len, 1);
}
static void reply_tele(void *ctx, const char *line, size_t len)
{
(void)ctx;
const bool ok = enqueue(MLINK_T_TELE, line, len, 0);
ST_EDIT(if (ok) { s_st.tele_sent++; } else { s_st.tele_skipped++; });
s_tele_inflight = false;
}
/* t_link 里调:把发件队列交给 esp-mqtt。s_mqtt 只在 t_link 里改,这里读它不用锁;
esp_mqtt_client_enqueue 可能等 api_lock 等到 esp-mqtt 的网络超时 —— 堵的是 t_link,
而 t_link 此刻不持任何我们的锁,esp-mqtt 的事件回调照样拿得到 s_mtx。 */
static void outbox_drain(void)
{
static out_msg_t m;
while (s_outbox != NULL && xQueueReceive(s_outbox, &m, 0) == pdTRUE) {
if (s_mqtt != NULL) {
(void)esp_mqtt_client_enqueue(s_mqtt, mlink_topic((mlink_topic_t)m.topic), m.data,
(int)m.len, m.qos, 0, true);
}
}
}
static void link_error(const char *payload, const char *err)
{
char out[96];
const size_t n = ulink_link_error(payload, err, out, sizeof(out));
if (n > 0) {
(void)enqueue(MLINK_T_RSP, out, n, 1);
}
}
/* ---- MQTT 事件(esp-mqtt 任务) ------------------------------------------- */
static void on_mqtt(void *arg, esp_event_base_t base, int32_t id, void *data)
{
(void)arg;
(void)base;
esp_mqtt_event_handle_t ev = data;
switch ((esp_mqtt_event_id_t)id) {
case MQTT_EVENT_CONNECTED: {
esp_mqtt_client_subscribe_single(ev->client, mlink_topic(MLINK_T_CMD), 1);
char buf[96];
const size_t n = mlink_state_online(phonerail_version(), s_st.ip, buf, sizeof(buf));
esp_mqtt_client_enqueue(ev->client, mlink_topic(MLINK_T_STATE), buf, (int)n, 1, 1, true);
ST_EDIT(s_st.mqtt = NET_UP);
memset(&s_tele, 0, sizeof(s_tele)); /* 一连上先报一条遥测 */
ESP_LOGI(TAG, "MQTT 已连上,订阅 %s", mlink_topic(MLINK_T_CMD));
break;
}
case MQTT_EVENT_DISCONNECTED:
ST_EDIT(if (s_st.mqtt == NET_UP) { s_st.mqtt_drops++; } s_st.mqtt = NET_CONNECTING);
break;
case MQTT_EVENT_DATA: {
const char *cmd_t = mlink_topic(MLINK_T_CMD);
if (ev->topic_len != (int)strlen(cmd_t) || memcmp(ev->topic, cmd_t, (size_t)ev->topic_len)) {
break; /* 切片的后续片没有 topic —— 首片已经按 too_long 回过了 */
}
char line[CMD_LINE_MAX];
const char *err = mlink_inbound(ev->data, ev->data_len, ev->total_data_len, line, sizeof(line));
if (err != NULL) {
link_error(NULL, err);
} else if (!cmd_queue_submit(CMD_LINK_WIFI, line, reply_mqtt, NULL)) {
link_error(line, "queue_full");
}
break;
}
default:
break;
}
}
static void mqtt_start(void)
{
static char client_id[24];
snprintf(client_id, sizeof(client_id), "phonerail-%s", mlink_id());
const esp_mqtt_client_config_t mc = {
.broker.address.uri = s_cur.uri,
.broker.verification.certificate =
(strncmp(s_cur.uri, "mqtts://", 8) == 0) ? evse_ca_pem_start : NULL,
.credentials.client_id = client_id,
.credentials.username = s_cur.user[0] ? s_cur.user : NULL,
.credentials.authentication.password = s_cur.mpass[0] ? s_cur.mpass : NULL,
.session.last_will = { .topic = mlink_topic(MLINK_T_STATE), .msg = MLINK_STATE_OFFLINE,
.qos = 1, .retain = 1 },
.session.keepalive = MQTT_KEEPALIVE_S,
.task.priority = task_desc(TASK_LINK)->prio,
.task.stack_size = 6144,
};
esp_mqtt_client_handle_t c = esp_mqtt_client_init(&mc);
if (c == NULL) {
ESP_LOGE(TAG, "MQTT 客户端建不起来");
return;
}
esp_mqtt_client_register_event(c, ESP_EVENT_ANY_ID, on_mqtt, NULL);
ST_EDIT(s_mqtt = c; s_st.mqtt = NET_CONNECTING);
esp_mqtt_client_start(c);
}
static void mqtt_stop(void)
{
esp_mqtt_client_handle_t c;
ST_EDIT(c = s_mqtt; s_mqtt = NULL; s_st.mqtt = NET_OFF);
if (c != NULL) {
esp_mqtt_client_stop(c); /* 会阻塞到它的任务退出 —— 所以只在 t_link 里调 */
esp_mqtt_client_destroy(c);
}
}
/* ---- WiFi 事件(系统事件循环) ------------------------------------------- */
/* 把 STA 配成表里第 idx 组(idx 必须 < count)。 */
static void wifi_set_sta(uint8_t idx)
{
const netcfg_wifi_t *w = &s_cur.wifi[idx];
wifi_config_t wc = { 0 };
memcpy(wc.sta.ssid, w->ssid, strlen(w->ssid));
memcpy(wc.sta.password, w->pass, strlen(w->pass));
wc.sta.threshold.authmode = w->pass[0] ? WIFI_AUTH_WPA2_PSK : WIFI_AUTH_OPEN;
esp_wifi_set_config(WIFI_IF_STA, &wc);
ST_EDIT(s_st.wifi_idx = idx);
}
static void retry_cb(void *arg)
{
(void)arg;
const int n = netcfg_wifi_count(&s_cur);
if (s_st.wifi != NET_UP && n > 0) {
const uint8_t i = s_idx < n ? s_idx : 0;
wifi_set_sta(i);
esp_wifi_connect();
}
}
static void on_wifi(void *arg, esp_event_base_t base, int32_t id, void *data)
{
(void)arg;
if (base == WIFI_EVENT && id == WIFI_EVENT_STA_DISCONNECTED) {
const wifi_event_sta_disconnected_t *d = data;
const int n = netcfg_wifi_count(&s_cur);
const bool was_up = s_st.wifi == NET_UP;
ST_EDIT(s_st.wifi_reason = d->reason; if (was_up) { s_st.wifi_drops++; } s_st.wifi =
n > 0 ? NET_CONNECTING : NET_OFF;
s_st.ip[0] = '\0'; s_st.rssi = 0);
if (s_leaving) {
s_leaving = false; /* wifi_apply 自己断的,它紧接着就去连了 */
return;
}
if (n > 0) {
/* 连上过又掉的:先重试同一组;没连上的:换下一组。退避按「轮完一整圈」算 ——
几组全连不上也是 1 s、2 s、4 s …… 地拉长,而不是每组各退各的。 */
if (!was_up && n > 1) {
s_idx = (uint8_t)((s_idx + 1) % n);
}
const uint32_t ms = mlink_backoff_ms(s_attempt++ / (uint32_t)n);
ESP_LOGW(TAG, "WiFi 断开,原因 %u,%u ms 后连「%s」", (unsigned)d->reason, (unsigned)ms,
s_cur.wifi[s_idx < n ? s_idx : 0].ssid);
esp_timer_stop(s_retry);
esp_timer_start_once(s_retry, (uint64_t)ms * 1000u);
}
} else if (base == IP_EVENT && id == IP_EVENT_STA_GOT_IP) {
const ip_event_got_ip_t *e = data;
s_attempt = 0;
ST_EDIT(s_st.wifi = NET_UP;
snprintf(s_st.ip, sizeof(s_st.ip), IPSTR, IP2STR(&e->ip_info.ip)));
ESP_LOGI(TAG, "WiFi「%s」已连上,IP %s", s_cur.wifi[s_idx].ssid, s_st.ip);
}
}
/* 按 s_cur 重新连 WiFi(t_link 里调)。 */
static void wifi_apply(void)
{
esp_timer_stop(s_retry);
s_leaving = s_st.wifi == NET_UP;
esp_wifi_disconnect();
s_attempt = 0;
const int n = netcfg_wifi_count(&s_cur);
if (n == 0) {
s_leaving = false;
ST_EDIT(s_st.wifi = NET_OFF);
return;
}
if (s_idx >= n) {
s_idx = 0;
}
wifi_set_sta(s_idx);
ST_EDIT(s_st.wifi = NET_CONNECTING);
esp_wifi_connect();
ESP_LOGI(TAG, "连 WiFi「%s」(%u/%d)", s_cur.wifi[s_idx].ssid, (unsigned)s_idx + 1, n); /* 只打 SSID,不打口令 */
}
/* ---- 对外 ---------------------------------------------------------------- */
hal_err_t mlink_esp_init(void)
{
s_mtx = xSemaphoreCreateMutex();
s_out_mtx = xSemaphoreCreateMutex();
s_outbox = xQueueCreate(OUTBOX_DEPTH, sizeof(out_msg_t));
esp_err_t e = nvs_flash_init();
if (e == ESP_ERR_NVS_NO_FREE_PAGES || e == ESP_ERR_NVS_NEW_VERSION_FOUND) {
nvs_flash_erase(); /* NVS 分区格式不认:清掉重来(里面只有本模块的配置与 WiFi 校准) */
e = nvs_flash_init();
}
if (s_mtx == NULL || s_out_mtx == NULL || s_outbox == NULL || e != ESP_OK) {
ESP_LOGE(TAG, "NVS 起不来:%s —— WiFi 链路不可用", esp_err_to_name(e));
netcfg_init(NULL, NULL, NULL, NULL);
return HAL_ERR_STATE;
}
netcfg_t loaded;
const bool have = nvs_load(&loaded);
netcfg_init(have ? &loaded : NULL, nvs_save, on_cfg_changed, NULL);
netcfg_get(&s_cur);
uint8_t mac[6];
esp_read_mac(mac, ESP_MAC_WIFI_STA);
mlink_init(mac);
ST_EDIT(snprintf(s_st.id, sizeof(s_st.id), "%s", mlink_id()));
const esp_timer_create_args_t ta = { .callback = retry_cb, .name = "wifi_retry" };
if (esp_netif_init() != ESP_OK || esp_event_loop_create_default() != ESP_OK ||
esp_netif_create_default_wifi_sta() == NULL || esp_timer_create(&ta, &s_retry) != ESP_OK) {
ESP_LOGE(TAG, "网络栈起不来 —— WiFi 链路不可用");
return HAL_ERR_STATE;
}
const wifi_init_config_t wi = WIFI_INIT_CONFIG_DEFAULT();
if (esp_wifi_init(&wi) != ESP_OK) {
ESP_LOGE(TAG, "WiFi 驱动起不来 —— WiFi 链路不可用");
return HAL_ERR_STATE;
}
esp_event_handler_register(WIFI_EVENT, WIFI_EVENT_STA_DISCONNECTED, on_wifi, NULL);
esp_event_handler_register(IP_EVENT, IP_EVENT_STA_GOT_IP, on_wifi, NULL);
esp_wifi_set_storage(WIFI_STORAGE_RAM); /* 凭据只存我们自己的那份,驱动别再存一份 */
esp_wifi_set_mode(WIFI_MODE_STA);
esp_wifi_start();
s_up = true;
ESP_LOGI(TAG, "板子 ID %s,topic %s/…", mlink_id(), mlink_topic(MLINK_T_CMD));
if (!have) {
ESP_LOGW(TAG, "还没配网:经 USB 发 wifi / mqtt 两条命令(tools/prlink.py)");
}
wifi_apply();
return HAL_OK;
}
void mlink_esp_poll(void)
{
if (!s_up) {
return;
}
if (s_dirty) {
s_dirty = false;
/* 加 / 删的不是正连着的那一组、口令也没变:WiFi 不断;broker 也没变:MQTT 也不断。
经 USB 往表里加一组备用热点,不该让板子掉线一次。 */
static netcfg_t next; /* ≈560 B,不放栈上(只有 t_link 进来) */
netcfg_get(&next);
const int j = (s_st.wifi == NET_UP) ? netcfg_wifi_find(&next, s_cur.wifi[s_idx].ssid) : -1;
const bool wifi_same = j >= 0 && strcmp(next.wifi[j].pass, s_cur.wifi[s_idx].pass) == 0;
const bool mqtt_same = strcmp(next.uri, s_cur.uri) == 0 && strcmp(next.user, s_cur.user) == 0 &&
strcmp(next.mpass, s_cur.mpass) == 0;
if (!(wifi_same && mqtt_same)) {
mqtt_stop();
}
s_cur = next;
if (wifi_same) {
s_idx = (uint8_t)j;
ST_EDIT(s_st.wifi_idx = s_idx);
} else {
wifi_apply();
}
}
/* MQTT 的生命周期只在这里管:WiFi 有地址、配了 broker、还没建客户端 ⇒ 建。
之后掉线重连由 esp-mqtt 自己做(network.reconnect_timeout_ms,默认 10 s)。 */
if (s_mqtt == NULL && s_st.wifi == NET_UP && s_cur.uri[0] != '\0') {
mqtt_start();
}
outbox_drain();
if (s_st.mqtt != NET_UP || !mlink_tele_due(&s_tele, hal_time_now_ms(), s_cur.tele_ms)) {
return;
}
wifi_ap_record_t ap;
if (esp_wifi_sta_get_ap_info(&ap) == ESP_OK) {
ST_EDIT(s_st.rssi = ap.rssi);
}
if (s_tele_inflight) {
ST_EDIT(s_st.tele_skipped++); /* 上一条还在队列里:网络或执行器堵了,不再往里塞 */
return;
}
s_tele_inflight = true;
if (!cmd_queue_submit(CMD_LINK_WIFI, "{\"cmd\":\"status\"}", reply_tele, NULL)) {
s_tele_inflight = false;
ST_EDIT(s_st.tele_skipped++);
}
}