299 lines
12 KiB
C
299 lines
12 KiB
C
/* network.c - 网络层:4G/WiFi 双通道抽象与故障切换
|
||
*
|
||
* 业务层(app_loop/relay/feed_scale)只调 NET_* 接口,不感知底层模组。
|
||
* 通道切换策略:
|
||
* 4G 模式:无 SIM 卡时每 10s 重试 air780e_init(),连续 3 次仍无卡 → 切 WiFi;
|
||
* WiFi 模式:net_wifi_init() 建链(已存AP 20s → 固定SSID 60s → SmartConfig 60s),
|
||
* 一轮失败即切回 4G,如此循环;运行中断链后自动重连。
|
||
*/
|
||
|
||
#include "network.h"
|
||
#include "net_wifi.h"
|
||
#include "air780e.h"
|
||
#include "led.h"
|
||
#include "log.h"
|
||
#include "main.h"
|
||
#include "uart.h"
|
||
#include "relay.h"
|
||
#include "proto.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;
|
||
|
||
/* 无 SIM 重试多少次后切 WiFi */
|
||
#define NET_NO_SIM_SWITCH_COUNT 3
|
||
/* WiFi 一轮完整建链(已存AP→固定SSID→SmartConfig)失败后切回 4G */
|
||
#define NET_WIFI_FAIL_SWITCH_COUNT 1
|
||
|
||
net_backend_t NET_GetActiveBackend(void)
|
||
{
|
||
return s_backend;
|
||
}
|
||
|
||
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;
|
||
s_backend = NET_BACKEND_4G;
|
||
|
||
air780e_init();
|
||
|
||
if (g_no_sim_detected) {
|
||
log_warn("> 4G no SIM, please insert SIM card");
|
||
/* 不立即切 WiFi:由 NET_process 重试几次仍无卡后再切换 */
|
||
}
|
||
|
||
g_net_mqtt_ready = g_air780e_mqtt_ready;
|
||
}
|
||
|
||
/* 切到 WiFi 通道:停 4G 接收(共享缓冲互斥),WiFi 建链由 NET_process 立即发起 */
|
||
static void net_switch_to_wifi(void)
|
||
{
|
||
log_warn("> NET: switch 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: switch 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();
|
||
air780e_init();
|
||
}
|
||
|
||
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 unavailable (no_sim=%d, no_signal=%d), retry init %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;
|
||
air780e_init();
|
||
if (!g_no_sim_detected && !g_no_signal_detected) {
|
||
log_info("> NET: 4G SIM/signal OK or network recovered");
|
||
fail_count = 0;
|
||
} else if (fail_count >= NET_NO_SIM_SWITCH_COUNT) {
|
||
log_warn("> NET: 4G unavailable after %d retries, fallback to 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;
|
||
}
|
||
|
||
air780e_process();
|
||
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 connect attempt %d/%d",
|
||
fail_count, NET_WIFI_FAIL_SWITCH_COUNT);
|
||
if (net_wifi_init() == 0) {
|
||
fail_count = 0;
|
||
} else if (fail_count >= NET_WIFI_FAIL_SWITCH_COUNT) {
|
||
log_warn("> NET: WiFi connect failed, fallback to 4G");
|
||
net_switch_to_4g();
|
||
fail_count = 0;
|
||
retry_tick = 0;
|
||
}
|
||
}
|
||
} else {
|
||
retry_tick = 0;
|
||
fail_count = 0;
|
||
}
|
||
|
||
net_wifi_process();
|
||
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: status dropped (tx busy)"); return; }
|
||
/* 云平台字段映射与参考代码字面相反:weight 显示为“空载重量”,zero 显示为“剩余料重” */
|
||
/* ver 字段:固件版本号,平台据此可识别设备是否发生了 OTA 回退 */
|
||
/* sw1 与 feeding 都映射继电器 1 状态(无秤模式两者等效),
|
||
* 两个字段都上报,平台的 sw1 开关和 feeding 状态才能同步刷新 */
|
||
if (s_backend == NET_BACKEND_4G) {
|
||
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 {
|
||
char json[256];
|
||
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);
|
||
net_wifi_publish_up(json);
|
||
}
|
||
}
|
||
|
||
void NET_publish_response(const char *cmd_id, int ok, const char *msg)
|
||
{
|
||
if (g_net_tx_busy) { log_info("> NET: response dropped (tx busy)"); 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);
|
||
}
|
||
}
|
||
|
||
/* 上报电量状态: on=1 正常, on=0 低电量 */
|
||
void NET_publish_power(int on)
|
||
{
|
||
if (g_net_tx_busy) { log_info("> NET: power dropped (tx busy)"); 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);
|
||
}
|
||
}
|
||
|
||
void NET_mqtt_report(const char *header, const char *ota_id, int val)
|
||
{
|
||
if (g_net_tx_busy) { log_info("> NET: report dropped (tx busy)"); return; }
|
||
if (s_backend == NET_BACKEND_4G) {
|
||
air780e_mqtt_report(header, ota_id, val);
|
||
return;
|
||
}
|
||
if (!header || !ota_id || !ota_id[0]) 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);
|
||
}
|
||
|
||
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: dup skip, 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 cmd!");
|
||
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 ver: cur=%u, new=%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: same or lower ver, skip (cur=%u, new=%u)", MqttInfoStr.Ver, new_ver);
|
||
NET_mqtt_report("iot.ota.progress.post", ota_id, 10);
|
||
} else if (s_backend == NET_BACKEND_WIFI) {
|
||
/* WiFi 通道不支持 OTA 下载(无 HTTP 分块下载实现),回复平台请用 4G 升级 */
|
||
proto_get_id((char*)msg, ota_id, sizeof(ota_id));
|
||
log_warn("> OTA: WiFi channel, OTA via 4G only, reject");
|
||
NET_publish_response(ota_id, 0, "WiFi channel, OTA via 4G only");
|
||
} 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 url: %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);
|
||
air780e_trigger_ota(host, port, path, ota_id, new_ver, crc16);
|
||
}
|
||
}
|
||
}
|