409 lines
15 KiB
C
409 lines
15 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 "net_wifi_ota.h"
|
||
#include "air780e.h"
|
||
#include "led.h"
|
||
#include "log.h"
|
||
#include "main.h"
|
||
#include "uart.h"
|
||
#include "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;
|
||
|
||
/* 当前激活的网络后端 */
|
||
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;
|
||
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 优先");
|
||
s_backend = NET_BACKEND_WIFI;
|
||
}
|
||
}
|
||
|
||
/* 切到 WiFi 通道:停 4G 接收(共享缓冲互斥),WiFi 建链由 NET_process 立即发起 */
|
||
static void net_switch_to_wifi(void)
|
||
{
|
||
log_warn("> NET: 切换 4G -> 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();
|
||
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;
|
||
}
|
||
}
|
||
|
||
void 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; }
|
||
if (net_at_trylock() != 0) { log_info("> NET: 状态上报被丢弃 (AT 忙)"); return; }
|
||
/* ver 字段:固件版本号,平台据此可识别设备是否发生了 OTA 回退 */
|
||
/* sw1 与 feeding 都映射继电器 1 状态(无秤模式两者等效),
|
||
* 两个字段都上报,平台的 sw1 开关和 feeding 状态才能同步刷新 */
|
||
/* 无秤模式(PRODUCT_WITH_FEED_SCALE=0)不上报 zero/onceWt 字段 */
|
||
if (s_backend == NET_BACKEND_4G) {
|
||
#if PRODUCT_WITH_FEED_SCALE
|
||
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}}\"",
|
||
MqttInfoStr.ClientID, (unsigned)MqttInfoStr.Ver, zero_kg, once_wt_kg, weight_kg, sw1, feeding, sw2, sw3, sw4);
|
||
#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}}\"",
|
||
MqttInfoStr.ClientID, (unsigned)MqttInfoStr.Ver, zero_kg, sw1, feeding, sw2, sw3, sw4);
|
||
#endif
|
||
} else {
|
||
char json[256];
|
||
#if PRODUCT_WITH_FEED_SCALE
|
||
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}}",
|
||
(unsigned)MqttInfoStr.Ver, zero_kg, once_wt_kg, weight_kg, sw1, feeding, sw2, sw3, sw4);
|
||
#else
|
||
snprintf(json, sizeof(json),
|
||
"{\"header\":\"iot.prop.post\",\"body\":{\"ver\":%u,\"weight\":%.1f,\"sw1\":%d,\"feeding\":%d,\"sw2\":%d,\"sw3\":%d,\"sw4\":%d}}",
|
||
(unsigned)MqttInfoStr.Ver, zero_kg, sw1, feeding, sw2, sw3, sw4);
|
||
#endif
|
||
net_wifi_publish_up(json);
|
||
}
|
||
net_at_unlock();
|
||
}
|
||
|
||
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();
|
||
}
|
||
|
||
/* 上报电量状态: on=1 正常, on=0 低电量 */
|
||
void NET_publish_power(int on)
|
||
{
|
||
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:{\\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();
|
||
}
|
||
|
||
void NET_mqtt_report(const char *header, const char *ota_id, int val)
|
||
{
|
||
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) {
|
||
air780e_mqtt_report(header, ota_id, val);
|
||
net_at_unlock();
|
||
return;
|
||
}
|
||
if (!header || !ota_id || !ota_id[0]) { net_at_unlock(); return; }
|
||
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();
|
||
}
|
||
|
||
void NET_ota_confirm(void)
|
||
{
|
||
/* 两通道复用同一确认流程:无挂起 OTA 时内部直接返回;
|
||
* 其中的上报已改为 NET_mqtt_report,按当前通道路由 */
|
||
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);
|
||
/* 两条通道共用 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);
|
||
}
|
||
}
|
||
}
|
||
}
|