V1.5: 修复OTA确认通知发送失败(确认延后至+9s避开上报风暴,本地确认先于通知落盘,通知失败30s重试,开机上报拉开300ms)

This commit is contained in:
helei 2026-08-16 19:14:20 +08:00
parent bf05a80f39
commit 9fe86a5111
7 changed files with 71 additions and 42 deletions

View File

@ -442,23 +442,25 @@ static int ota_ver_save(uint16_t ver) {
} }
/*==== MQTT Publish: OTA进度/结果上报 ====*/ /*==== MQTT Publish: OTA进度/结果上报 ====*/
void air780e_mqtt_report(const char *header, const char *ota_id, int val) { int air780e_mqtt_report(const char *header, const char *ota_id, int val) {
if (!header || !ota_id || !ota_id[0]) return; if (!header || !ota_id || !ota_id[0]) return -1;
static char cmd[300]; static char cmd[300];
if (strstr(header, "progress")) { if (strstr(header, "progress")) {
/* 进度上报:允许丢包,发了就走 */ /* 进度上报:允许丢包,发了就走 */
sprintf(cmd, "AT+MPUB=\"/iot/data/up/%s\",0,0,\"{\\22header\\22:\\22%s\\22,\\22body\\22:{\\22id\\22:\\22%.16s\\22,\\22progress\\22:%d}}\"", sprintf(cmd, "AT+MPUB=\"/iot/data/up/%s\",0,0,\"{\\22header\\22:\\22%s\\22,\\22body\\22:{\\22id\\22:\\22%.16s\\22,\\22progress\\22:%d}}\"",
MqttInfoStr.ClientID, header, ota_id, val); MqttInfoStr.ClientID, header, ota_id, val);
CAT1_printf("%s", cmd); CAT1_printf("%s", cmd);
return 0;
} else { } else {
/* 结果上报:平台据此判定升级完成,必须可靠送达——等 OK 并重试 */ /* 结果上报:平台据此判定升级完成,必须可靠送达——等 OK 并重试 */
sprintf(cmd, "AT+MPUB=\"/iot/data/up/%s\",0,0,\"{\\22header\\22:\\22%s\\22,\\22body\\22:{\\22id\\22:\\22%.16s\\22,\\22success\\22:%s}}\"", sprintf(cmd, "AT+MPUB=\"/iot/data/up/%s\",0,0,\"{\\22header\\22:\\22%s\\22,\\22body\\22:{\\22id\\22:\\22%.16s\\22,\\22success\\22:%s}}\"",
MqttInfoStr.ClientID, header, ota_id, val ? "true" : "false"); MqttInfoStr.ClientID, header, ota_id, val ? "true" : "false");
if (catSendCmd(cmd, "OK", 3, 30) != 0) { if (catSendCmd(cmd, "OK", 3, 30) != 0) {
log_warn("> OTA 结果上报失败(重试 3 次无 OK)"); log_warn("> OTA 结果上报失败(重试 3 次无 OK)");
} else { return -1;
log_info("> OTA 结果已上报: success=%d", val);
} }
log_info("> OTA 结果已上报: success=%d", val);
return 0;
} }
} }
@ -715,31 +717,31 @@ static void air780e_trigger_ota_body(const char *host, int port, const char *pat
} }
/* 固件更新后由 APP 调用,确认运行稳定后写入 EEPROM 版本号 */ /* 固件更新后由 APP 调用,确认运行稳定后写入 EEPROM 版本号 */
void air780e_ota_confirm(void) /* 返回 0=确认通知已送达平台1=无挂起 OTA-1=本地已确认但通知未送达(上层应稍后重试) */
int air780e_ota_confirm(void)
{ {
W25Q64_OtaInfo info; W25Q64_OtaInfo info;
if (W25Q64_OTA_ReadInfo(&info) != 0) return; if (W25Q64_OTA_ReadInfo(&info) != 0) return 1;
if (info.update_state != W25Q64_STATE_INSTALLED) return; if (info.update_state != W25Q64_STATE_INSTALLED) return 1;
if (info.app_confirmed) return;
/* 先向服务器上报“确认成功”走网络层路由4G/WiFi 通道均可) */ if (!info.app_confirmed) {
/* 本地确认先行(版本号 + 确认标志落盘),防止通知发送失败导致 BL 误判回滚 */
if (info.new_version > 0) ota_ver_save(info.new_version);
int ok = 0;
for (int i = 0; i < 3 && !ok; i++) {
W25Q64_OTA_SetConfirmed();
W25Q64_OtaInfo chk;
if (W25Q64_OTA_ReadInfo(&chk) == 0 && chk.app_confirmed) ok = 1;
else bg_delay(50);
}
if (ok) log_info("> OTA 确认标志已写入");
else log_error("> OTA 确认写入失败(已重试 3 次)");
}
/* 再向服务器上报"确认成功"走网络层路由4G/WiFi 通道均可) */
char id_buf[24] = {0}; char id_buf[24] = {0};
snprintf(id_buf, sizeof(id_buf), "%u", info.new_version); /* 用版本号作为 id 上报 */ snprintf(id_buf, sizeof(id_buf), "%u", info.new_version); /* 用版本号作为 id 上报 */
NET_mqtt_report("iot.ota.confirm.post", id_buf, 1); return NET_mqtt_report("iot.ota.confirm.post", id_buf, 1);
if (info.new_version > 0) ota_ver_save(info.new_version);
/* 确认标志必须可靠落盘,否则 BL 会一直认为未确认;
3 */
int ok = 0;
for (int i = 0; i < 3 && !ok; i++) {
W25Q64_OTA_SetConfirmed();
W25Q64_OtaInfo chk;
if (W25Q64_OTA_ReadInfo(&chk) == 0 && chk.app_confirmed) ok = 1;
else bg_delay(50);
}
if (ok) log_info("> OTA 确认标志已写入");
else log_error("> OTA 确认写入失败(已重试 3 次)");
} }
@ -780,11 +782,11 @@ void air780e_init(void) {
char pub[256]; char pub[256];
sprintf(pub, "AT+MPUB=\"/iot/data/up/%s\",0,0,\"{\\22header\\22:\\22iot.prop.post\\22,\\22body\\22:{\\22pow\\22:1}}\"", MqttInfoStr.ClientID); sprintf(pub, "AT+MPUB=\"/iot/data/up/%s\",0,0,\"{\\22header\\22:\\22iot.prop.post\\22,\\22body\\22:{\\22pow\\22:1}}\"", MqttInfoStr.ClientID);
CAT1_printf("%s", pub); CAT1_printf("%s", pub);
HAL_Delay(50); HAL_Delay(300); /* 开机上报拉开间隔,避免模组 MQTT 发送排队撞车 */
if (s_iccid[0]) { if (s_iccid[0]) {
sprintf(pub, "AT+MPUB=\"/iot/data/up/%s\",0,0,\"{\\22header\\22:\\22iot.prop.post\\22,\\22body\\22:{\\22iccid\\22:\\22%s\\22}}\"", MqttInfoStr.ClientID, s_iccid); sprintf(pub, "AT+MPUB=\"/iot/data/up/%s\",0,0,\"{\\22header\\22:\\22iot.prop.post\\22,\\22body\\22:{\\22iccid\\22:\\22%s\\22}}\"", MqttInfoStr.ClientID, s_iccid);
CAT1_printf("%s", pub); CAT1_printf("%s", pub);
HAL_Delay(50); HAL_Delay(300);
} }
return; return;
} }

