固件 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]>
这个提交包含在:
zhou yang
2026-09-29 08:16:21 +08:00
共同撰写人 Claude Opus 5.5
父节点 385d2f9c47
当前提交 9fb5bba367
共修改 35 个文件,包含 5120 行新增和 3675 行删除
+121 -27
查看文件
@@ -45,8 +45,28 @@
static const char *TAG = "mlink";
#define NVS_NS "phonerail"
#define NVS_KEY "net"
#define NVS_VERSION 1 /* netcfg_t 布局一变就加一:旧版本的 blob 直接当没配置 */
#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:// 时用。 */
@@ -63,6 +83,8 @@ 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;
@@ -106,8 +128,22 @@ static hal_err_t nvs_save(void *ctx, const netcfg_t *cfg)
if (nvs_open(NVS_NS, NVS_READWRITE, &h) != ESP_OK) {
return HAL_ERR_BUS;
}
const nvs_blob_t b = { .ver = NVS_VERSION, .cfg = *cfg };
esp_err_t e = nvs_set_blob(h, NVS_KEY, &b, sizeof(b));
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);
}
@@ -121,14 +157,29 @@ static bool nvs_load(netcfg_t *out)
if (nvs_open(NVS_NS, NVS_READONLY, &h) != ESP_OK) {
return false;
}
nvs_blob_t b;
size_t n = sizeof(b);
const bool ok = nvs_get_blob(h, NVS_KEY, &b, &n) == ESP_OK && n == sizeof(b) &&
b.ver == NVS_VERSION;
nvs_close(h);
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;
}
@@ -280,10 +331,25 @@ static void mqtt_stop(void)
/* ---- 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;
if (s_st.wifi != NET_UP && s_cur.ssid[0] != '\0') {
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();
}
}
@@ -293,13 +359,24 @@ 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;
ESP_LOGW(TAG, "WiFi 断开,原因 %u,%u ms 后重试", (unsigned)d->reason,
(unsigned)mlink_backoff_ms(s_attempt));
ST_EDIT(s_st.wifi_reason = d->reason;if (s_st.wifi == NET_UP) { s_st.wifi_drops++; } s_st.wifi =
s_cur.ssid[0] ? NET_CONNECTING : NET_OFF;
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_cur.ssid[0] != '\0') {
const uint32_t ms = mlink_backoff_ms(s_attempt++);
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);
}
@@ -308,7 +385,7 @@ static void on_wifi(void *arg, esp_event_base_t base, int32_t id, void *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 已连上,IP %s", s_st.ip);
ESP_LOGI(TAG, "WiFi「%s」已连上,IP %s", s_cur.wifi[s_idx].ssid, s_st.ip);
}
}
@@ -316,20 +393,22 @@ static void on_wifi(void *arg, esp_event_base_t base, int32_t id, void *data)
static void wifi_apply(void)
{
esp_timer_stop(s_retry);
s_leaving = s_st.wifi == NET_UP;
esp_wifi_disconnect();
s_attempt = 0;
if (s_cur.ssid[0] == '\0') {
const int n = netcfg_wifi_count(&s_cur);
if (n == 0) {
s_leaving = false;
ST_EDIT(s_st.wifi = NET_OFF);
return;
}
wifi_config_t wc = { 0 };
memcpy(wc.sta.ssid, s_cur.ssid, strlen(s_cur.ssid));
memcpy(wc.sta.password, s_cur.pass, strlen(s_cur.pass));
wc.sta.threshold.authmode = s_cur.pass[0] ? WIFI_AUTH_WPA2_PSK : WIFI_AUTH_OPEN;
esp_wifi_set_config(WIFI_IF_STA, &wc);
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」", s_cur.ssid); /* 只打 SSID,不打口令 */
ESP_LOGI(TAG, "连 WiFi「%s」(%u/%d)", s_cur.wifi[s_idx].ssid, (unsigned)s_idx + 1, n); /* 只打 SSID,不打口令 */
}
/* ---- 对外 ---------------------------------------------------------------- */
@@ -391,9 +470,24 @@ void mlink_esp_poll(void)
}
if (s_dirty) {
s_dirty = false;
mqtt_stop();
netcfg_get(&s_cur);
wifi_apply();
/* 加 / 删的不是正连着的那一组、口令也没变: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)。 */