/* 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 会话占用 */ static int net_at_trylock(void) { if (s_net_at_mutex && xTaskGetSchedulerState() == taskSCHEDULER_RUNNING) { return xSemaphoreTakeRecursive(s_net_at_mutex, 0) == 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: last mode saved -> %s", mode == NET_MODE_4G ? "4G" : mode == NET_MODE_WIFI_FIXED ? "WiFi(fixed AP)" : "WiFi(smartconfig AP)"); } 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 no SIM, please insert SIM card"); /* 不立即切 WiFi:由 NET_process 重试几次仍无卡后再切换 */ } g_net_mqtt_ready = g_air780e_mqtt_ready; } else { /* 上次 WiFi 成功:WiFi 优先,NET_process 会立即发起 net_wifi_init */ log_info("> NET: last mode is WiFi, WiFi first"); s_backend = NET_BACKEND_WIFI; } } /* 切到 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(); 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 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; net_at_lock(); air780e_init(); net_at_unlock(); 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; NET_SaveMode(NET_MODE_4G); /* 4G 联网成功:更新方式记忆(未变化则空操作) */ } 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); 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 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; } if (net_at_trylock() != 0) { log_info("> NET: status dropped (AT busy)"); 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: response dropped (tx busy)"); return; } if (net_at_trylock() != 0) { log_info("> NET: response dropped (AT 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); } net_at_unlock(); } /* 上报电量状态: on=1 正常, on=0 低电量 */ void NET_publish_power(int on) { if (g_net_tx_busy) { log_info("> NET: power dropped (tx busy)"); return; } if (net_at_trylock() != 0) { log_info("> NET: power dropped (AT 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); } net_at_unlock(); } 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 (net_at_trylock() != 0) { log_info("> NET: report dropped (AT busy)"); 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: 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 { 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='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); } } } }