View File

@ -44,8 +44,8 @@ uint16_t crc16_modbus_update(uint16_t crc, uint8_t data); /* MODBUS CRC16
void air780e_init(void); void air780e_init(void);
void air780e_process(void); void air780e_process(void);
void air780e_trigger_ota(const char *host, int port, const char *path, const char *ota_id, uint16_t new_ver, uint16_t crc16); void air780e_trigger_ota(const char *host, int port, const char *path, const char *ota_id, uint16_t new_ver, uint16_t crc16);
void air780e_mqtt_report(const char *header, const char *ota_id, int percent_or_success); int air780e_mqtt_report(const char *header, const char *ota_id, int percent_or_success); /* ·µ»Ø 0=ËÍ´ï */
void air780e_ota_confirm(void); int air780e_ota_confirm(void); /* 0=֪ͨÒÑËÍ´ï 1=ÎÞ¹ÒÆð -1=֪ͨδËÍ´ï(ÐèÖØÊÔ) */
uint16_t ota_get_version(void); uint16_t ota_get_version(void);
void CAT1_printf(const char *fmt, ...); void CAT1_printf(const char *fmt, ...);

View File

@ -352,16 +352,16 @@ void NET_publish_energy(float kwh)
net_at_unlock(); net_at_unlock();
} }
void NET_mqtt_report(const char *header, const char *ota_id, int val) int NET_mqtt_report(const char *header, const char *ota_id, int val)
{ {
if (g_net_tx_busy) { log_info("> NET: 进度/结果上报被丢弃 (发送忙)"); return; } if (g_net_tx_busy) { log_info("> NET: 进度/结果上报被丢弃 (发送忙)"); return -1; }
if (net_at_trylock() != 0) { log_info("> NET: 进度/结果上报被丢弃 (AT 忙)"); return; } if (net_at_trylock() != 0) { log_info("> NET: 进度/结果上报被丢弃 (AT 忙)"); return -1; }
if (s_backend == NET_BACKEND_4G) { if (s_backend == NET_BACKEND_4G) {
air780e_mqtt_report(header, ota_id, val); int rc = air780e_mqtt_report(header, ota_id, val);
net_at_unlock(); net_at_unlock();
return; return rc;
} }
if (!header || !ota_id || !ota_id[0]) { net_at_unlock(); return; } if (!header || !ota_id || !ota_id[0]) { net_at_unlock(); return -1; }
char json[200]; char json[200];
if (strstr(header, "progress")) { if (strstr(header, "progress")) {
snprintf(json, sizeof(json), snprintf(json, sizeof(json),
@ -374,13 +374,14 @@ void NET_mqtt_report(const char *header, const char *ota_id, int val)
} }
net_wifi_publish_up(json); net_wifi_publish_up(json);
net_at_unlock(); net_at_unlock();
return 0;
} }
void NET_ota_confirm(void) int NET_ota_confirm(void)
{ {
/* 两通道复用同一确认流程:无挂起 OTA 时内部直接返回; /* 两通道复用同一确认流程:无挂起 OTA 时内部直接返回;
* NET_mqtt_report */ * NET_mqtt_report */
air780e_ota_confirm(); return air780e_ota_confirm();
} }
/*==== 下行消息统一分发(从 air780e.c 上提,与模组无关)==== /*==== 下行消息统一分发(从 air780e.c 上提,与模组无关)====

View File

@ -54,8 +54,8 @@ void NET_publish_status(int sw1, int feeding, int sw2, int sw3, int sw4,
void NET_publish_response(const char *cmd_id, int ok, const char *msg); void NET_publish_response(const char *cmd_id, int ok, const char *msg);
void NET_publish_power(int on); void NET_publish_power(int on);
void NET_publish_energy(float kwh); /* 上报累计电量,标识符 energy单位 kWh */ void NET_publish_energy(float kwh); /* 上报累计电量,标识符 energy单位 kWh */
void NET_mqtt_report(const char *header, const char *ota_id, int val); int NET_mqtt_report(const char *header, const char *ota_id, int val); /* ·µ»Ø 0=ËÍ´ï */
void NET_ota_confirm(void); int NET_ota_confirm(void); /* 0=È·ÈÏ֪ͨÒÑËÍ´ï 1=ÎÞ¹ÒÆð -1=δËÍ´ï(ÐèÖØÊÔ) */
/* 下行消息统一分发与模组无关id去重 → iot.prop.set继电器控制 → iot.ota升级。 /* 下行消息统一分发与模组无关id去重 → iot.prop.set继电器控制 → iot.ota升级。
* 4G +MSUB WiFi MQTT PUBLISH payload * 4G +MSUB WiFi MQTT PUBLISH payload

View File

@ -139,6 +139,8 @@ static void app_task(void *arg)
static uint8_t ota_simulate_fail = 0; /* =1测试模拟 OTA 确认失败(验证回退) */ static uint8_t ota_simulate_fail = 0; /* =1测试模拟 OTA 确认失败(验证回退) */
static uint8_t ota_fail_logged = 0; static uint8_t ota_fail_logged = 0;
static uint32_t ota_confirm_tick = 0; static uint32_t ota_confirm_tick = 0;
static uint8_t ota_report_pending = 0;
static uint32_t ota_report_tick = 0;
static uint32_t mqtt_ready_tick = 0; static uint32_t mqtt_ready_tick = 0;
static uint8_t mqtt_ready_recorded = 0; static uint8_t mqtt_ready_recorded = 0;
static uint8_t mqtt_first_status_reported = 0; static uint8_t mqtt_first_status_reported = 0;
@ -160,9 +162,9 @@ static void app_task(void *arg)
ota_confirm_tick = HAL_GetTick(); ota_confirm_tick = HAL_GetTick();
} }
/* 首次发布成功后延迟 3s 再 OTA 确认 */ /* 首次发布成功后延迟 9s 再 OTA 确认避开开机上报风暴pow/ICCID/状态/电能) */
if (!ota_confirmed && g_net_mqtt_ready && g_net_first_publish_ok && if (!ota_confirmed && g_net_mqtt_ready && g_net_first_publish_ok &&
(HAL_GetTick() - ota_confirm_tick) > 3000) { (HAL_GetTick() - ota_confirm_tick) > 9000) {
if (ota_simulate_fail) { if (ota_simulate_fail) {
if (!ota_fail_logged) { if (!ota_fail_logged) {
ota_fail_logged = 1; ota_fail_logged = 1;
@ -171,8 +173,23 @@ static void app_task(void *arg)
/* 不置位 ota_confirmed让 1 分钟健康超时触发复位 */ /* 不置位 ota_confirmed让 1 分钟健康超时触发复位 */
} else { } else {
ota_confirmed = 1; ota_confirmed = 1;
NET_ota_confirm(); if (NET_ota_confirm() < 0) { /* <0 才是未送达1=无挂起 OTA 属正常 */
log_info("> OTA 已确认MQTT Ready + 首次发布成功 + 延迟3s"); ota_report_pending = 1; /* 通知未送达30s 后重试 */
ota_report_tick = HAL_GetTick();
log_warn("> OTA 确认通知未送达,稍后重试");
} else {
log_info("> OTA 已确认MQTT Ready + 首次发布成功 + 延迟9s");
}
}
}
/* OTA 确认通知未送达:每 30s 重试直到成功(本地确认标志已落盘,仅补平台通知) */
if (ota_report_pending && g_net_mqtt_ready &&
(HAL_GetTick() - ota_report_tick) > 30000) {
ota_report_tick = HAL_GetTick();
if (NET_ota_confirm() >= 0) {
ota_report_pending = 0;
log_info("> OTA 确认通知重试成功");
} }
} }

View File

@ -252,6 +252,12 @@ W25Q64 外部 Flash8MB
| 槽 B | `0x040000` (256KB) | 新固件下载暂存 | | 槽 B | `0x040000` (256KB) | 新固件下载暂存 |
| 信息区 A/B | `0x080000` / `0x081000` (各 4KB) | OTA 状态双副本magic + 序号 + checksum | | 信息区 A/B | `0x080000` / `0x081000` (各 4KB) | OTA 状态双副本magic + 序号 + checksum |
### OTA 确认通知可靠性
- 确认通知(`iot.ota.confirm.post`)延后到首次发布成功后 **9 秒**发送避开开机上报风暴pow/ICCID/状态/电能挤占模组 MQTT 发送窗口)
- 本地确认标志和版本号**先于**通知落盘——通知发送失败不会导致 BL 误判回滚
- 通知未送达时每 30s 自动重试直到成功(仅补平台通知);开机 pow/ICCID 上报间隔拉开到 300ms
### 健康检查与回退 ### 健康检查与回退
新固件"健康" = 4G 附着 → MQTT 连接 → 首次发布成功。回退判据(**3 次真实故障**才回退): 新固件"健康" = 4G 附着 → MQTT 连接 → 首次发布成功。回退判据(**3 次真实故障**才回退):

View File

@ -137,3 +137,6 @@
(标识符 iccid字符串平台物模型加字符串字段即可便于物联网卡套餐到期查询缴费 (标识符 iccid字符串平台物模型加字符串字段即可便于物联网卡套餐到期查询缴费
- 4G 运行时关闭 WiFi 模块ESP_EN(PA6) 默认低电平,切 WiFi 才拉高、切回 4G 拉低断电(省电/延长法拉电容续航) - 4G 运行时关闭 WiFi 模块ESP_EN(PA6) 默认低电平,切 WiFi 才拉高、切回 4G 拉低断电(省电/延长法拉电容续航)
- 断电消息提速:电量 <10% 判定从 3s 去抖改为 100ms 快速复核确认后优先发 pow:0再写记录 500ms 让模组发完再复位5.5F 电容下原逻辑来不及上报 - 断电消息提速:电量 <10% 判定从 3s 去抖改为 100ms 快速复核确认后优先发 pow:0再写记录 500ms 让模组发完再复位5.5F 电容下原逻辑来不及上报
- 修复 OTA 确认通知发送失败("重试 3 次无 OK"):确认从首次发布+3s 延后到 +9s 避开开机上报风暴;
本地确认标志/版本号先于通知落盘(通知失败不再影响防回滚);通知未送达每 30s 重试直到成功;
开机 pow/ICCID 上报间隔拉开到 300ms