固件 0.1.1-c5:硬重启后自动开机、MQTT 发布死锁、欠压放宽到 −250 mV
- 硬重启等到 PG 后长按电源键 5 s(新状态 hr-boot):电池端上电的小米8 停在关机态,原来的 power_cycle 等于关机。ACT_KEY_MS_ON 2000 → 5000 (实测 2 s 松手即掉电)。NO_PG 时不按。 - 修死锁:执行器持 s_mtx 等 esp-mqtt 的 api_lock,esp-mqtt 任务持 api_lock 分发 DISCONNECTED 时等 s_mtx。热点一断 USB/WiFi 全失联、按键可能松不开。 执行器现在只往 s_outbox 拷贝,由 t_link 交给 esp-mqtt。 - SR-5 欠压一侧 −100 → −250 mV(T_SAFETY_UNDER_MV),过压仍 +100 mV: 带屏开机峰值 2.1 A 时实测 −115 mV 被误关。需求 SR-5 / SW-2 / SW-5 / 验收表同步。 板 B 上板:power_cycle → hr-boot → 手机开机,最低 3778 mV / 2.12 A 不再跳闸。 Co-Authored-By: Claude Opus 5.5 <[email protected]>
这个提交包含在:
+64
-15
@@ -7,10 +7,14 @@
|
||||
| WiFi / IP 事件(掉线、拿到地址) | 系统事件循环 |
|
||||
| MQTT 事件(连上、收到命令) | esp-mqtt 自己的任务,优先级压到与 `t_link` 相同(§10.3:链路最低) |
|
||||
| **MQTT 客户端的创建 / 销毁、重配置、遥测节拍** | `t_link`(`mlink_esp_poll()`)—— 生命周期只在一个任务里管 |
|
||||
| 回应 / 遥测的发布 | 执行器任务(cmd_queue 的回调)—— 只用 `esp_mqtt_client_enqueue()`,**不阻塞** |
|
||||
| 回应 / 遥测的发布 | 执行器任务只把一份拷贝塞进 `s_outbox`(不等);`t_link` 取出来 `esp_mqtt_client_enqueue()` |
|
||||
|
||||
`esp_mqtt_client_publish()` 在网络慢的时候会阻塞到超时,放在执行器任务里就是让
|
||||
「按键该松了」等 TCP。enqueue 只进发件箱,由 esp-mqtt 自己的任务发。
|
||||
⚠️ **执行器任务一律不碰 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 重试)。
|
||||
@@ -25,6 +29,7 @@
|
||||
#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"
|
||||
@@ -65,6 +70,20 @@ 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 的实参)也能整个传进来。 */
|
||||
@@ -119,39 +138,66 @@ static void on_cfg_changed(void *ctx)
|
||||
s_dirty = true; /* 执行器任务里只置标志;断开重连在 t_link 里做(esp_mqtt_client_stop 会阻塞) */
|
||||
}
|
||||
|
||||
/* ---- 发布(执行器任务里调,只进发件箱) ----------------------------------- */
|
||||
/* ---- 发布:任何任务都只进 s_outbox,不等、不碰 esp-mqtt ------------------- */
|
||||
|
||||
static void enqueue(mlink_topic_t t, const char *data, size_t len, int qos, int retain)
|
||||
static bool enqueue(mlink_topic_t t, const char *data, size_t len, int qos)
|
||||
{
|
||||
if (xSemaphoreTake(s_mtx, pdMS_TO_TICKS(10)) != pdTRUE) {
|
||||
return;
|
||||
/* 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;
|
||||
}
|
||||
if (s_mqtt != NULL && s_st.mqtt == NET_UP) {
|
||||
(void)esp_mqtt_client_enqueue(s_mqtt, mlink_topic(t), data, (int)len, qos, retain, true);
|
||||
/* 没连上就不攒(上线后 on_mqtt 会先补一条遥测)。读一个枚举,不拿 s_mtx。 */
|
||||
if (s_st.mqtt != NET_UP) {
|
||||
return false;
|
||||
}
|
||||
xSemaphoreGive(s_mtx);
|
||||
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;
|
||||
enqueue(MLINK_T_RSP, line, len, 1, 0);
|
||||
(void)enqueue(MLINK_T_RSP, line, len, 1);
|
||||
}
|
||||
|
||||
static void reply_tele(void *ctx, const char *line, size_t len)
|
||||
{
|
||||
(void)ctx;
|
||||
enqueue(MLINK_T_TELE, line, len, 0, 0);
|
||||
ST_EDIT(s_st.tele_sent++);
|
||||
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) {
|
||||
enqueue(MLINK_T_RSP, out, n, 1, 0);
|
||||
(void)enqueue(MLINK_T_RSP, out, n, 1);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -291,12 +337,14 @@ static void wifi_apply(void)
|
||||
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 || e != ESP_OK) {
|
||||
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;
|
||||
@@ -352,6 +400,7 @@ void mlink_esp_poll(void)
|
||||
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;
|
||||
}
|
||||
|
||||
在新工单中引用
屏蔽一个用户