562 lines
22 KiB
C
562 lines
22 KiB
C
/* 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)
|
||
* → 固定 SSID(60s)→ SmartConfig(60s)
|
||
* 任何方式联网成功后调 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~194:OTA 平台任务 id(确认后清 0xFF) */
|
||
|
||
static net_backend_t s_backend = NET_BACKEND_4G;
|
||
|
||
/* 上次成功联网方式(EEPROM 偏移 130,秤参数占用到 126,避开) */
|
||
#define EEPROM_NETMODE_ADDR 130
|
||
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("只存一次") */
|
||
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;
|
||
|
||
/* 读 EEPROM 记忆的上次成功联网方式:非法值(含 EEPROM 未初始化的 0xFF)按 4G */
|
||
uint8_t m = NET_MODE_4G;
|
||
eepromReadData(EEPROM_NETMODE_ADDR, &m, 1);
|
||
if (m > NET_MODE_WIFI_SMART) m = NET_MODE_4G;
|
||
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: 上次联网方式为 WiFi,WiFi 优先");
|
||
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) {
|
||
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) {
|
||
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 URC(4G)或纯 JSON(WiFi),均按子串匹配处理 */
|
||
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,
|
||
* 现场设备无法远程刷 BL),EEPROM 是 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);
|
||
}
|
||
}
|
||
}
|
||
}
|