shuichan_old/V1.5/STM32F103_App1/HardWare/NETWORK/network.c

429 lines
16 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 "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;
/* 当前激活的网络后端 */
volatile net_ui_phase_t g_net_ui_phase = NET_UI_NONE;
volatile int g_net_ui_countdown = 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: 上次联网方式为 WiFiWiFi 优先");
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();
}
/* 上报累计电量: energy 字段,单位 kWh两位小数 */
void NET_publish_energy(float kwh)
{
if (g_net_tx_busy) { log_info("> NET: energy 上报被丢弃 (发送忙)"); return; }
if (net_at_trylock() != 0) { log_info("> NET: energy 上报被丢弃 (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:{\22energy\22:%.2f}}\"",
MqttInfoStr.ClientID, kwh);
} else {
char json[96];
snprintf(json, sizeof(json),
"{\"header\":\"iot.prop.post\",\"body\":{\"energy\":%.2f}}", kwh);
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 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);
/* 两条通道共用 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);
}
}
}
}