277 lines
10 KiB
C
277 lines
10 KiB
C
/* net_wifi_ota.c - WiFi 通道 OTA 固件下载
|
||
*
|
||
* ESP-01S 是单连接透传(CIPMUX=0),OTA 期间必须拆掉 MQTT 连接:
|
||
* 退透传("+++") → 连固件服务器 → 透传发 HTTP GET → 流式接收固件体
|
||
* → 每 3072B 写 W25Q64 槽 B + 回读校验 + 累计 CRC16
|
||
* → 整包 CRC + 固件头校验 → 重连 MQTT 上报结果 → 写 OTA 信息区 → 复位进 BL
|
||
*
|
||
* 参考工程《4路继电器控制带WIFI和4G V3.4 WIFI无回弹》的透传整包下载思路,
|
||
* 但复用本工程 W25Q64 双槽 + CRC + 向量校验 + BootLoader 回退体系(比参考工程
|
||
* 写内部 Flash APP2 的方案可靠得多)。吞吐靠 TCP 流控自适应:MCU 消费慢时
|
||
* 接收窗口收缩,服务器自然降速,不会丢字节。
|
||
*/
|
||
|
||
#include "net_wifi_ota.h"
|
||
#include "net_wifi.h"
|
||
#include "esp8266.h"
|
||
#include "network.h"
|
||
#include "air780e.h" /* bg_delay / U2_CopyBuff / Verify_Firmware_External / crc16_modbus_update */
|
||
#include "ota_app.h" /* Application_Size */
|
||
#include "uart.h"
|
||
#include "relay.h"
|
||
#include "feed_scale.h"
|
||
#include "led.h"
|
||
#include "log.h"
|
||
#include "reset_log.h"
|
||
#include "../W25Q64/w25q64.h"
|
||
#include "stdio.h"
|
||
#include "string.h"
|
||
#include <stdlib.h>
|
||
|
||
#define WIFI_OTA_CHUNK 3072u /* 与 4G OTA 相同的分块粒度 */
|
||
#define WIFI_OTA_HDR_TIMEOUT 15000u /* 等 HTTP 响应头超时 */
|
||
#define WIFI_OTA_IDLE_TIMEOUT 20000u /* 数据流静默超时(TCP 流控可能主动降速) */
|
||
|
||
static uint8_t s_blk[WIFI_OTA_CHUNK]; /* 分块暂存:写flash/校验/CRC 都按块处理 */
|
||
|
||
/* 二进制安全子串查找 */
|
||
static uint8_t *ota_memfind(uint8_t *hay, uint16_t hay_len, const char *needle)
|
||
{
|
||
uint16_t nlen = strlen(needle);
|
||
if (hay_len < nlen) return NULL;
|
||
for (uint16_t i = 0; i <= hay_len - nlen; i++) {
|
||
if (memcmp(hay + i, needle, nlen) == 0) return hay + i;
|
||
}
|
||
return NULL;
|
||
}
|
||
|
||
/* 大小写不敏感查找(Content-Length 头各服务器大小写不一) */
|
||
static uint8_t *ota_memfind_i(uint8_t *hay, uint16_t hay_len, const char *needle)
|
||
{
|
||
uint16_t nlen = strlen(needle);
|
||
if (hay_len < nlen) return NULL;
|
||
for (uint16_t i = 0; i <= hay_len - nlen; i++) {
|
||
uint16_t j;
|
||
for (j = 0; j < nlen; j++) {
|
||
char a = hay[i + j], b = needle[j];
|
||
if (a >= 'A' && a <= 'Z') a += 32;
|
||
if (b >= 'A' && b <= 'Z') b += 32;
|
||
if (a != b) break;
|
||
}
|
||
if (j == nlen) return hay + i;
|
||
}
|
||
return NULL;
|
||
}
|
||
|
||
/* 消费接收缓冲前 n 字节 */
|
||
static void ota_consume(uint16_t n)
|
||
{
|
||
if (n >= U2_CopyIndex) {
|
||
U2_CopyIndex = 0;
|
||
} else {
|
||
memmove(U2_CopyBuff, U2_CopyBuff + n, U2_CopyIndex - n);
|
||
U2_CopyIndex -= n;
|
||
}
|
||
U2_CopyBuff[U2_CopyIndex] = '\0';
|
||
}
|
||
|
||
/*==== HTTP Range 分块下载固件到 W25Q64 槽 B,成功返回 0 且 *out_size=固件大小 ====
|
||
* 每块 3072B 一个 GET(Range: bytes=off-end),MCU 自定节奏:
|
||
* 服务器一次只发一块,不存在大流量灌爆接收缓冲的问题(实测整包下载时
|
||
* 服务器 6 秒灌完 57KB,MCU 写 flash 消费不动,缓冲溢出丢字节)。 */
|
||
static int wifi_ota_download(const char *host, int port, const char *path,
|
||
uint16_t crc16, uint32_t *out_size)
|
||
{
|
||
char req[224];
|
||
uint32_t offset = 0, total_size = 0;
|
||
uint16_t crc_acc = 0xFFFF;
|
||
int last_pct = -1;
|
||
|
||
/* 拆掉 MQTT 连接,换连固件服务器(同为透传模式) */
|
||
esp_exit_transparent();
|
||
if (esp_enter_transparent(host, (uint16_t)port) != 0) {
|
||
log_warn("> WiFi OTA: 连接固件服务器失败");
|
||
return -1;
|
||
}
|
||
|
||
while (1) {
|
||
/* 最后一块按总大小截断(total_size 第一块响应头里才拿到) */
|
||
uint32_t want_end = offset + WIFI_OTA_CHUNK - 1;
|
||
if (total_size > 0 && want_end >= total_size) want_end = total_size - 1;
|
||
|
||
/* HTTP/1.1 默认 keep-alive,同一 TCP 连接上顺序发多个 Range 请求 */
|
||
snprintf(req, sizeof(req),
|
||
"GET %s HTTP/1.1\r\nHost: %s:%d\r\nUser-Agent: esp8266-ota\r\nAccept: */*\r\nRange: bytes=%u-%u\r\n\r\n",
|
||
path, host, port, (unsigned)offset, (unsigned)want_end);
|
||
esp_rx_clear();
|
||
esp_send((uint8_t *)req, strlen(req));
|
||
|
||
/* 等响应头完整(\r\n\r\n 结束) */
|
||
uint32_t hdr_end = 0;
|
||
uint32_t tick = HAL_GetTick();
|
||
while (HAL_GetTick() - tick < WIFI_OTA_HDR_TIMEOUT) {
|
||
bg_delay(50);
|
||
uint8_t *p = ota_memfind(U2_CopyBuff, U2_CopyIndex, "\r\n\r\n");
|
||
if (p) { hdr_end = (uint32_t)(p - U2_CopyBuff) + 4; break; }
|
||
if (ota_memfind(U2_CopyBuff, U2_CopyIndex, "CLOSED")) {
|
||
log_warn("> WiFi OTA: 服务器关闭连接 @%u", (unsigned)offset);
|
||
return -1;
|
||
}
|
||
}
|
||
if (hdr_end == 0) {
|
||
log_warn("> WiFi OTA: 响应头超时 @%u", (unsigned)offset);
|
||
return -1;
|
||
}
|
||
|
||
/* 必须 206(服务器支持 Range;200 说明忽略了 Range,整包灌流量消费不动,放弃) */
|
||
if (strncmp((char *)U2_CopyBuff, "HTTP/", 5) != 0) {
|
||
log_warn("> WiFi OTA: 响应异常 @%u", (unsigned)offset);
|
||
return -1;
|
||
}
|
||
if (!ota_memfind(U2_CopyBuff, 16, " 206")) {
|
||
U2_CopyBuff[hdr_end < 120 ? hdr_end : 120] = '\0';
|
||
log_warn("> WiFi OTA: 非 206 响应(服务器不支持 Range?): %.60s", U2_CopyBuff);
|
||
return -1;
|
||
}
|
||
|
||
/* 第一块:从 Content-Range: bytes 0-x/TOTAL 拿总大小,然后擦槽 B */
|
||
if (total_size == 0) {
|
||
uint8_t *cr = ota_memfind_i(U2_CopyBuff, hdr_end, "content-range:");
|
||
uint8_t *slash = cr ? (uint8_t *)strchr((char *)cr, '/') : NULL;
|
||
if (slash) total_size = (uint32_t)atoi((char *)slash + 1);
|
||
if (total_size == 0 || total_size > Application_Size) {
|
||
log_warn("> WiFi OTA: 固件总大小异常 %u", (unsigned)total_size);
|
||
return -1;
|
||
}
|
||
log_info("**************************************");
|
||
log_info("* WiFi OTA 升级开始 *");
|
||
log_info("* 固件大小: %u 字节 *", (unsigned)total_size);
|
||
log_info("**************************************");
|
||
log_info("> 正在擦除 W25Q64 新槽位...");
|
||
W25Q64_EraseRegion(W25Q64_SLOT_NEW_ADDR, W25Q64_SLOT_SIZE);
|
||
log_info("> W25Q64 新槽位擦除完成");
|
||
}
|
||
|
||
/* 本块长度 = Content-Length(≤3072,最后一块可能更小) */
|
||
uint32_t clen = 0;
|
||
{
|
||
uint8_t *cl = ota_memfind_i(U2_CopyBuff, hdr_end, "content-length:");
|
||
if (cl) clen = (uint32_t)atoi((char *)cl + 15);
|
||
}
|
||
if (clen == 0 || clen > WIFI_OTA_CHUNK) {
|
||
log_warn("> WiFi OTA: 块长度异常 %u @%u", (unsigned)clen, (unsigned)offset);
|
||
return -1;
|
||
}
|
||
ota_consume((uint16_t)hdr_end);
|
||
|
||
/* 精确收 clen 字节固件体到暂存块 */
|
||
uint16_t got = 0;
|
||
tick = HAL_GetTick();
|
||
while (got < clen) {
|
||
if (U2_CopyIndex > 0) {
|
||
tick = HAL_GetTick();
|
||
uint16_t n = U2_CopyIndex;
|
||
if (n > (uint16_t)(clen - got)) n = (uint16_t)(clen - got);
|
||
memcpy(s_blk + got, U2_CopyBuff, n);
|
||
got += n;
|
||
ota_consume(n);
|
||
} else {
|
||
if (HAL_GetTick() - tick > WIFI_OTA_IDLE_TIMEOUT) {
|
||
log_warn("> WiFi OTA: 数据接收超时 @%u 已收 %u/%u",
|
||
(unsigned)offset, got, (unsigned)clen);
|
||
log_warn("> OTA 诊断: CNDTR=%u RxState=%u rxcplt=%u ore=%u pending=%u",
|
||
(unsigned)huart4.hdmarx->Instance->CNDTR,
|
||
(unsigned)huart4.RxState,
|
||
(unsigned)g_uart4_rxcplt_cnt,
|
||
(unsigned)g_uart4_ore_cnt,
|
||
(unsigned)U2_CopyIndex);
|
||
return -1;
|
||
}
|
||
bg_delay(20);
|
||
}
|
||
}
|
||
|
||
/* 写 W25Q64 槽 B + 回读校验 + 累计 CRC */
|
||
W25Q64_WriteBuffer(W25Q64_SLOT_NEW_ADDR + offset, s_blk, clen);
|
||
if (!W25Q64_Verify(W25Q64_SLOT_NEW_ADDR + offset, s_blk, clen)) {
|
||
log_error("> WiFi OTA: W25Q64 回读校验失败 offset=%u", (unsigned)offset);
|
||
return -1;
|
||
}
|
||
for (uint16_t i = 0; i < clen; i++) {
|
||
crc_acc = crc16_modbus_update(crc_acc, s_blk[i]);
|
||
}
|
||
offset += clen;
|
||
|
||
int pct = (int)(offset * 100 / total_size);
|
||
if (pct > 100) pct = 100;
|
||
if (pct / 10 > last_pct / 10) {
|
||
last_pct = pct;
|
||
log_info("> [%3d%%] %u/%uB", pct, (unsigned)offset, (unsigned)total_size);
|
||
LED2_TOGGLE;
|
||
}
|
||
|
||
if (offset >= total_size) break;
|
||
}
|
||
|
||
/* 整包 CRC16 校验 */
|
||
log_info("> W25Q64 CRC16=0x%04X (服务器=0x%04X)", crc_acc, crc16);
|
||
if (crc16 != 0 && crc_acc != crc16) {
|
||
log_error("> WiFi OTA: CRC16 校验不匹配!中止下载");
|
||
return -1;
|
||
}
|
||
|
||
/* 固件头合法性校验(栈顶/复位向量) */
|
||
if (!Verify_Firmware_External(W25Q64_SLOT_NEW_ADDR, total_size)) {
|
||
log_error("> WiFi OTA: 固件头校验无效");
|
||
return -1;
|
||
}
|
||
|
||
*out_size = total_size;
|
||
log_info("> WiFi OTA: 下载完成,%uB 校验通过", (unsigned)total_size);
|
||
return 0;
|
||
}
|
||
|
||
/*==== WiFi OTA 入口:与 air780e_trigger_ota 对齐的语义 ====
|
||
* 成功:上报 result=1 后写信息区直接复位进 BootLoader;
|
||
* 失败:上报 result=0,恢复 MQTT 链路继续正常业务 */
|
||
void net_wifi_ota(const char *host, int port, const char *path,
|
||
const char *ota_id, uint16_t new_ver, uint16_t crc16)
|
||
{
|
||
uint32_t file_size = 0;
|
||
|
||
log_info("> WiFi OTA: 开始");
|
||
feedScaleStop(); /* 称重模式下停止投喂状态机 */
|
||
relaySet(1, 0); /* 无秤模式下直接断开投喂继电器 */
|
||
|
||
/* MQTT 还在线:先上报开始(progress 0),再屏蔽业务发布 */
|
||
NET_mqtt_report("iot.ota.progress.post", ota_id, 0);
|
||
bg_delay(500);
|
||
g_net_tx_busy = 1;
|
||
g_wifi_mqtt_ready = 0; /* MQTT 链路即将被 OTA 连接顶替 */
|
||
|
||
int ret = wifi_ota_download(host, port, path, crc16, &file_size);
|
||
|
||
/* 重连 MQTT(走完整 WiFi 建链:复位模块→连AP→透传→CONNECT/SUB)上报结果 */
|
||
if (net_wifi_init() == 0) {
|
||
g_net_tx_busy = 0;
|
||
NET_mqtt_report("iot.ota.result.post", ota_id, ret == 0 ? 1 : 0);
|
||
bg_delay(2000); /* 等结果消息发完 */
|
||
} else {
|
||
g_net_tx_busy = 0;
|
||
log_warn("> WiFi OTA: MQTT 重连失败,结果上报丢失");
|
||
}
|
||
|
||
if (ret == 0) {
|
||
log_info("**************************************");
|
||
log_info("* WiFi OTA 下载完成! *");
|
||
log_info("* 正在重启进入 BootLoader... *");
|
||
log_info("**************************************");
|
||
/* 只把固件信息写入 W25Q64,版本号在 APP 确认成功后再写入 EEPROM */
|
||
W25Q64_OTA_SetNewInfo(new_ver, file_size, crc16, ota_id);
|
||
reset_log_mark(RSN_OTA_UPDATE);
|
||
bg_delay(100);
|
||
NVIC_SystemReset();
|
||
}
|
||
log_warn("> WiFi OTA: 已中止,恢复正常运行");
|
||
}
|