- 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]>
574 行
24 KiB
C
574 行
24 KiB
C
/* 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 重试)。
|
||
没有任何「连不上就……」的分支会碰到输出 —— 五台手机照常供电。
|
||
0.1.19 起多一件:WiFi 连续 60 s 连不上就**开配网热点**(prov.h),STA 照常在后台重连,
|
||
连上了热点自己关。热点同样不碰输出。 */
|
||
#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 "prov.h"
|
||
#include "prov_esp.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;
|
||
static volatile bool s_slow; /* 有人在配网网页上:重连间隔不短于 PROV_SLOW_MS(prov.h) */
|
||
static uint32_t s_ap_try_ms; /* 上次开 / 关热点是什么时候(没成功时别每拍都试) */
|
||
static bool s_ap_tried;
|
||
|
||
/* 发件队列(见文件头):执行器 / 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 sta_connect(void)
|
||
{
|
||
if (esp_wifi_connect() != ESP_OK) {
|
||
esp_timer_stop(s_retry);
|
||
esp_timer_start_once(s_retry, 3000u * 1000u);
|
||
}
|
||
}
|
||
|
||
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);
|
||
sta_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);
|
||
}
|
||
uint32_t ms = mlink_backoff_ms(s_attempt++ / (uint32_t)n);
|
||
if (s_slow && ms < PROV_SLOW_MS) {
|
||
ms = PROV_SLOW_MS; /* 每试一次热点都要离开信道扫一圈,试密了网页就卡(prov.h) */
|
||
}
|
||
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);
|
||
sta_connect();
|
||
ESP_LOGI(TAG, "连 WiFi「%s」(%u/%d)", s_cur.wifi[s_idx].ssid, (unsigned)s_idx + 1, n); /* 只打 SSID,不打口令 */
|
||
}
|
||
|
||
/* ---- 配网热点(t_link 里调):判据在 prov.c,开关在 prov_esp.c ------------- */
|
||
|
||
static void prov_poll(void)
|
||
{
|
||
const uint32_t now = hal_time_now_ms();
|
||
const prov_in_t in = { .n_wifi = netcfg_wifi_count(&s_cur), .wifi_up = s_st.wifi == NET_UP,
|
||
.ap_sta = prov_esp_sta_count(), .touched = prov_esp_take_touch() };
|
||
const prov_out_t o = prov_step(&in, now);
|
||
s_slow = o.hold;
|
||
if (o.ap != prov_esp_on() && (!s_ap_tried || (uint32_t)(now - s_ap_try_ms) >= 10000u)) {
|
||
s_ap_tried = true;
|
||
s_ap_try_ms = now;
|
||
prov_esp_set(o.ap); /* 没成功就 10 s 后再来,不是每拍都试 */
|
||
}
|
||
const bool on = prov_esp_on();
|
||
if (on == o.ap) {
|
||
s_ap_tried = false;
|
||
}
|
||
if (on != s_st.ap_on || (uint8_t)in.ap_sta != s_st.ap_sta || (uint8_t)o.why != s_st.ap_why) {
|
||
ST_EDIT(s_st.ap_on = on; s_st.ap_sta = (uint8_t)in.ap_sta; s_st.ap_why = (uint8_t)o.why);
|
||
}
|
||
}
|
||
|
||
/* ---- 对外 ---------------------------------------------------------------- */
|
||
|
||
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;
|
||
}
|
||
esp_netif_t *ap_netif = esp_netif_create_default_wifi_ap(); /* 配网热点用;平时模式是 STA,它闲着 */
|
||
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();
|
||
prov_init(hal_time_now_ms());
|
||
prov_esp_init(ap_netif, mlink_id());
|
||
ST_EDIT(snprintf(s_st.ap_ssid, sizeof(s_st.ap_ssid), "%s", prov_esp_ssid());
|
||
snprintf(s_st.ap_pass, sizeof(s_st.ap_pass), "%s", prov_esp_pass()));
|
||
s_up = true;
|
||
ESP_LOGI(TAG, "板子 ID %s,topic %s/…", mlink_id(), mlink_topic(MLINK_T_CMD));
|
||
if (!have) {
|
||
ESP_LOGW(TAG, "还没配网:连热点「%s」开网页,或经 USB 发 wifi / mqtt(tools/prlink.py)", prov_esp_ssid());
|
||
}
|
||
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();
|
||
}
|
||
/* 没连着的时候:刚加 / 刚改口令的那一组先试 —— 人正等着看它连不连得上(配网网页尤其)。 */
|
||
uint8_t first = s_idx;
|
||
for (int i = 0; i < netcfg_wifi_count(&next); i++) {
|
||
const int k = netcfg_wifi_find(&s_cur, next.wifi[i].ssid);
|
||
if (k < 0 || strcmp(s_cur.wifi[k].pass, next.wifi[i].pass) != 0) {
|
||
first = (uint8_t)i;
|
||
break;
|
||
}
|
||
}
|
||
s_cur = next;
|
||
if (wifi_same) {
|
||
s_idx = (uint8_t)j;
|
||
ST_EDIT(s_st.wifi_idx = s_idx);
|
||
} else {
|
||
s_idx = first;
|
||
wifi_apply();
|
||
}
|
||
}
|
||
prov_poll();
|
||
/* 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++);
|
||
}
|
||
}
|