shuichan_old/V1.1/STM32F103_App1/HardWare/CAR/air780e.c

758 lines
30 KiB
C
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#include "air780e.h"
#include "ota_app.h"
#include "relay.h"
#include "led.h"
#include "network.h"
#include "uart.h"
#include "main.h"
#include "system.h"
#include "24c02.h"
#include "proto.h"
#include "feed_scale.h"
#include "app_loop.h"
#include "../W25Q64/w25q64.h"
#include "stdio.h"
#include "string.h"
#include <stdarg.h>
/* AT指令发送: 格式化后通过USART3发出, 末尾自动加\r\n */
void CAT1_printf(const char *fmt, ...)
{
static uint32_t last_tx_tick = 0;
char buf[384];
int n;
va_list ap;
/* 任意两条 AT 指令之间至少间隔 30ms保证 4G 模块不丢包 */
while (HAL_GetTick() - last_tx_tick < 30) {
HAL_Delay(1);
}
va_start(ap, fmt);
n = vsnprintf(buf, sizeof(buf), fmt, ap);
va_end(ap);
HAL_UART_Transmit(&huart3, (uint8_t *)buf, strlen(buf), 500);
HAL_UART_Transmit(&huart3, (uint8_t *)"\r\n", 2, 500);
last_tx_tick = HAL_GetTick();
if (n >= (int)sizeof(buf)) {
log_warn("> CAT1: tx truncated! need %d bytes", n);
}
/* 任意 MPUB 发送成功即认为首次发布成功,作为 OTA 业务健康标志之一 */
if (g_air780e_mqtt_ready && !g_air780e_first_publish_ok && strstr(buf, "AT+MPUB")) {
g_air780e_first_publish_ok = 1;
log_info("> 4G: first publish ok");
}
}
uint8_t U2_CopyBuff[U2_COPY_SIZE];
volatile uint16_t U2_CopyIndex = 0;
volatile uint8_t U2_CopyFlag = 0;
volatile uint8_t g_air780e_mqtt_ready = 0;
volatile uint8_t g_air780e_first_publish_ok = 0;
static MqttConfig mqtt_cfg;
/*==== 外部 Flash 校验:读取固件头 8 字节判断合法性 ====*/
static int Verify_Firmware_External(uint32_t ext_addr, uint32_t size)
{
uint8_t hdr[8];
W25Q64_Read(ext_addr, hdr, 8);
uint32_t sp = (uint32_t)hdr[0] | ((uint32_t)hdr[1] << 8) | ((uint32_t)hdr[2] << 16) | ((uint32_t)hdr[3] << 24);
uint32_t rv = (uint32_t)hdr[4] | ((uint32_t)hdr[5] << 8) | ((uint32_t)hdr[6] << 16) | ((uint32_t)hdr[7] << 24);
if ((sp & 0x2FFE0000ul) != 0x20000000ul) { log_info("> VF ext: SP=0x%X", (unsigned)sp); return 0; }
if (rv < Application_1_Addr || rv > (Application_1_Addr + size)) { log_info("> VF ext: RV=0x%X", (unsigned)rv); return 0; }
return 1;
}
/*==== 4G Reset Control ====*/
/*==== 4G Reset Control ====*/
static void cat_reset(void) {
log_info("> 4G: Reset...");
/* 关闭 NVIC 中断, 防止模块复位期间乱码触发中断风暴 */
HAL_NVIC_DisableIRQ(USART3_IRQn);
HAL_NVIC_DisableIRQ(DMA1_Channel3_IRQn);
HAL_UART_DMAStop(&huart3);
/* 确保 DMA 通道关闭, 清除标志 */
DMA1_Channel3->CCR &= ~DMA_CCR_EN;
while (DMA1_Channel3->CCR & DMA_CCR_EN);
DMA1->IFCR = DMA_IFCR_CGIF3;
__HAL_UART_CLEAR_IDLEFLAG(&huart3);
__HAL_UART_CLEAR_OREFLAG(&huart3);
__HAL_UART_CLEAR_PEFLAG(&huart3);
__HAL_UART_CLEAR_FEFLAG(&huart3);
__HAL_UART_CLEAR_NEFLAG(&huart3);
U2_CopyIndex = 0;
U2_CopyBuff[0] = 0;
U2_CopyFlag = 0;
GPIO_InitTypeDef g = {0};
__HAL_RCC_GPIOA_CLK_ENABLE();
g.Pin = CAT_RST_PIN; g.Mode = GPIO_MODE_OUTPUT_PP; g.Pull = GPIO_NOPULL; g.Speed = GPIO_SPEED_FREQ_LOW;
HAL_GPIO_Init(CAT_RST_PORT, &g);
CAT_RST_HIGH(); HAL_Delay(1000); CAT_RST_LOW();
for (int i = 0; i < 40; i++) { IWDG_Feed(); HAL_Delay(100); } /* 等4s期间喂狗 */
/* 模块复位完成后重新启动 DMA+IDLE 接收 */
U2_CopyIndex = 0;
U2_CopyBuff[0] = 0;
U2_CopyFlag = 0;
/* 清可能存在的 UART 错误/溢出标志 */
__HAL_UART_CLEAR_PEFLAG(&huart3);
__HAL_UART_CLEAR_FEFLAG(&huart3);
__HAL_UART_CLEAR_NEFLAG(&huart3);
__HAL_UART_CLEAR_OREFLAG(&huart3);
__HAL_UART_CLEAR_IDLEFLAG(&huart3);
HAL_UART_Receive_DMA(&huart3, Rx_Buf, Rx_Max);
__HAL_UART_ENABLE_IT(&huart3, UART_IT_IDLE);
HAL_NVIC_EnableIRQ(DMA1_Channel3_IRQn);
HAL_NVIC_EnableIRQ(USART3_IRQn);
log_info("> 4G: Reset done");
}
/* OTA 前直接硬件复位 4G 模块, 与初始化时一致 */
static void ota_module_reset(void) {
cat_reset();
}
/*==== AT Command: 带重试, 参考代码风格 ====*/
/* 切片等待: 每 20ms 让出一次后台任务(蓝牙命令/秤轮询/LED/电量),
* 避免长时间 AT 等待把主循环业务全部堵死 */
static void bg_delay(uint32_t ms)
{
while (ms >= 20) {
HAL_Delay(20);
app_bg_service();
ms -= 20;
}
if (ms) HAL_Delay(ms);
}
/* cmd: AT指令, expect: 期望应答关键字, retry: 重试次数, timeout: 超时(×100ms) */
/* 返回 0=成功, 非0=失败 */
uint8_t catSendCmd(const char *cmd, const char *expect, uint8_t retry, uint8_t timeout) {
uint8_t result = 1;
while (retry > 0 && result != 0) {
/* 只复位已消费的部分, 不擦除整个20KB缓冲区 */
U2_CopyIndex = 0;
U2_CopyBuff[0] = 0;
U2_CopyFlag = 0;
CAT1_printf("%s", cmd);
uint8_t t = timeout;
while (--t) {
bg_delay(100);
if (U2_CopyFlag) {
U2_CopyFlag = 0;
if (strstr((char *)U2_CopyBuff, expect)) {
result = 0;
break;
}
}
}
if (t == 0) {
log_warn("> AT t/o [r%u]: %s", retry, cmd);
}
retry--;
}
return result;
}
/* 兼容旧接口: 单次发送, timeout 毫秒 (不会自动追加重试) */
int air780e_send_at(const char *cmd, const char *expect, int timeout_ms) {
return catSendCmd(cmd, expect, 1, (uint8_t)((timeout_ms + 99) / 100));
}
/* 等待模块网络附着: 轮询 AT+CGATT?, 直到 +CGATT: 1 或 PDN 激活 */
static int cat_wait_cgatt_attached(int max_wait_s) {
int waited = 0;
while (waited < max_wait_s * 10) {
U2_CopyIndex = 0;
U2_CopyBuff[0] = 0;
U2_CopyFlag = 0;
CAT1_printf("AT+CGATT?");
uint8_t t = 30; /* 单次查询等 3 秒 */
while (--t) {
bg_delay(100);
if (U2_CopyFlag) {
U2_CopyFlag = 0;
if (strstr((char *)U2_CopyBuff, "+CGATT: 1")) {
log_info("> 4G: CGATT attached");
return 0;
}
if (strstr((char *)U2_CopyBuff, "+CGEV: ME PDN ACT 1")) {
log_info("> 4G: PDN activated");
return 0;
}
}
}
waited += 30; /* 每次轮询约 3 秒 */
log_info("> 4G: CGATT not attached, retry %d/%d", waited / 10, max_wait_s);
}
log_warn("> 4G: CGATT attach timeout");
return -1;
}
/*==== Network ====*/
/* 4G 模块目标波特率:模块出厂默认 115200AT+IPR 配置后掉电保存 */
#define CAT_BAUD_TARGET 921600u
#define CAT_BAUD_DEFAULT 115200u
static int cat_connect_network(void) {
log_info("> 4G: Net...");
/* 波特率同步:永远先按 115200 探测(出厂/自适应模式的模块不能被
其他波特率的乱码干扰侦测窗口),通了就固定为 921600 并保存;
不通说明模块已是固定 921600直接切换 */
USART3_SetBaudRate(CAT_BAUD_DEFAULT);
if (catSendCmd("AT", "OK", 1, 5) == 0) {
if (catSendCmd("AT+IPR=921600", "OK", 1, 30) != 0) return -1;
USART3_SetBaudRate(CAT_BAUD_TARGET);
bg_delay(100);
catSendCmd("AT&W", "OK", 1, 20); /* 保存配置,模块复位后保持 921600 */
if (catSendCmd("AT", "OK", 3, 20) != 0) return -1;
log_info("> 4G: baud -> 921600");
} else {
USART3_SetBaudRate(CAT_BAUD_TARGET);
if (catSendCmd("AT", "OK", 3, 20) != 0) return -1;
}
if (catSendCmd("AT", "OK", 3, 20) != 0) return -1;
if (catSendCmd("ATE0", "OK", 1, 10) != 0) return -1;
/* 检测 SIM 卡:多次查询 AT+CPIN?,识别无 SIM 卡场景 */
int sim_retry = 3;
while (sim_retry-- > 0) {
U2_CopyIndex = 0; U2_CopyBuff[0] = 0; U2_CopyFlag = 0;
CAT1_printf("AT+CPIN?");
uint8_t t = 30;
while (--t) {
bg_delay(100);
if (U2_CopyFlag) {
U2_CopyFlag = 0;
if (strstr((char *)U2_CopyBuff, "READY")) {
break;
}
if (strstr((char *)U2_CopyBuff, "NO SIM") ||
strstr((char *)U2_CopyBuff, "NOSIM") ||
strstr((char *)U2_CopyBuff, "SIM NOT INSERTED") ||
strstr((char *)U2_CopyBuff, "+CME ERROR: 10")) {
log_warn("> 4G: no SIM card detected");
g_no_sim_detected = 1;
return -2;
}
}
}
if (strstr((char *)U2_CopyBuff, "READY")) break;
HAL_Delay(1000);
}
if (!strstr((char *)U2_CopyBuff, "READY")) {
log_warn("> 4G: SIM not ready");
return -1;
}
if (cat_wait_cgatt_attached(30) != 0) return -1;
if (catSendCmd("AT+SAPBR=3,1,\"CONTYPE\",\"GPRS\"", "OK", 1, 20) != 0) return -1;
if (catSendCmd("AT+SAPBR=3,1,\"APN\",\"\"", "OK", 1, 20) != 0) return -1;
if (catSendCmd("AT+SAPBR=1,1", "OK", 3, 150) != 0) return -1;
log_info("> 4G: Net OK"); return 0;
}
/*==== MQTT Connect ====*/
static int cat_connect_mqtt(void) {
char b[120]; log_info("> 4G: MQTT...");
/* 复用 catSendCmd 等待 MCONNECT 的真正连接确认, 不清空已有缓冲区 */
sprintf(b, "AT+MCONFIG=\"%s\",\"%s\",\"%s\",0,0,0,0", mqtt_cfg.ClientID, mqtt_cfg.Username, mqtt_cfg.Passward);
if (catSendCmd(b, "OK", 1, 15) != 0) return -1;
/* 等待 TCP 建立: "CONNECT OK" 或 "OK" */
sprintf(b, "AT+MIPSTART=\"%s\",%d", mqtt_cfg.ServerIP, mqtt_cfg.ServerPort);
if (catSendCmd(b, "OK", 1, 60) != 0) return -1;
/* 参考代码也仅等待 OK, 所以兼容处理: 发完后再等一下 CONNECT OK */
if (!strstr((char *)U2_CopyBuff, "CONNECT OK")) {
int tcp_t = 30; /* 3s */
U2_CopyFlag = 0;
while (--tcp_t >= 0) {
bg_delay(100);
if (U2_CopyFlag) {
U2_CopyFlag = 0;
if (strstr((char *)U2_CopyBuff, "CONNECT OK") ||
strstr((char *)U2_CopyBuff, "+MIPCALLBACK")) break;
}
}
if (tcp_t < 0) {
log_warn("> 4G: TCP connect t/o, last[%d]: %.80s", U2_CopyIndex, U2_CopyBuff);
}
}
/* 建立 MQTT 连接 */
if (catSendCmd("AT+MCONNECT=1,60", "OK", 1, 60) != 0) return -1;
/* 等待 MQTT 连接完成Air780E 返回 CONNACK OK 表示成功 */
if (!strstr((char *)U2_CopyBuff, "CONNACK OK") && !strstr((char *)U2_CopyBuff, "CONNECT OK")) {
int mqtt_t = 30; /* 3s */
U2_CopyFlag = 0;
while (--mqtt_t >= 0) {
bg_delay(100);
if (U2_CopyFlag) {
U2_CopyFlag = 0;
if (strstr((char *)U2_CopyBuff, "CONNACK OK") ||
strstr((char *)U2_CopyBuff, "CONNECT OK") ||
strstr((char *)U2_CopyBuff, "+MCONNECTED")) break;
}
}
if (mqtt_t < 0) {
log_warn("> 4G: MQTT connect t/o, last[%d]: %.80s", U2_CopyIndex, U2_CopyBuff);
return -1;
}
}
sprintf(b, "AT+MSUB=\"%s%s\",0", mqtt_cfg.Topic, mqtt_cfg.ClientID);
if (catSendCmd(b, "SUBACK", 1, 50) != 0) {
/* 如果没等到 SUBACK 但等到了 OK, 也算订阅命令已接受 */
if (strstr((char *)U2_CopyBuff, "OK")) {
log_info("> 4G: MSUB accepted (OK)");
} else {
log_warn("> 4G: MSUB fail, buff[%d]: %.80s", U2_CopyIndex, U2_CopyBuff);
return -1;
}
}
log_info("> 4G: MQTT OK"); return 0;
}
/*==== OTA: 版本管理 ====*/
/* 版本号存于外置 EEPROM (MqttInfoStr.Ver), 断电保持 */
uint16_t ota_get_version(void) {
return MqttInfoStr.Ver;
}
static int ota_ver_save(uint16_t ver) {
MqttInfo_Str tmp;
int retry = 3;
while (retry--) {
MqttInfoStr.Ver = ver;
uint8_t err = eepromWriteData(0, &MqttInfoStr, MQTT_STRUCT_LEN);
if (err == 0) {
/* 写入后立刻回读验证 */
memset(&tmp, 0, MQTT_STRUCT_LEN);
eepromReadData(0, &tmp, MQTT_STRUCT_LEN);
if (tmp.Ver == ver) {
log_info("> OTA ver saved & verified: %u", ver);
return 0;
}
log_warn("> OTA ver verify mismatch: wrote=%u, read=%u", ver, tmp.Ver);
}
HAL_Delay(20);
}
log_warn("> OTA ver save FAILED: %u", ver);
return -1;
}
/*==== MQTT Publish: OTA进度/结果上报 ====*/
void air780e_mqtt_report(const char *header, const char *ota_id, int val) {
if (!header || !ota_id || !ota_id[0]) return;
static char cmd[300];
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}}\"",
MqttInfoStr.ClientID, header, ota_id, val);
CAT1_printf("%s", cmd);
} else {
/* 结果上报:平台据此判定升级完成,必须可靠送达——等 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}}\"",
MqttInfoStr.ClientID, header, ota_id, val ? "true" : "false");
if (catSendCmd(cmd, "OK", 3, 30) != 0) {
log_warn("> OTA result report FAILED (no OK after 3 retries)");
} else {
log_info("> OTA result reported: success=%d", val);
}
}
}
/*==== MQTT Dispatch (with dedup) ====*/
/* 处理 U2_CopyBuff 中的第一条 MQTT 消息, 如果有下一条则移动到缓冲区开头 */
static void dispatch_one_mqtt_msg(void) {
char *d = (char *)U2_CopyBuff;
char *sub = strstr(d, "+MSUB:");
char *next = NULL;
int has_next = 0;
if (!sub && !strstr(d, "CLOSED") && !strstr(d, "+NO Service") && !strstr(d, "boot.rom")) {
U2_CopyIndex = 0;
U2_CopyBuff[0] = 0;
return;
}
/* 找下一条 +MSUB */
if (sub) {
char *p = sub + 6;
next = strstr(p, "+MSUB:");
if (next) has_next = 1;
}
/* 临时截断只处理第一条 */
char saved = 0;
if (next) {
saved = *next;
*next = '\0';
}
log_info("> MQTT: %.200s", d);
if (strstr(d, "+NO Service")) {
if (next) *next = saved;
log_error("> 4G exception: NO Service, reset");
HAL_Delay(1000);
NVIC_SystemReset();
}
if (strstr(d, "CLOSED")) {
if (next) *next = saved;
log_error("> 4G exception: MQTT/TCP CLOSED, reset");
HAL_Delay(1000);
NVIC_SystemReset();
}
if (strstr(d, "boot.rom")) {
if (next) *next = saved;
log_error("> 4G exception: module reboot (boot.rom), reset");
HAL_Delay(1000);
NVIC_SystemReset();
}
/* dedup by message id */
static char last_id[20] = {0};
char cur_id[20] = {0};
proto_get_id((char*)d, cur_id, sizeof(cur_id));
if (cur_id[0] && strcmp(cur_id, last_id) == 0) {
log_info("> MQTT: dup skip, id=%.16s", cur_id);
} else {
strncpy(last_id, cur_id, sizeof(last_id)-1);
/* relay control: 执行后再消费缓冲区, 让 relayAction 里的 AT 发送完成 */
if (strstr(d, "iot.prop.set")) { relay_action((uint8_t *)d); }
/* OTA upgrade */
if (strstr(d, "iot.ota.upgrade.post")) {
log_info("> OTA cmd!");
char host[30]={0}, path[80]={0}, ota_id[20]={0}; int port = 81;
uint16_t new_ver = 0;
uint16_t crc16 = 0;
/* 解析版本号与 crc16 */
{ uint32_t v = 0; if (proto_get_u32((char*)d, "ver", &v)) new_ver = (uint16_t)v; }
proto_get_hex16((char*)d, "crc16", &crc16);
log_info("> OTA ver: cur=%u, new=%u, crc16=0x%04X", MqttInfoStr.Ver, new_ver, crc16);
/* 版本号 ≤ 当前版本 → 跳过下载 (防止重复或降级) */
if (new_ver > 0 && new_ver <= MqttInfoStr.Ver) {
proto_get_id((char*)d, ota_id, sizeof(ota_id));
log_info("> OTA: same or lower ver, skip (cur=%u, new=%u)", MqttInfoStr.Ver, new_ver);
air780e_mqtt_report("iot.ota.progress.post", ota_id, 10);
} else {
strncpy(host, mqtt_cfg.ServerIP, sizeof(host)-1); strcpy(path, "/firmware.bin");
proto_get_id((char*)d, ota_id, sizeof(ota_id));
{ char fu[100]={0};
if (proto_get_str((char*)d, "url", fu, sizeof(fu))) {
log_info("> OTA url: %s", fu); char *p=fu;
if(strncmp(p,"http://",7)==0)p+=7;
char *sl=strchr(p,'/'),*co=strchr(p,':');
if(co&&(!sl||co<sl)){ int hl=co-p; memcpy(host,p,hl); host[hl]=0;
co++; port=0; while(*co>='0'&&*co<='9'){port=port*10+(*co-'0');co++;}
if(sl)strncpy(path,sl,sizeof(path)-1); }
else if(sl){ int hl=sl-p; memcpy(host,p,hl); host[hl]=0; strncpy(path,sl,sizeof(path)-1); }
else strncpy(host,p,sizeof(host)-1);
}}
log_info("> OTA: %s:%d%s", host, port, path);
air780e_trigger_ota(host, port, path, ota_id, new_ver, crc16);
}
}
}
/* 恢复截断 */
if (next) *next = saved;
/* 移动剩余内容到缓冲区开头 */
if (has_next && next) {
int remain = U2_CopyIndex - (int)(next - d);
if (remain > 0) {
memmove(U2_CopyBuff, next, remain);
U2_CopyIndex = remain;
U2_CopyBuff[U2_CopyIndex] = '\0';
U2_CopyFlag = 1; /* 还有消息待处理 */
} else {
U2_CopyIndex = 0;
U2_CopyBuff[0] = 0;
}
} else {
U2_CopyIndex = 0;
U2_CopyBuff[0] = 0;
}
}
/*==== MODBUS CRC16 ====*/
static uint16_t crc16_modbus_update(uint16_t crc, uint8_t data) {
crc ^= data;
for (int i = 0; i < 8; i++) {
if (crc & 1) crc = (crc >> 1) ^ 0xA001;
else crc >>= 1;
}
return crc;
}
/*==== OTA Download via HTTP Range ====*/
static void air780e_trigger_ota_body(const char *host, int port, const char *path, const char *ota_id, uint16_t new_ver, uint16_t crc16);
/* OTA 入口: 停止投喂 + 屏蔽业务网络发布, 然后执行下载。
* 下载成功会在 body 内直接复位; 失败返回后恢复正常业务。 */
void air780e_trigger_ota(const char *host, int port, const char *path, const char *ota_id, uint16_t new_ver, uint16_t crc16) {
log_info("> OTA: in");
feedScaleStop(); /* 称重模式下停止投喂状态机 */
relaySet(1, 0); /* 无秤模式下直接断开投喂继电器 */
g_net_tx_busy = 1; /* 屏蔽业务侧 NET_publish_*, 防止与 AT 应答交织 */
air780e_trigger_ota_body(host, port, path, ota_id, new_ver, crc16);
g_net_tx_busy = 0;
log_warn("> OTA: aborted, resume normal operation");
}
static void air780e_trigger_ota_body(const char *host, int port, const char *path, const char *ota_id, uint16_t new_ver, uint16_t crc16) {
ota_module_reset(); int r; log_info("> OTA: Net...");
for (r=5;r>0;r--){if(air780e_send_at("AT","OK",3000)==0)break;log_info("> AT %d",r);bg_delay(1000);}
if(r==0)return;
bg_delay(500); /* 模块刚回OK, 稍等再发下一条 */
log_info("> OTA: 2nd AT test");
if(air780e_send_at("AT","OK",5000)!=0){log_warn("> 2nd AT fail");return;}
log_info("> OTA: 2nd AT ok");
if(air780e_send_at("ATE0","OK",5000)!=0)return;
for(r=3;r>0;r--){if(air780e_send_at("AT+CPIN?","READY",5000)==0)break;log_info("> SIM %d",r);}
if(r==0)return;
if(air780e_send_at("AT+CSQ","OK",3000)!=0)return;
log_info("> SAPBR...");
if(air780e_send_at("AT+SAPBR=3,1,\"CONTYPE\",\"GPRS\"","OK",3000)!=0){log_info("> S1");return;}
if(air780e_send_at("AT+SAPBR=3,1,\"APN\",\"\"","OK",3000)!=0){log_info("> S2");return;}
if(air780e_send_at("AT+SAPBR=1,1","OK",10000)!=0){log_info("> S3");return;}
if(air780e_send_at("AT+SAPBR=2,1","OK",3000)!=0){log_info("> S4");return;}
/* MQTT connect for OTA progress reporting逐步检查连接失败要能看到 */
{ static char b[120]; sprintf(b,"AT+MCONFIG=\"%s\",\"%s\",\"%s\",0,0,0,0",mqtt_cfg.ClientID,mqtt_cfg.Username,mqtt_cfg.Passward);
if(air780e_send_at(b,"OK",2000)!=0)log_warn("> OTA: MCONFIG fail");
sprintf(b,"AT+MIPSTART=\"%s\",%d",mqtt_cfg.ServerIP,mqtt_cfg.ServerPort);
if(air780e_send_at(b,"OK",10000)!=0)log_warn("> OTA: MIPSTART fail");
if(air780e_send_at("AT+MCONNECT=1,60","OK",10000)!=0)log_warn("> OTA: MCONNECT fail");
log_info("> OTA: MQTT ready"); }
static char buf[250], fu[150]; sprintf(fu,"http://%s:%d%s",host,port,path);
log_info("> HTTP: %s",fu);
if(air780e_send_at("AT+HTTPINIT","OK",5000)!=0){log_info("> INIT");return;}
sprintf(buf,"AT+HTTPPARA=\"URL\",\"%s\"",fu);
if(air780e_send_at(buf,"OK",5000)!=0){log_info("> URL");air780e_send_at("AT+HTTPTERM","OK",2000);return;}
log_info("> HEAD...");
if(air780e_send_at("AT+HTTPACTION=2","+HTTPACTION:",10000)!=0){log_info("> HEAD");air780e_send_at("AT+HTTPTERM","OK",2000);return;}
int file_size=0;
{
memset(U2_CopyBuff,0,U2_COPY_SIZE);U2_CopyIndex=0;U2_CopyFlag=0;
HAL_UART_Transmit(&huart3,(uint8_t*)"AT+HTTPHEAD\r\n",14,500);
int t=2000;while(t>0){bg_delay(50);t-=50;if(strstr((char*)U2_CopyBuff,"OK"))break;}
char *cl=strstr((char*)U2_CopyBuff,"Content-Length:");if(!cl)cl=strstr((char*)U2_CopyBuff,"content-length:");
if(cl){cl=strchr(cl,':');if(cl){cl++;while(*cl==' '||*cl=='\t')cl++;while(*cl>='0'&&*cl<='9'){file_size=file_size*10+(*cl-'0');cl++;}}}
}
log_info("> Size=%dB",file_size);
log_info("**************************************");
log_info("* OTA Update Start *");
log_info("* Size: %d bytes *",file_size);
log_info("**************************************");
log_info("> Erasing W25Q64 new slot...");
W25Q64_EraseRegion(W25Q64_SLOT_NEW_ADDR, W25Q64_SLOT_SIZE);
log_info("> W25Q64 new slot erased");
/* Air780EG 手册HTTPREAD 单段最大 3072 字节 */
#define OTA_DL_CHUNK 3072
int total=0, offset=0, last_pct = -1;
uint16_t crc_acc = 0xFFFF;
air780e_mqtt_report("iot.ota.progress.post", ota_id, 0);
log_info("[Download] Start >>>");
while(1){
log_info("> DL offset=%d", offset);
sprintf(buf,"AT+HTTPPARA=\"BREAK\",%d",offset);
if(air780e_send_at(buf,"OK",3000)!=0){log_info("> BRK fail");break;}
sprintf(buf,"AT+HTTPPARA=\"BREAKEND\",%d",offset+OTA_DL_CHUNK-1);
if(air780e_send_at(buf,"OK",3000)!=0){log_info("> BRKEND fail");break;}
if(air780e_send_at("AT+HTTPACTION=0","+HTTPACTION:",5000)!=0){log_info("> ACT @%d",offset);break;}
int start = U2_CopyIndex;
HAL_UART_Transmit(&huart3, (uint8_t *)"AT+HTTPREAD\r\n", 14, 500);
/* 按 "+HTTPREAD: <len>" 头声明的长度接收:收够 len 字节立即处理,
不再等 500ms 静默尾,每个分块约省 0.5s */
int dlen=0, body=-1;
{ uint32_t t0=HAL_GetTick();
while(HAL_GetTick()-t0 < 10000){
if(body<0){
for(int k=start;k<(int)U2_CopyIndex-11;k++){
if(U2_CopyBuff[k]=='+'&&U2_CopyBuff[k+1]=='H'&&U2_CopyBuff[k+2]=='T'&&U2_CopyBuff[k+3]=='T'&&U2_CopyBuff[k+4]=='P'&&
U2_CopyBuff[k+5]=='R'&&U2_CopyBuff[k+6]=='E'&&U2_CopyBuff[k+7]=='A'&&U2_CopyBuff[k+8]=='D'&&U2_CopyBuff[k+9]==':'){
int j=k+10, d=0;
while(j<(int)U2_CopyIndex&&(U2_CopyBuff[j]==' '||U2_CopyBuff[j]=='\t'))j++;
while(j<(int)U2_CopyIndex&&U2_CopyBuff[j]>='0'&&U2_CopyBuff[j]<='9'){d=d*10+(U2_CopyBuff[j]-'0');j++;}
while(j<(int)U2_CopyIndex&&U2_CopyBuff[j]!='\n')j++;
if(j<(int)U2_CopyIndex){dlen=d;body=j+1;} /* 头部完整才生效,否则继续等 */
break;
}
}
}else if((int)U2_CopyIndex-body>=dlen){
break; /* 数据收齐,立即处理 */
}
bg_delay(20);
}
}
if(dlen<=0||body<0){log_info("> No data @%d",offset);break;}
/* 校验实际接收长度是否足够,防止 DMA 缓冲区溢出导致数据丢失 */
if ((int)U2_CopyIndex - body < dlen) {
log_warn("> DL chunk @%d incomplete: received %d/%d bytes, retry...", offset, U2_CopyIndex - body, dlen);
air780e_send_at("AT+HTTPTERM","OK",3000);
air780e_mqtt_report("iot.ota.result.post", ota_id, 0);
return;
}
W25Q64_WriteBuffer(W25Q64_SLOT_NEW_ADDR + offset, &U2_CopyBuff[body], dlen);
if (!W25Q64_Verify(W25Q64_SLOT_NEW_ADDR + offset, &U2_CopyBuff[body], dlen)) {
log_error("> W25Q64 verify fail at offset=%d, abort OTA", offset);
air780e_send_at("AT+HTTPTERM","OK",3000);
air780e_mqtt_report("iot.ota.result.post", ota_id, 0);
return;
}
for (int i = 0; i < dlen; i++) crc_acc = crc16_modbus_update(crc_acc, U2_CopyBuff[body + i]);
offset+=dlen;total+=dlen;
if(file_size>0){int pct=total*100/file_size;if(pct>100)pct=100;
log_info("> [%3d%%] %d/%dB",pct,total,file_size);
if(pct/10 > last_pct/10){last_pct = pct; air780e_mqtt_report("iot.ota.progress.post", ota_id, pct); LED2_TOGGLE;}
}
else log_info("> +%dB=%dB",dlen,total);
if(dlen<OTA_DL_CHUNK||(file_size>0&&total>=file_size)||offset>=Application_Size)break;
}
air780e_send_at("AT+HTTPTERM","OK",3000);
if(total==0){log_info("> Nothing");air780e_mqtt_report("iot.ota.result.post", ota_id, 0);return;}
if(file_size<=0)file_size=total;
/* 下载完成后用 MODBUS CRC16 校验整个外部固件 */
log_info("> W25Q64 CRC16=0x%04X (server=0x%04X)", crc_acc, crc16);
if (crc16 != 0 && crc_acc != crc16) {
log_error("> W25Q64 CRC16 mismatch! abort OTA");
air780e_mqtt_report("iot.ota.result.post", ota_id, 0);
return;
}
log_info(">>> Verify %dB...", file_size);
if(!Verify_Firmware_External(W25Q64_SLOT_NEW_ADDR, file_size)){
log_info("[FAIL]");air780e_mqtt_report("iot.ota.result.post", ota_id, 0);return;
}
log_info("[ OK ] Firmware OK!");
log_info("**************************************");
log_info("* OTA Download Complete! *");
log_info("* Rebooting to BootLoader... *");
log_info("**************************************");
air780e_mqtt_report("iot.ota.result.post", ota_id, 1);
bg_delay(3000); /* 等待 MQTT 结果消息发送完成后再复位 */
/* 只把固件信息写入 W25Q64版本号在 APP 确认成功后再写入 EEPROM */
W25Q64_OTA_SetNewInfo(new_ver, file_size, crc16);
bg_delay(100); NVIC_SystemReset();
}
/* 固件更新后由 APP 调用,确认运行稳定后写入 EEPROM 版本号 */
void air780e_ota_confirm(void)
{
W25Q64_OtaInfo info;
if (W25Q64_OTA_ReadInfo(&info) != 0) return;
if (info.update_state != W25Q64_STATE_INSTALLED) return;
if (info.app_confirmed) return;
/* 先向服务器上报“确认成功” */
char id_buf[24] = {0};
snprintf(id_buf, sizeof(id_buf), "%u", info.new_version); /* 用版本号作为 id 上报 */
air780e_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 confirm flag written");
else log_error("> OTA confirm write FAILED after 3 tries");
}
/*==== Init ====*/
void air780e_init(void) {
log_info("> 4G: Init...");
memset(&mqtt_cfg,0,sizeof(mqtt_cfg));
strncpy(mqtt_cfg.ClientID,MQTT_CLIENT_ID,sizeof(mqtt_cfg.ClientID)-1);
strncpy(mqtt_cfg.Username,MQTT_USERNAME,sizeof(mqtt_cfg.Username)-1);
strncpy(mqtt_cfg.Passward,MQTT_PASSWORD,sizeof(mqtt_cfg.Passward)-1);
strncpy(mqtt_cfg.ServerIP,MQTT_SERVER_IP,sizeof(mqtt_cfg.ServerIP)-1);
mqtt_cfg.ServerPort=MQTT_SERVER_PORT;
strncpy(mqtt_cfg.Topic,MQTT_TOPIC,sizeof(mqtt_cfg.Topic)-1);
int retry = 3;
while (retry-- > 0) {
cat_reset();
int net_ret = cat_connect_network();
if (net_ret == -2) {
/* 无 SIM 卡,退出让上层切换到 WiFi */
log_warn("> 4G: no SIM, abort 4G init");
return;
}
if (net_ret != 0) {
log_warn("> 4G: network failed, retry %d", retry);
HAL_Delay(5000);
continue;
}
if (cat_connect_mqtt() == 0) {
g_air780e_mqtt_ready = 1;
log_info("> 4G: Ready");
HAL_Delay(50);
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);
CAT1_printf("%s", pub);
HAL_Delay(50);
return;
}
log_warn("> 4G: MQTT failed, retry %d", retry);
air780e_send_at("AT+MDISCONNECT", "OK", 2000);
air780e_send_at("AT+MIPSTOP", "OK", 2000);
HAL_Delay(3000);
}
log_error("> 4G: connect failed, no SIM=%d", g_no_sim_detected);
/* 若已判定无 SIM则不复位让上层切 WiFi否则保持原有复位行为 */
if (!g_no_sim_detected) {
HAL_Delay(100);
NVIC_SystemReset();
}
}
/*==== Process ====*/
void air780e_process(void){
/* 循环处理缓冲区中所有完整 MQTT 消息 */
while(U2_CopyFlag){
U2_CopyFlag = 0;
dispatch_one_mqtt_msg();
}
}