shuichan_old/_air780e_baseline.c

894 lines
35 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 "FreeRTOS.h"
#include "task.h"
#include "stdio.h"
#include "string.h"
#include <stdarg.h>
#include <stdlib.h>
#include <math.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;
/* GNSS 能力标志cat_connect_network 里 AT+CGNSPWR 成功才置 1Air780EG 才有) */
static uint8_t s_gnss_present = 0;
/*==== 外部 Flash 校验:读取固件头 8 字节判断合法性 ====
* 非 staticWiFi OTA(net_wifi_ota.c) 复用 */
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: 带重试, 参考代码风格 ====*/
/* 切片延时RTOS 版):
* 调度器运行中 → vTaskDelay 让出 CPU其他任务秤/LED/喂狗)照常运行;
* 调度器未启动(开机初始化阶段)→ 退化为 HAL_Delay + 喂狗。
* 非 staticWiFi 后端(esp8266.c)的 AT 等待同样复用该机制 */
void bg_delay(uint32_t ms)
{
if (xTaskGetSchedulerState() == taskSCHEDULER_RUNNING) {
while (ms >= 20) {
vTaskDelay(20);
ms -= 20;
}
if (ms) vTaskDelay(ms);
} else {
while (ms >= 20) {
HAL_Delay(20);
IWDG_Feed();
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;
/* 信号质量检测AT+CSQ → +CSQ: rssi,berrssi 0~3199=未知/无信号)。
* 转成百分比输出一次;无信号时置标志返回 -3让上层重试/回退 WiFi
* 信号偏弱(<20%)只告警不阻断 */
g_no_signal_detected = 0;
if (catSendCmd("AT+CSQ", "+CSQ:", 1, 15) == 0) {
char *p = strstr((char *)U2_CopyBuff, "+CSQ: ");
int rssi = p ? atoi(p + 6) : 99;
int pct = (rssi >= 0 && rssi <= 31) ? (rssi * 100 / 31) : 0;
log_info("> 4G: signal %d%% (rssi=%d)", pct, rssi);
if (rssi == 99 || rssi < 2) {
log_error("> 4G: no signal (%d%%)", pct);
g_no_signal_detected = 1;
return -3;
}
if (pct < 20) {
log_warn("> 4G: signal weak (%d%%)", pct);
}
} else {
log_warn("> 4G: CSQ query failed, continue anyway");
}
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;
/* GNSS 上电 + 辅助定位Air780EG 内置北斗/GPS需 PDP 激活后才能开辅助定位)。
* 非 GNSS 版本模组(纯 Air780E会回 ERROR仅告警不阻断联网
* 同时置 s_gnss_present=0 关闭后续定位轮询 */
s_gnss_present = 0;
if (catSendCmd("AT+CGNSPWR=1", "OK", 1, 30) != 0) {
log_warn("> 4G: GNSS power on failed (module without GNSS?)");
} else if (catSendCmd("AT+CGNSAID=31,1,1,1", "OK", 1, 30) != 0) {
log_warn("> 4G: GNSS aiding failed");
} else {
s_gnss_present = 1;
log_info("> 4G: GNSS on");
}
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();
}
/* 业务分发id去重/继电器/OTA已上提到网络层与模组无关 */
net_dispatch_message(d);
/* 恢复截断 */
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 ====
* 非 staticWiFi OTA(net_wifi_ota.c) 复用 */
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;
/* 先向服务器上报“确认成功”走网络层路由4G/WiFi 通道均可) */
char id_buf[24] = {0};
snprintf(id_buf, sizeof(id_buf), "%u", info.new_version); /* 用版本号作为 id 上报 */
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 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 == -3) {
/* 无信号,退出让上层重试/切换 WiFi */
log_warn("> 4G: no signal, 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, no signal=%d", g_no_sim_detected, g_no_signal_detected);
/* 若已判定无 SIM 或无信号,则不复位,让上层重试/切 WiFi否则保持原有复位行为 */
if (!g_no_sim_detected && !g_no_signal_detected) {
HAL_Delay(100);
NVIC_SystemReset();
}
}
/*==== GNSS (北斗/GPS) 定位周期上报:仅 4G 通道(Air780EG 内置GNSS) ====
* 参考工程《air780EG-V1.1加入北斗GPS数据》的实现
* AT+CGNSINF → +CGNSINF: run,fix,UTC,lat,lon,...(经纬度为十进制度字符串)
* 修正了参考工程的几个缺陷定位无效不上报空串、OTA 期间不插队 AT、
* 有平台下行命令插队时主动让行、手工解析不依赖 sscanf 的 scanset。 */
#define GPS_FIRST_DELAY_MS 60000u /* 联网后首次查询延迟,给搜星留时间 */
#define GPS_REPORT_INTERVAL_MS 300000u /* 定位上报周期 5min */
/*==== WGS-84 → GCJ-02高德/火星坐标系)转换 ====
* GPS 模块输出 WGS-84 原始坐标,直接输入国内高德地图会有 100~600m 加密偏移,
* 上报前先转换成 GCJ-02。公开的标准算法中国境外直接返回原值。 */
#define GCJ_PI 3.1415926535897932384626
#define GCJ_A 6378245.0 /* 克拉索夫斯基椭球长半轴 */
#define GCJ_EE 0.00669342162296594323 /* 偏心率平方 */
static double gcj_transform_lat(double x, double y)
{
double ret = -100.0 + 2.0 * x + 3.0 * y + 0.2 * y * y + 0.1 * x * y + 0.2 * sqrt(fabs(x));
ret += (20.0 * sin(6.0 * x * GCJ_PI) + 20.0 * sin(2.0 * x * GCJ_PI)) * 2.0 / 3.0;
ret += (20.0 * sin(y * GCJ_PI) + 40.0 * sin(y / 3.0 * GCJ_PI)) * 2.0 / 3.0;
ret += (160.0 * sin(y / 12.0 * GCJ_PI) + 320.0 * sin(y * GCJ_PI / 30.0)) * 2.0 / 3.0;
return ret;
}
static double gcj_transform_lon(double x, double y)
{
double ret = 300.0 + x + 2.0 * y + 0.1 * x * x + 0.1 * x * y + 0.1 * sqrt(fabs(x));
ret += (20.0 * sin(6.0 * x * GCJ_PI) + 20.0 * sin(2.0 * x * GCJ_PI)) * 2.0 / 3.0;
ret += (20.0 * sin(x * GCJ_PI) + 40.0 * sin(x / 3.0 * GCJ_PI)) * 2.0 / 3.0;
ret += (150.0 * sin(x / 12.0 * GCJ_PI) + 300.0 * sin(x / 30.0 * GCJ_PI)) * 2.0 / 3.0;
return ret;
}
static void wgs84_to_gcj02(double wgs_lon, double wgs_lat, double *gcj_lon, double *gcj_lat)
{
/* 中国境外不做偏移 */
if (wgs_lon < 72.004 || wgs_lon > 137.8347 || wgs_lat < 0.8293 || wgs_lat > 55.8271) {
*gcj_lon = wgs_lon;
*gcj_lat = wgs_lat;
return;
}
double dlat = gcj_transform_lat(wgs_lon - 105.0, wgs_lat - 35.0);
double dlon = gcj_transform_lon(wgs_lon - 105.0, wgs_lat - 35.0);
double radlat = wgs_lat / 180.0 * GCJ_PI;
double magic = sin(radlat);
magic = 1 - GCJ_EE * magic * magic;
double sqrtmagic = sqrt(magic);
dlat = (dlat * 180.0) / ((GCJ_A * (1 - GCJ_EE)) / (magic * sqrtmagic) * GCJ_PI);
dlon = (dlon * 180.0) / (GCJ_A / sqrtmagic * cos(radlat) * GCJ_PI);
*gcj_lat = wgs_lat + dlat;
*gcj_lon = wgs_lon + dlon;
}
static uint32_t s_gps_tick = 0;
static uint8_t s_gps_first = 1;
static void air780e_gps_poll(void)
{
if (!g_air780e_mqtt_ready) { s_gps_tick = 0; s_gps_first = 1; return; }
if (!s_gnss_present) return;
if (g_net_tx_busy) return; /* OTA 下载期间禁止插队 AT */
uint32_t now = HAL_GetTick();
if (s_gps_tick == 0) { s_gps_tick = now; return; }
if (now - s_gps_tick < (s_gps_first ? GPS_FIRST_DELAY_MS : GPS_REPORT_INTERVAL_MS)) return;
s_gps_tick = now;
s_gps_first = 0;
/* 清缓冲,发 AT+CGNSINF 查询定位 */
U2_CopyIndex = 0; U2_CopyBuff[0] = 0; U2_CopyFlag = 0;
CAT1_printf("AT+CGNSINF");
int t = 20; /* 最多等 2s */
while (--t) {
bg_delay(100);
if (U2_CopyFlag) {
/* 有平台下行命令插队:本轮放弃查询,先让 dispatch 处理命令 */
if (strstr((char *)U2_CopyBuff, "+MSUB:")) return;
if (strstr((char *)U2_CopyBuff, "+CGNSINF:")) break;
}
}
if (t == 0) { log_warn("> GPS: CGNSINF timeout"); return; }
/* 手工解析逗号字段:+CGNSINF: run,fix,UTC,lat,lon,... */
char *q = strstr((char *)U2_CopyBuff, "+CGNSINF:") + 10;
int run = atoi(q);
char *c1 = strchr(q, ','); /* run 后的逗号 */
char *c2 = c1 ? strchr(c1 + 1, ',') : NULL; /* fix 后的逗号 */
char *c3 = c2 ? strchr(c2 + 1, ',') : NULL; /* UTC 后的逗号 → lat 起点 */
char *c4 = c3 ? strchr(c3 + 1, ',') : NULL; /* lat 后的逗号 → lon 起点 */
char *c5 = c4 ? strchr(c4 + 1, ',') : NULL; /* lon 后的逗号 */
int fix = c1 ? atoi(c1 + 1) : 0;
char lat[12] = {0}, lon[13] = {0};
if (c3 && c4 && c5) {
int ll = (int)(c4 - (c3 + 1));
int ol = (int)(c5 - (c4 + 1));
if (ll > 0 && ll <= 11) { memcpy(lat, c3 + 1, ll); }
if (ol > 0 && ol <= 12) { memcpy(lon, c4 + 1, ol); }
}
U2_CopyIndex = 0; U2_CopyBuff[0] = 0; U2_CopyFlag = 0;
/* 定位无效(fix!=1)或字段缺失:跳过本次上报(室内搜不到星属正常) */
if (fix != 1 || lat[0] == 0 || lon[0] == 0) {
log_info("> GPS: no fix (run=%d fix=%d), skip", run, fix);
return;
}
/* WGS-84 → GCJ-02高德坐标系上报的坐标可直接用于高德地图 */
{
double glon, glat;
wgs84_to_gcj02(atof(lon), atof(lat), &glon, &glat);
snprintf(lat, sizeof(lat), "%.6f", glat);
snprintf(lon, sizeof(lon), "%.6f", glon);
}
log_info("> GPS: %s,%s", lon, lat); /* 高德/GeoJSON 习惯:经度在前 */
CAT1_printf("AT+MPUB=\"/iot/data/up/%s\",0,0,\"{\\22header\\22:\\22iot.prop.post\\22,\\22body\\22:{\\22GPS\\22:\\22%s,%s\\22}}\"",
MqttInfoStr.ClientID, lon, lat);
}
/*==== Process ====*/
void air780e_process(void){
/* 循环处理缓冲区中所有完整 MQTT 消息 */
while(U2_CopyFlag){
U2_CopyFlag = 0;
dispatch_one_mqtt_msg();
}
air780e_gps_poll();
}