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

997 lines
38 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 "reset_log.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: 发送截断!需 %d 字节", 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: 首次发布成功");
}
}
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;
/*==== GPS 定位运行时开关EEPROM 173空白用编译宏默认====*/
uint8_t g_gps_on = 0;
void gps_load_mode(void)
{
uint8_t v = 0xFF;
eepromReadData(GPS_MODE_ADDR, &v, 1);
if (v > 1) v = PRODUCT_WITH_GPS;
g_gps_on = v;
log_info("> GPS 定位: %s", v ? "开启" : "关闭(GPS0)");
}
void gps_set_mode(uint8_t on)
{
on = on ? 1 : 0;
g_gps_on = on;
eepromWriteData(GPS_MODE_ADDR, &on, 1);
}
/*==== 外部 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) {
g_net_ui_phase = NET_UI_4G_RESET;
log_info("> 4G: 复位...");
/* 关闭 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: 复位完成");
}
/* 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();
app_oled_presched_tick(); /* 阻塞建链期间刷新屏幕联网进度 */
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 超时 [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;
g_net_ui_phase = NET_UI_4G_ATTACH;
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 已附着");
return 0;
}
if (strstr((char *)U2_CopyBuff, "+CGEV: ME PDN ACT 1")) {
log_info("> 4G: PDN 已激活");
return 0;
}
}
}
waited += 30; /* 每次轮询约 3 秒 */
log_info("> 4G: CGATT 未附着,重试 %d/%d", waited / 10, max_wait_s);
}
log_warn("> 4G: CGATT 附着超时");
return -1;
}
/*==== Network ====*/
/* 4G 模块目标波特率:模块出厂默认 115200AT+IPR 配置后掉电保存 */
#define CAT_BAUD_TARGET 921600u
#define CAT_BAUD_DEFAULT 115200u
static char s_iccid[24] = {0}; /* SIM 卡号MQTT OK 后上报平台 */
static int cat_connect_network(void) {
log_info("> 4G: 网络...");
g_net_ui_phase = NET_UI_4G_AT;
/* 波特率同步:永远先按 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: 波特率 -> 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;
g_net_ui_phase = NET_UI_4G_SIM; /* AT 通过,开始检测 SIM/信号 */
/* 检测 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: 未检测到 SIM 卡");
g_net_ui_phase = NET_UI_4G_NOSIM;
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 未就绪");
return -1;
}
/* 读取 SIM 卡号 ICCID物联网卡套餐管理平台可查卡号缴费 */
{
s_iccid[0] = 0;
if (catSendCmd("AT+ICCID", "+ICCID:", 1, 10) == 0) {
char *p = strstr((char *)U2_CopyBuff, "+ICCID:");
if (p) {
p += 7;
while (*p == ' ') p++;
int n = 0;
while (n < 20 && ((p[n] >= '0' && p[n] <= '9') ||
(p[n] >= 'A' && p[n] <= 'Z') || (p[n] >= 'a' && p[n] <= 'z'))) n++; /* 数字+大小写字母都保留,最多 20 位 */
memcpy(s_iccid, p, n);
s_iccid[n] = 0;
}
}
if (s_iccid[0]) log_info("> 4G: ICCID %s", s_iccid);
else log_warn("> 4G: ICCID 读取失败");
}
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: 信号 %d%% (rssi=%d)", pct, rssi);
if (rssi == 99 || rssi < 2) {
log_error("> 4G: 无信号 (%d%%)", pct);
g_no_signal_detected = 1;
return -3;
}
if (pct < 20) {
log_warn("> 4G: 信号弱 (%d%%)", pct);
}
} else {
log_warn("> 4G: CSQ 查询失败,继续执行");
}
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 (g_gps_on) {
if (catSendCmd("AT+CGNSPWR=1", "OK", 1, 30) != 0) {
log_warn("> 4G: GNSS 上电失败(模块无 GNSS?)");
} else if (catSendCmd("AT+CGNSAID=31,1,1,1", "OK", 1, 30) != 0) {
log_warn("> 4G: GNSS 辅助失败");
} else {
s_gnss_present = 1;
log_info("> 4G: GNSS 已开启");
}
}
log_info("> 4G: 网络 OK"); return 0;
}
/*==== MQTT Connect ====*/
static int cat_connect_mqtt(void) {
g_net_ui_phase = NET_UI_4G_MQTT;
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 连接超时,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 连接超时,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 已接受(OK)");
} else {
log_warn("> 4G: MSUB 失败,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 版本已保存并校验: %u", ver);
return 0;
}
log_warn("> OTA 版本校验不匹配: 写入=%u, 读取=%u", ver, tmp.Ver);
}
HAL_Delay(20);
}
log_warn("> OTA 版本保存失败: %u", ver);
return -1;
}
/*==== MQTT Publish: OTA进度/结果上报 ====*/
/* 结果上报的"发了就走"版本:不等 OK 不阻塞。用于下载完成后的 result.post(success)
* 平台以重启后的 confirm.post等 OK+30s 重试)为权威成功信号 */
void air780e_mqtt_result_nowait(const char *header, const char *ota_id, int val) {
if (!header || !ota_id || !ota_id[0]) return;
static char cmd[300];
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");
CAT1_printf("%s", cmd);
log_info("> OTA 结果已发送(不等OK): success=%d", val);
}
int air780e_mqtt_report(const char *header, const char *ota_id, int val) {
if (!header || !ota_id || !ota_id[0]) return -1;
static char cmd[300];
if (strstr(header, "progress")) {
/* 进度上报:等 OK 确认送达(失败自动重试 1 次),让平台"升级中"可靠置位。
* 代价是每 10% 最多多花 ~3s换来进度可见 */
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);
if (catSendCmd(cmd, "OK", 2, 15) != 0) {
log_warn("> OTA 进度上报无应答: %d%%", val);
}
return 0;
} 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 结果上报失败(重试 3 次无 OK)");
return -1;
}
log_info("> OTA 结果已上报: success=%d", val);
return 0;
}
}
/*==== 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 异常: 无服务,复位");
reset_log_mark(RSN_NO_SERVICE);
HAL_Delay(1000);
NVIC_SystemReset();
}
if (strstr(d, "CLOSED")) {
if (next) *next = saved;
log_error("> 4G 异常: MQTT/TCP 已断开,复位");
reset_log_mark(RSN_KICKED);
HAL_Delay(1000);
NVIC_SystemReset();
}
if (strstr(d, "boot.rom")) {
if (next) *next = saved;
log_error("> 4G 异常: 模块重启(boot.rom),复位");
reset_log_mark(RSN_4G_MODULE);
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: 进入");
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;
/* body 任何失败路径返回后必须复位OTA 入口已复位过 4G 模组,
* MQTT 实际已断开而 ready 标志仍是 1"假在线"),不复位将无法控制也无法再次 OTA */
log_warn("> OTA: 失败中止,复位恢复联网");
reset_log_mark(RSN_OTA_FAIL);
HAL_Delay(100);
NVIC_SystemReset();
}
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: 网络...");
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: 第二次 AT 测试");
if(air780e_send_at("AT","OK",5000)!=0){log_warn("> 第二次 AT 失败");return;}
log_info("> OTA: 第二次 AT 成功");
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;
/* 模组 OTA 入口刚复位过,必须先等网络重新附着(CATT=1)再激活承载,
* 否则注册慢(弱信号/物联卡)时 SAPBR=1,1 必失败(S3) */
log_info("> OTA: 等待网络附着...");
if (cat_wait_cgatt_attached(30) != 0) { log_warn("> OTA: 附着超时"); 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 失败");
sprintf(b,"AT+MIPSTART=\"%s\",%d",mqtt_cfg.ServerIP,mqtt_cfg.ServerPort);
if(air780e_send_at(b,"OK",10000)!=0)log_warn("> OTA: MIPSTART 失败");
if(air780e_send_at("AT+MCONNECT=1,60","OK",10000)!=0)log_warn("> OTA: MCONNECT 失败");
log_info("> OTA: MQTT 就绪"); }
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("> 大小=%dB",file_size);
log_info("**************************************");
log_info("* OTA 升级开始 *");
log_info("* 大小: %d 字节 *",file_size);
log_info("**************************************");
log_info("> 正在擦除 W25Q64 新槽位...");
W25Q64_EraseRegion(W25Q64_SLOT_NEW_ADDR, W25Q64_SLOT_SIZE);
log_info("> W25Q64 新槽位擦除完成");
/* 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("[下载] 开始 >>>");
while(1){
log_info("> DL 偏移=%d", offset);
sprintf(buf,"AT+HTTPPARA=\"BREAK\",%d",offset);
if(air780e_send_at(buf,"OK",3000)!=0){log_info("> BRK 失败");break;}
sprintf(buf,"AT+HTTPPARA=\"BREAKEND\",%d",offset+OTA_DL_CHUNK-1);
if(air780e_send_at(buf,"OK",3000)!=0){log_info("> BRKEND 失败");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("> 无数据 @%d",offset);break;}
/* 校验实际接收长度是否足够,防止 DMA 缓冲区溢出导致数据丢失 */
if ((int)U2_CopyIndex - body < dlen) {
log_warn("> DL 数据块 @%d 不完整: 已收 %d/%d 字节,重试...", 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 校验失败 offset=%d,中止 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("> 无数据");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 (服务器=0x%04X)", crc_acc, crc16);
if (crc16 != 0 && crc_acc != crc16) {
log_error("> W25Q64 CRC16 不匹配!中止 OTA");
air780e_mqtt_report("iot.ota.result.post", ota_id, 0);
return;
}
log_info(">>> 校验 %dB...", file_size);
if(!Verify_Firmware_External(W25Q64_SLOT_NEW_ADDR, file_size)){
log_info("[失败]");air780e_mqtt_report("iot.ota.result.post", ota_id, 0);return;
}
log_info("[ OK ] 固件校验通过!");
log_info("**************************************");
log_info("* OTA 下载完成! *");
log_info("* 正在重启进入 BootLoader... *");
log_info("**************************************");
air780e_mqtt_result_nowait("iot.ota.result.post", ota_id, 1);
bg_delay(1000); /* 给模组一点发送时间即复位(权威通知由重启后的 confirm.post 承担) */
/* 只把固件信息写入 W25Q64版本号在 APP 确认成功后再写入 EEPROM */
W25Q64_OTA_SetNewInfo(new_ver, file_size, crc16);
reset_log_mark(RSN_OTA_UPDATE);
bg_delay(100); NVIC_SystemReset();
}
/* 固件更新后由 APP 调用,确认运行稳定后写入 EEPROM 版本号 */
/* 返回 0=确认通知已送达平台1=无挂起 OTA-1=本地已确认但通知未送达(上层应稍后重试) */
int air780e_ota_confirm(void)
{
W25Q64_OtaInfo info;
if (W25Q64_OTA_ReadInfo(&info) != 0) return 1;
if (info.update_state != W25Q64_STATE_INSTALLED) return 1;
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 次)");
}
/* 重启后的新鲜可靠连接上,先补发 result.post带平台任务 id驱动"完成"标志)。
* 任务 id 存在 EEPROM(175~194)(下载开始时由网络层写入),发送成功后清回 0xFF */
int rc = 0;
{
char oid[20];
eepromReadData(175, oid, sizeof(oid));
if ((uint8_t)oid[0] != 0xFF && oid[0] != 0) {
oid[19] = 0;
rc |= NET_mqtt_report("iot.ota.result.post", oid, 1);
if (rc == 0) {
memset(oid, 0xFF, sizeof(oid));
eepromWriteData(175, oid, sizeof(oid)); /* 已送达,清除防止重复 */
}
}
}
/* 再向服务器上报"确认成功"走网络层路由4G/WiFi 通道均可) */
char id_buf[24] = {0};
snprintf(id_buf, sizeof(id_buf), "%u", info.new_version); /* 用版本号作为 id 上报 */
rc |= NET_mqtt_report("iot.ota.confirm.post", id_buf, 1);
return rc;
}
/*==== Init ====*/
void air780e_init(void) {
log_info("> 4G: 初始化...");
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: 无 SIM,中止 4G 初始化");
return;
}
if (net_ret == -3) {
/* 无信号,退出让上层重试/切换 WiFi */
log_warn("> 4G: 无信号,中止 4G 初始化");
return;
}
if (net_ret != 0) {
log_warn("> 4G: 网络失败,重试 %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(300); /* 开机上报拉开间隔,避免模组 MQTT 发送排队撞车 */
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);
CAT1_printf("%s", pub);
HAL_Delay(300);
}
return;
}
log_warn("> 4G: MQTT 失败,重试 %d", retry);
air780e_send_at("AT+MDISCONNECT", "OK", 2000);
air780e_send_at("AT+MIPSTOP", "OK", 2000);
HAL_Delay(3000);
}
log_error("> 4G: 连接失败,无 SIM=%d, 无信号=%d", g_no_sim_detected, g_no_signal_detected);
/* 若已判定无 SIM 或无信号,则不复位,让上层重试/切 WiFi否则保持原有复位行为 */
if (!g_no_sim_detected && !g_no_signal_detected) {
reset_log_mark(RSN_4G_FAIL);
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_gps_on) { s_gps_tick = 0; s_gps_first = 1; return; } /* GPS0 关闭定位 */
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 超时"); 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: 未定位(run=%d fix=%d),跳过", 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();
}