/* 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 "../W25Q64/w25q64.h" #include "stdio.h" #include "string.h" #include #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: connect firmware server failed"); 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: server closed @%u", (unsigned)offset); return -1; } } if (hdr_end == 0) { log_warn("> WiFi OTA: header timeout @%u", (unsigned)offset); return -1; } /* 必须 206(服务器支持 Range;200 说明忽略了 Range,整包灌流量消费不动,放弃) */ if (strncmp((char *)U2_CopyBuff, "HTTP/", 5) != 0) { log_warn("> WiFi OTA: bad response @%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: not 206 (server no 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: bad total size %u", (unsigned)total_size); return -1; } log_info("**************************************"); log_info("* WiFi OTA Update Start *"); log_info("* Size: %u bytes *", (unsigned)total_size); log_info("**************************************"); log_info("> Erasing W25Q64 new slot..."); W25Q64_EraseRegion(W25Q64_SLOT_NEW_ADDR, W25Q64_SLOT_SIZE); log_info("> W25Q64 new slot erased"); } /* 本块长度 = 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: bad chunk len %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: body timeout @%u got %u/%u", (unsigned)offset, got, (unsigned)clen); log_warn("> OTA diag: 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 verify fail at 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 (server=0x%04X)", crc_acc, crc16); if (crc16 != 0 && crc_acc != crc16) { log_error("> WiFi OTA: CRC16 mismatch! abort"); return -1; } /* 固件头合法性校验(栈顶/复位向量) */ if (!Verify_Firmware_External(W25Q64_SLOT_NEW_ADDR, total_size)) { log_error("> WiFi OTA: firmware header invalid"); return -1; } *out_size = total_size; log_info("> WiFi OTA: download complete, %uB OK", (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: in"); 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 reconnect failed, result report lost"); } if (ret == 0) { log_info("**************************************"); log_info("* WiFi OTA Download Complete! *"); log_info("* Rebooting to BootLoader... *"); log_info("**************************************"); /* 只把固件信息写入 W25Q64,版本号在 APP 确认成功后再写入 EEPROM */ W25Q64_OTA_SetNewInfo(new_ver, file_size, crc16); bg_delay(100); NVIC_SystemReset(); } log_warn("> WiFi OTA: aborted, resume normal operation"); }