shuichan_new/STM32F103_App1/HardWare/NETWORK/network.c

568 lines
22 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.

/* network.c - 网络层4G/WiFi 双通道抽象与故障切换
*
* 业务层(app_loop/relay/feed_scale)只调 NET_* 接口,不感知底层模组。
* 通道切换策略(优先顺序由 EEPROM 记忆的"上次成功联网方式"决定):
* 上次 4G 成功4G无卡/无信号每 10s 重试3 次失败)→ WiFi → 回 4G…循环
* 上次 WiFi 成功WiFi 建链一轮失败 → 4G同上重试→ 回 WiFi…循环
* WiFi 建链子顺序:上次是配网热点则先查已存 AP 自动连接CIPSTATUS, 10s
* → 固定 SSID60s→ SmartConfig60s
* 任何方式联网成功后调 NET_SaveMode(),仅方式变化时写一次 EEPROM。
*/
#include "network.h"
#include "net_wifi.h"
#include "mqtt_client.h"
#include "hlw8032.h"
#include "feed_scale.h"
#include "net_wifi_ota.h"
#include "air780e.h"
#include "led.h"
#include "log.h"
#include "main.h"
#include "uart.h"
#include "relay.h"
#include "ext_relay.h"
#include "proto.h"
#include "24c02.h"
#include "FreeRTOS.h"
#include "task.h"
#include "semphr.h"
#include "stdio.h"
#include "string.h"
volatile uint8_t g_net_mqtt_ready = 0;
volatile uint8_t g_net_first_publish_ok = 0;
/* 网络发送互斥标志OTA 下载等 AT 交互期间置位 */
volatile uint8_t g_net_tx_busy = 0;
/* 4G 无 SIM 卡标志:由 air780e.c 在初始化失败时设置 */
volatile uint8_t g_no_sim_detected = 0;
/* 4G 无信号标志:由 air780e.c 在 AT+CSQ 检测时设置 */
volatile uint8_t g_no_signal_detected = 0;
/* 当前激活的网络后端 */
/* WiFi 模块使能脚PA6高电平工作4G 运行时拉低断电省电 */
#define ESP_EN_PORT GPIOA
#define ESP_EN_PIN GPIO_PIN_6
static void esp_power(uint8_t on)
{
HAL_GPIO_WritePin(ESP_EN_PORT, ESP_EN_PIN, on ? GPIO_PIN_SET : GPIO_PIN_RESET);
}
/* 供 WiFi 驱动做模块整体断电重启esp_reset 用) */
void NET_esp_power(uint8_t on)
{
esp_power(on);
}
volatile net_ui_phase_t g_net_ui_phase = NET_UI_NONE;
volatile int g_ota_pct = 0;
volatile uint8_t g_ota_active = 0;
volatile int g_net_ui_countdown = 0;
#define OTA_ID_EEP_ADDR 175 /* EEPROM 175~194OTA 平台任务 id确认后清 0xFF */
static net_backend_t s_backend = NET_BACKEND_4G;
/* 上次成功联网方式EEPROM 偏移 130秤参数占用到 126避开 */
static uint8_t s_last_mode = NET_MODE_4G;
/* 无 SIM 重试多少次后切 WiFi */
#define NET_NO_SIM_SWITCH_COUNT 3
/* WiFi 一轮完整建链(已存AP→固定SSID→SmartConfig)失败后切回 4G */
#define NET_WIFI_FAIL_SWITCH_COUNT 1
/*==== 网络模组 AT 会话互斥RTOS 版)====
* net 任务在做长 AT 会话(建链/重连/OTA期间持有锁
* 其他任务的 NET_publish_* 采用尝试锁,拿不到就丢弃本次发布,
* 避免业务发布与进行中的 AT 应答在串口上交织导致误判。
* 递归锁net 任务自己调用的发布(如 net_wifi_init 里的电量上报)可重入。 */
static SemaphoreHandle_t s_net_at_mutex = NULL;
void NET_LockInit(void)
{
s_net_at_mutex = xSemaphoreCreateRecursiveMutex();
}
static void net_at_lock(void)
{
if (s_net_at_mutex && xTaskGetSchedulerState() == taskSCHEDULER_RUNNING) {
xSemaphoreTakeRecursive(s_net_at_mutex, portMAX_DELAY);
}
}
static void net_at_unlock(void)
{
if (s_net_at_mutex && xTaskGetSchedulerState() == taskSCHEDULER_RUNNING) {
xSemaphoreGiveRecursive(s_net_at_mutex);
}
}
/* 尝试拿锁0=拿到(含调度器未启动的直通),-1=被 AT 会话占用。
* 带 2s 超时而非立即放弃net 任务每轮 process 会短暂持锁消费接收缓冲,
* 业务发布稍微等一下即可,避免因互斥频繁丢包 */
static int net_at_trylock(void)
{
if (s_net_at_mutex && xTaskGetSchedulerState() == taskSCHEDULER_RUNNING) {
return xSemaphoreTakeRecursive(s_net_at_mutex, pdMS_TO_TICKS(2000)) == pdTRUE ? 0 : -1;
}
return 0;
}
net_backend_t NET_GetActiveBackend(void)
{
return s_backend;
}
uint8_t NET_GetLastMode(void)
{
return s_last_mode;
}
/* 联网方式记忆:仅方式变化时写一次 EEPROM"只存一次" */
/* 开机按键选定的联网方式(默认 4G); 选定后锁定通道, 连不上原地重试不切换 */
static uint8_t s_force_mode = NET_MODE_4G;
void NET_SetForceMode(uint8_t mode)
{
if (mode > NET_MODE_WIFI_SMART) mode = NET_MODE_4G;
s_force_mode = mode;
}
uint8_t NET_GetForceMode(void) { return s_force_mode; }
void NET_SaveMode(uint8_t mode)
{
if (mode == s_last_mode || mode > NET_MODE_WIFI_SMART) return;
s_last_mode = mode;
eepromWriteData(EEPROM_NETMODE_ADDR, &mode, 1);
log_info("> NET: 联网方式已保存 -> %s",
mode == NET_MODE_4G ? "4G" :
mode == NET_MODE_WIFI_FIXED ? "WiFi(固定热点)" : "WiFi(配网热点)");
}
void NET_init(void)
{
g_net_mqtt_ready = 0;
g_net_first_publish_ok = 0;
g_no_sim_detected = 0;
g_no_signal_detected = 0;
/* 联网方式 = 开机按键选定(NET_SetForceMode), 不再读 EEPROM 上次成功方式 */
uint8_t m = s_force_mode;
s_last_mode = m;
if (m == NET_MODE_4G) {
s_backend = NET_BACKEND_4G;
esp_power(0); /* 4G 优先:关闭 WiFi 模块省电 */
net_at_lock();
air780e_init();
net_at_unlock();
if (g_no_sim_detected) {
log_warn("> 4G 无 SIM 卡,请插入 SIM 卡");
/* 不立即切 WiFi由 NET_process 重试几次仍无卡后再切换 */
}
g_net_mqtt_ready = g_air780e_mqtt_ready;
} else {
/* 上次 WiFi 成功WiFi 优先NET_process 会立即发起 net_wifi_init */
log_info("> NET: 上次联网方式为 WiFiWiFi 优先");
esp_power(1); /* WiFi 优先:使能 WiFi 模块 */
s_backend = NET_BACKEND_WIFI;
}
}
/* 切到 WiFi 通道:停 4G 接收共享缓冲互斥WiFi 建链由 NET_process 立即发起 */
static void net_switch_to_wifi(void)
{
log_warn("> NET: 切换 4G -> WiFi");
esp_power(1); /* 使能 WiFi 模块 */
USART3_StopRx();
g_net_mqtt_ready = 0;
g_net_first_publish_ok = 0;
s_backend = NET_BACKEND_WIFI;
}
/* 切回 4G 通道:停 WiFi 接收,恢复 4G 接收并重新初始化 */
static void net_switch_to_4g(void)
{
log_warn("> NET: 切换 WiFi -> 4G");
UART4_StopRx();
esp_power(0); /* 关闭 WiFi 模块省电 */
g_net_mqtt_ready = 0;
g_net_first_publish_ok = 0;
g_no_sim_detected = 0;
g_no_signal_detected = 0;
g_air780e_mqtt_ready = 0;
g_air780e_first_publish_ok = 0;
s_backend = NET_BACKEND_4G;
USART3_StartRx();
net_at_lock();
air780e_init();
net_at_unlock();
}
void NET_process(void)
{
static uint32_t retry_tick = 0;
static uint16_t fail_count = 0;
if (s_backend == NET_BACKEND_4G) {
/* 网络未就绪时自动重试 */
if (!g_air780e_mqtt_ready) {
if (g_no_sim_detected || g_no_signal_detected) {
/* 无 SIM 卡或无信号:每 10s 重试一次,连续多次仍不可用则切换到 WiFi 通道 */
if (retry_tick == 0 ||
(HAL_GetTick() - retry_tick) >= 10000) {
retry_tick = HAL_GetTick();
fail_count++;
log_warn("> NET: 4G 不可用 (no_sim=%d, no_signal=%d),重试初始化 %d/%d",
g_no_sim_detected, g_no_signal_detected,
fail_count, NET_NO_SIM_SWITCH_COUNT);
g_no_sim_detected = 0;
g_no_signal_detected = 0;
net_at_lock();
air780e_init();
net_at_unlock();
if (!g_no_sim_detected && !g_no_signal_detected) {
log_info("> NET: 4G SIM卡/信号正常或网络已恢复");
fail_count = 0;
} else if (fail_count >= NET_NO_SIM_SWITCH_COUNT && s_force_mode != NET_MODE_4G) {
log_warn("> NET: 4G 重试 %d 次仍不可用,回退到 WiFi",
NET_NO_SIM_SWITCH_COUNT);
net_switch_to_wifi();
fail_count = 0;
retry_tick = 0;
}
}
} else {
retry_tick = 0;
fail_count = 0;
}
} else {
retry_tick = 0;
fail_count = 0;
NET_SaveMode(NET_MODE_4G); /* 4G 联网成功:更新方式记忆(未变化则空操作) */
}
net_at_lock();
air780e_process();
net_at_unlock();
g_net_mqtt_ready = g_air780e_mqtt_ready;
g_net_first_publish_ok = g_air780e_first_publish_ok;
} else {
/* WiFi 通道:未就绪时重新建链(首轮立即),一轮失败即切回 4G */
if (!g_wifi_mqtt_ready) {
if (retry_tick == 0 ||
(HAL_GetTick() - retry_tick) >= 10000) {
retry_tick = HAL_GetTick();
fail_count++;
log_warn("> NET: WiFi 连接尝试 %d/%d",
fail_count, NET_WIFI_FAIL_SWITCH_COUNT);
net_at_lock();
int wifi_rc = net_wifi_init();
net_at_unlock();
if (wifi_rc == 0) {
fail_count = 0;
} else if (fail_count >= NET_WIFI_FAIL_SWITCH_COUNT && s_force_mode == NET_MODE_4G) {
log_warn("> NET: WiFi 连接失败,回退到 4G");
net_switch_to_4g();
fail_count = 0;
retry_tick = 0;
}
}
} else {
retry_tick = 0;
fail_count = 0;
}
net_at_lock();
net_wifi_process();
net_at_unlock();
g_net_mqtt_ready = g_wifi_mqtt_ready;
g_net_first_publish_ok = g_wifi_first_publish_ok;
}
}
int NET_publish_status(int sw1, int feeding, int sw2, int sw3, int sw4,
float weight_kg, float once_wt_kg, float zero_kg)
{
if (g_net_tx_busy) { log_info("> NET: 状态上报被丢弃 (发送忙)"); return -1; }
if (net_at_trylock() != 0) { log_info("> NET: 状态上报被丢弃 (AT 忙)"); return -1; }
/* 485 扩展继电器板在线时附加 sw5~sw8(平台物模型需加对应字段) */
char ext_plain[80] = "";
char ext_esc[112] = "";
if (ext_relay_online()) {
uint8_t m = ext_relay_state_mask();
snprintf(ext_plain, sizeof(ext_plain),
",\"sw5\":%d,\"sw6\":%d,\"sw7\":%d,\"sw8\":%d",
(m>>0)&1, (m>>1)&1, (m>>2)&1, (m>>3)&1);
snprintf(ext_esc, sizeof(ext_esc),
",\\22sw5\\22:%d,\\22sw6\\22:%d,\\22sw7\\22:%d,\\22sw8\\22:%d",
(m>>0)&1, (m>>1)&1, (m>>2)&1, (m>>3)&1);
}
/* ver 字段:固件版本号,平台据此可识别设备是否发生了 OTA 回退 */
/* sw1 与 feeding 都映射继电器 1 状态(无秤模式两者等效),
* 两个字段都上报,平台的 sw1 开关和 feeding 状态才能同步刷新 */
/* 无秤模式PRODUCT_WITH_FEED_SCALE=0不上报 zero/onceWt 字段 */
if (s_backend == NET_BACKEND_4G) {
if (g_feed_scale_on) {
CAT1_printf("AT+MPUB=\"/iot/data/up/%s\",0,0,\"{\\22header\\22:\\22iot.prop.post\\22,\\22body\\22:{\\22ver\\22:%u,\\22weight\\22:%.1f,\\22onceWt\\22:%.1f,\\22zero\\22:%.1f,\\22sw1\\22:%d,\\22feeding\\22:%d,\\22sw2\\22:%d,\\22sw3\\22:%d,\\22sw4\\22:%d%s}}\"",
MqttInfoStr.ClientID, (unsigned)MqttInfoStr.Ver, zero_kg, once_wt_kg, weight_kg, sw1, feeding, sw2, sw3, sw4, ext_esc);
} else {
CAT1_printf("AT+MPUB=\"/iot/data/up/%s\",0,0,\"{\\22header\\22:\\22iot.prop.post\\22,\\22body\\22:{\\22ver\\22:%u,\\22weight\\22:%.1f,\\22sw1\\22:%d,\\22feeding\\22:%d,\\22sw2\\22:%d,\\22sw3\\22:%d,\\22sw4\\22:%d%s,\\22energy\\22:%.2f}}\"",
MqttInfoStr.ClientID, (unsigned)MqttInfoStr.Ver, zero_kg, sw1, feeding, sw2, sw3, sw4, ext_esc, (double)hlw8032_get_energy_kwh());
}
} else {
char json[256];
if (g_feed_scale_on) {
snprintf(json, sizeof(json),
"{\"header\":\"iot.prop.post\",\"body\":{\"ver\":%u,\"weight\":%.1f,\"onceWt\":%.1f,\"zero\":%.1f,\"sw1\":%d,\"feeding\":%d,\"sw2\":%d,\"sw3\":%d,\"sw4\":%d%s}}",
(unsigned)MqttInfoStr.Ver, zero_kg, once_wt_kg, weight_kg, sw1, feeding, sw2, sw3, sw4, ext_plain);
} else {
snprintf(json, sizeof(json),
"{\"header\":\"iot.prop.post\",\"body\":{\"ver\":%u,\"weight\":%.1f,\"sw1\":%d,\"feeding\":%d,\"sw2\":%d,\"sw3\":%d,\"sw4\":%d%s,\"energy\":%.2f}}",
(unsigned)MqttInfoStr.Ver, zero_kg, sw1, feeding, sw2, sw3, sw4, ext_plain, (double)hlw8032_get_energy_kwh());
}
net_wifi_publish_up(json);
}
net_at_unlock();
return 0;
}
void NET_publish_response(const char *cmd_id, int ok, const char *msg)
{
if (g_net_tx_busy) { log_info("> NET: 应答上报被丢弃 (发送忙)"); return; }
if (net_at_trylock() != 0) { log_info("> NET: 应答上报被丢弃 (AT 忙)"); return; }
const char *text = msg ? msg : (ok ? "OK" : "command failed");
if (s_backend == NET_BACKEND_4G) {
CAT1_printf("AT+MPUB=\"/iot/data/up/%s\",0,0,\"{\\22id\\22:\\22%s\\22,\\22code\\22:0,\\22message\\22:\\22%s\\22}\"",
MqttInfoStr.ClientID, cmd_id, text);
} else {
char json[200];
snprintf(json, sizeof(json),
"{\"id\":\"%s\",\"code\":0,\"message\":\"%s\"}", cmd_id, text);
net_wifi_publish_up(json);
}
net_at_unlock();
}
/* 指令应答可靠版: 4G 通道等模组回 OK(约1.5s/次, 重试2次), 失败打 WARN;
* 背景: 普通版发完就走, 模组 UART 应答交织/链路半死时应答丢失,
* 表现为"继电器已动作但平台显示响应超时" */
int NET_publish_response_wait(const char *cmd_id, int ok, const char *msg)
{
if (g_net_tx_busy) { log_info("> NET: 应答上报被丢弃 (OTA忙)"); return -1; }
const char *text = msg ? msg : (ok ? "OK" : "command failed");
int rc = 0;
if (s_backend == NET_BACKEND_4G) {
if (net_at_trylock() != 0) { log_info("> NET: 应答上报被丢弃 (AT 忙)"); return -1; }
static char cmd[256];
sprintf(cmd, "AT+MPUB=\\22/iot/data/up/%s\\22,0,0,\\22{\\22id\\22:\\22%s\\22,\\22code\\22:0,\\22message\\22:\\22%s\\22}\\22",
MqttInfoStr.ClientID, cmd_id, text);
if (catSendCmd(cmd, "OK", 2, 15) != 0) {
rc = -1;
log_warn("> NET: 指令应答发送失败(重试后仍无 OK)");
}
net_at_unlock();
} else {
/* WiFi(软件 MQTT): QoS1 发布 + 等 PUBACK(broker 确认收到), 最多 3 次
* 注意: 收包解析在 net 任务, 等 PUBACK 必须释放 AT 锁, 否则死锁 */
char json[200];
snprintf(json, sizeof(json),
"{\"id\":\"%s\",\"code\":0,\"message\":\"%s\"}", cmd_id, text);
static uint16_t pkt_id = 0;
rc = -1;
for (int attempt = 0; attempt < 3; attempt++) {
net_at_lock();
pkt_id++;
if (pkt_id == 0) pkt_id = 1;
int prc = net_wifi_publish_qos1(json, pkt_id);
net_at_unlock();
if (prc != 0) break; /* 未就绪, 不必重试 */
if (mqttc_wait_puback(pkt_id, 1500) == 0) { rc = 0; break; }
log_warn("> NET: 应答 PUBACK 超时, 重试 %d/3", attempt + 1);
}
if (rc != 0) log_warn("> NET: 指令应答发送失败(PUBACK 超时)");
}
return rc;
}
/* 上报代码版本字符串: ver 字段,内容即 FIRMWARE_VERSION_STR每次联网成功报一次 */
void NET_publish_version(const char *ver)
{
if (g_net_tx_busy) { log_info("> NET: 版本上报被丢弃 (发送忙)"); return; }
if (net_at_trylock() != 0) { log_info("> NET: 版本上报被丢弃 (AT 忙)"); return; }
if (s_backend == NET_BACKEND_4G) {
CAT1_printf("AT+MPUB=\"/iot/data/up/%s\",0,0,\"{\\22header\\22:\\22iot.prop.post\\22,\\22body\\22:{\\22fwver\\22:\\22%s\\22}}\"",
MqttInfoStr.ClientID, ver);
} else {
char json[96];
snprintf(json, sizeof(json),
"{\"header\":\"iot.prop.post\",\"body\":{\"fwver\":\"%s\"}}", ver);
net_wifi_publish_up(json);
}
net_at_unlock();
}
/* 上报电量状态: on=1 正常, on=0 低电量。返回 0=已发出,-1=通道忙被丢弃 */
int NET_publish_power(int on)
{
if (g_net_tx_busy) { log_info("> NET: 电量上报被丢弃 (发送忙)"); return -1; }
if (net_at_trylock() != 0) { log_info("> NET: 电量上报被丢弃 (AT 忙)"); return -1; }
if (s_backend == NET_BACKEND_4G) {
CAT1_printf("AT+MPUB=\"/iot/data/up/%s\",0,0,\"{\\22header\\22:\\22iot.prop.post\\22,\\22body\\22:{\\22pow\\22:%d}}\"",
MqttInfoStr.ClientID, on ? 1 : 0);
} else {
char json[96];
snprintf(json, sizeof(json),
"{\"header\":\"iot.prop.post\",\"body\":{\"pow\":%d}}", on ? 1 : 0);
net_wifi_publish_up(json);
}
net_at_unlock();
return 0;
}
/* 断电预警专用:发 pow 并等模组回 OK最多 ~1.5s),确认模组已受理。
* 返回 0=模组已受理,-1=通道忙/无应答。WiFi 通道 QoS0 无 OK 概念,发完即返 0 */
int NET_publish_power_wait(int on)
{
if (g_net_tx_busy) return -1;
if (net_at_trylock() != 0) return -1;
int rc = 0;
if (s_backend == NET_BACKEND_4G) {
static char cmd[160];
sprintf(cmd, "AT+MPUB=\"/iot/data/up/%s\",0,0,\"{\\22header\\22:\\22iot.prop.post\\22,\\22body\\22:{\\22pow\\22:%d}}\"",
MqttInfoStr.ClientID, on ? 1 : 0);
if (catSendCmd(cmd, "OK", 1, 15) != 0) rc = -1;
} else {
char json[96];
snprintf(json, sizeof(json),
"{\"header\":\"iot.prop.post\",\"body\":{\"pow\":%d}}", on ? 1 : 0);
net_wifi_publish_up(json);
}
net_at_unlock();
return rc;
}
/* 上报累计电量+实时功率: energy(kWh) 数值 + power 字符串("220V,0.21A,46.2W"),同一条报文。
* power 物模型为字符串类型,一次性展示电压/电流/功率 */
void NET_publish_energy(float kwh, float power_w)
{
if (g_net_tx_busy) { log_info("> NET: energy 上报被丢弃 (发送忙)"); return; }
if (net_at_trylock() != 0) { log_info("> NET: energy 上报被丢弃 (AT 忙)"); return; }
char pwr[40];
snprintf(pwr, sizeof(pwr), "%dV,%.2fA,%.1fW",
(int)hlw8032_get_voltage_v(), (double)hlw8032_get_current_a(), (double)power_w);
if (s_backend == NET_BACKEND_4G) {
CAT1_printf("AT+MPUB=\"/iot/data/up/%s\",0,0,\"{\\22header\\22:\\22iot.prop.post\\22,\\22body\\22:{\\22energy\\22:%.2f,\\22power\\22:\\22%s\\22}}\"",
MqttInfoStr.ClientID, kwh, pwr);
} else {
char json[128];
snprintf(json, sizeof(json),
"{\"header\":\"iot.prop.post\",\"body\":{\"energy\":%.2f,\"power\":\"%s\"}}", kwh, pwr);
net_wifi_publish_up(json);
}
net_at_unlock();
}
int NET_mqtt_report(const char *header, const char *ota_id, int val)
{
if (g_net_tx_busy) { log_info("> NET: 进度/结果上报被丢弃 (发送忙)"); return -1; }
if (net_at_trylock() != 0) { log_info("> NET: 进度/结果上报被丢弃 (AT 忙)"); return -1; }
if (s_backend == NET_BACKEND_4G) {
int rc = air780e_mqtt_report(header, ota_id, val);
net_at_unlock();
return rc;
}
if (!header || !ota_id || !ota_id[0]) { net_at_unlock(); return -1; }
char json[200];
if (strstr(header, "progress")) {
snprintf(json, sizeof(json),
"{\"header\":\"%s\",\"body\":{\"id\":\"%.16s\",\"progress\":%d}}",
header, ota_id, val);
} else {
snprintf(json, sizeof(json),
"{\"header\":\"%s\",\"body\":{\"id\":\"%.16s\",\"success\":%s}}",
header, ota_id, val ? "true" : "false");
}
net_wifi_publish_up(json);
net_at_unlock();
return 0;
}
int NET_ota_confirm(void)
{
/* 两通道复用同一确认流程:无挂起 OTA 时内部直接返回;
* 其中的上报已改为 NET_mqtt_report按当前通道路由 */
return air780e_ota_confirm();
}
/*==== 下行消息统一分发(从 air780e.c 上提,与模组无关)====
* msg 可能是整条 +MSUB URC4G或纯 JSONWiFi均按子串匹配处理 */
void net_dispatch_message(const char *msg)
{
/* dedup by message id */
static char last_id[20] = {0};
char cur_id[20] = {0};
proto_get_id((char*)msg, cur_id, sizeof(cur_id));
if (cur_id[0] && strcmp(cur_id, last_id) == 0) {
log_info("> MQTT: 重复消息跳过id=%.16s", cur_id);
return;
}
strncpy(last_id, cur_id, sizeof(last_id)-1);
/* relay control */
if (strstr(msg, "iot.prop.set")) { relay_action((uint8_t *)msg); }
/* OTA upgrade */
if (strstr(msg, "iot.ota.upgrade.post")) {
log_info("> OTA 升级命令!");
char host[30]={0}, path[80]={0}, ota_id[20]={0}; int port = 81;
uint16_t new_ver = 0;
uint16_t crc16 = 0;
/* 解析版本号与 crc16 */
{ uint32_t v = 0; if (proto_get_u32((char*)msg, "ver", &v)) new_ver = (uint16_t)v; }
proto_get_hex16((char*)msg, "crc16", &crc16);
log_info("> OTA 版本: 当前=%u, 新版本=%u, crc16=0x%04X", MqttInfoStr.Ver, new_ver, crc16);
/* 版本号 ≤ 当前版本 → 跳过下载 (防止重复或降级) */
if (new_ver > 0 && new_ver <= MqttInfoStr.Ver) {
proto_get_id((char*)msg, ota_id, sizeof(ota_id));
log_info("> OTA: 版本相同或更低,跳过 (当前=%u, 新版本=%u)", MqttInfoStr.Ver, new_ver);
NET_mqtt_report("iot.ota.progress.post", ota_id, 10);
} else {
strncpy(host, MqttInfoStr.ServerIP, sizeof(host)-1); strcpy(path, "/firmware.bin");
proto_get_id((char*)msg, ota_id, sizeof(ota_id));
{ char fu[100]={0};
if (proto_get_str((char*)msg, "url", fu, sizeof(fu))) {
log_info("> OTA 下载地址: %s", fu); char *p=fu;
if(strncmp(p,"http://",7)==0)p+=7;
char *sl=strchr(p,'/'),*co=strchr(p,':');
if(co&&(!sl||co<sl)){ int hl=co-p; memcpy(host,p,hl); host[hl]=0;
co++; port=0; while(*co>='0'&&*co<='9'){port=port*10+(*co-'0');co++;}
if(sl)strncpy(path,sl,sizeof(path)-1); }
else if(sl){ int hl=sl-p; memcpy(host,p,hl); host[hl]=0; strncpy(path,sl,sizeof(path)-1); }
else strncpy(host,p,sizeof(host)-1);
}}
log_info("> OTA: %s:%d%s", host, port, path);
/* 平台任务 id 存 EEPROM偏移 175重启确认时补发 result.post 用。
* 注意:故意不用 W25Q64 OTA 信息结构(它 BL/APP 共享,改结构要重刷 BL
* 现场设备无法远程刷 BLEEPROM 是 APP 私有的 */
if (ota_id[0]) {
char id20[20] = {0};
strncpy(id20, ota_id, sizeof(id20) - 1);
eepromWriteData(OTA_ID_EEP_ADDR, id20, sizeof(id20));
}
/* 两条通道共用 W25Q64 双槽 + CRC + BootLoader 回退体系,仅下载传输不同 */
if (s_backend == NET_BACKEND_WIFI) {
net_wifi_ota(host, port, path, ota_id, new_ver, crc16);
} else {
air780e_trigger_ota(host, port, path, ota_id, new_ver, crc16);
}
}
}
}