978 lines
37 KiB
C
978 lines
37 KiB
C
#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 成功才置 1(Air780EG 才有) */
|
||
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 字节判断合法性 ====
|
||
* 非 static:WiFi 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 + 喂狗。
|
||
* 非 static:WiFi 后端(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 模块目标波特率:模块出厂默认 115200,AT+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,ber(rssi 0~31,99=未知/无信号)。
|
||
* 转成百分比输出一次;无信号时置标志返回 -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")) {
|
||
/* 进度上报:允许丢包,发了就走 */
|
||
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);
|
||
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 ====
|
||
* 非 static:WiFi 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 次)");
|
||
}
|
||
|
||
/* 再向服务器上报"确认成功"(走网络层路由,4G/WiFi 通道均可) */
|
||
char id_buf[24] = {0};
|
||
snprintf(id_buf, sizeof(id_buf), "%u", info.new_version); /* 用版本号作为 id 上报 */
|
||
return NET_mqtt_report("iot.ota.confirm.post", id_buf, 1);
|
||
}
|
||
|
||
|
||
/*==== 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();
|
||
}
|
||
|