diff --git a/__pycache__/at_client.cpython-312.pyc b/__pycache__/at_client.cpython-312.pyc index 16417d1..70304c5 100644 Binary files a/__pycache__/at_client.cpython-312.pyc and b/__pycache__/at_client.cpython-312.pyc differ diff --git a/__pycache__/config.cpython-312.pyc b/__pycache__/config.cpython-312.pyc index 87ca0ac..2293549 100644 Binary files a/__pycache__/config.cpython-312.pyc and b/__pycache__/config.cpython-312.pyc differ diff --git a/__pycache__/network.cpython-312.pyc b/__pycache__/network.cpython-312.pyc index b5ee661..d2ea7a2 100644 Binary files a/__pycache__/network.cpython-312.pyc and b/__pycache__/network.cpython-312.pyc differ diff --git a/__pycache__/power.cpython-312.pyc b/__pycache__/power.cpython-312.pyc index 74bbc83..a44af99 100644 Binary files a/__pycache__/power.cpython-312.pyc and b/__pycache__/power.cpython-312.pyc differ diff --git a/__pycache__/version.cpython-310.pyc b/__pycache__/version.cpython-310.pyc index 1bfb602..eecf161 100644 Binary files a/__pycache__/version.cpython-310.pyc and b/__pycache__/version.cpython-310.pyc differ diff --git a/__pycache__/wifi.cpython-312.pyc b/__pycache__/wifi.cpython-312.pyc index 3607e2c..4605320 100644 Binary files a/__pycache__/wifi.cpython-312.pyc and b/__pycache__/wifi.cpython-312.pyc differ diff --git a/at_client.py b/at_client.py index 5bb003d..99acff5 100644 --- a/at_client.py +++ b/at_client.py @@ -69,7 +69,7 @@ class ATClient: # 同上:避免在 _reader_loop 持锁期间二次 acquire self._http_events.append(ev) - def send(self, cmd: str, expect: str = "OK", timeout_ms: int = 2000): + def send(self, cmd: str, expect: str = "OK", timeout_ms: int = 2000, abort_event=None): """ 发送 AT 命令并等待 expect(子串匹配)。 注意:expect=">" 用于等待 prompt。 @@ -90,6 +90,9 @@ class ATClient: t0 = time.ticks_ms() while abs(time.ticks_diff(time.ticks_ms(), t0)) < timeout_ms: + if abort_event is not None and abort_event.is_set(): + self._waiting = False + break if (not self._waiting) or (self._expect in self._resp): self._waiting = False break @@ -102,6 +105,39 @@ class ATClient: except: return str(self._resp) + def send_raw_and_wait(self, data: bytes, expect: str = "OK", timeout_ms: int = 1000, + suffix: bytes = b""): + """Register the response waiter before writing raw UART data.""" + expect_b = expect.encode() if isinstance(expect, str) else expect + with self._cmd_lock: + with self._q_lock: + self._waiting = True + self._expect = expect_b + self._resp = b"" + + total = 0 + while total < len(data): + n = self.uart.write(data[total:]) + if not n or n < 0: + time.sleep_ms(1) + continue + total += n + if suffix: + self.uart.write(suffix) + + t0 = time.ticks_ms() + while abs(time.ticks_diff(time.ticks_ms(), t0)) < timeout_ms: + if (not self._waiting) or (self._expect in self._resp): + self._waiting = False + break + time.sleep_ms(5) + + self._waiting = False + try: + return self._resp.decode(errors="ignore") + except: + return str(self._resp) + def _find_urc_tag(self, tag: bytes): """ 只在"真正的 URC 边界"查找 tag,避免误命中 HTTP payload 内容。 diff --git a/config.py b/config.py index ca67358..9413ab7 100644 --- a/config.py +++ b/config.py @@ -346,10 +346,13 @@ AUTO_POWER_OFF_IN_SECONDS = 10 * 60 # 自动关机时间(秒),0表示不 # 实机数据:正常放电约为正电流,插入充电线后约为负电流。 CHARGING_SHUTDOWN_ENABLED = True # True=充电时退出应用,False=关闭充电关机功能 CHARGING_DIAGNOSTIC_LOG_ENABLED = False -CHARGING_CHECK_INTERVAL_MS = 5000 +CHARGING_CHECK_INTERVAL_MS = 3000 CHARGING_CURRENT_THRESHOLD_MA = 100.0 CHARGING_CONFIRM_COUNT = 2 CHARGING_NOTIFY_TIMEOUT_MS = 30000 +CHARGING_4G_UART_LOCK_TIMEOUT_SEC = 2.5 +CHARGING_4G_PROMPT_TIMEOUT_MS = 1500 +CHARGING_4G_CONFIRM_TIMEOUT_MS = 1000 CHARGING_EXIT_SCRIPT = APP_DIR + "/charging_exit.sh" BATTERY_SOC_LPF_ALPHA = 0.5 diff --git a/network.py b/network.py index ec110b6..756201b 100644 --- a/network.py +++ b/network.py @@ -67,6 +67,7 @@ class NetworkManager: self._queue_lock = threading.Lock() self._send_event = threading.Event() self._uart4g_lock = threading.Lock() + self._terminal_send_event = threading.Event() self._device_id = None self._password = None self._raw_line_data = [] @@ -676,7 +677,7 @@ class NetworkManager: except OSError: pass w = network.wifi.Wifi() - e = w.connect(ssid, password, wait=True, timeout=15) + e = w.connect(ssid, password, wait=True, timeout=10) err.check_raise(e, "connect wifi failed") if self.logger: self.logger.info(f"[ota] Connect success, got ip{w.get_ip()}") @@ -715,6 +716,29 @@ class NetworkManager: self._enqueue((msg_type, data_dict, sent_event), high) return bool(sent_event.wait(max(0, int(timeout_ms)) / 1000.0)) + def safe_replace_queue_and_wait(self, data_dict, msg_type=2, timeout_ms=30000): + """Drop queued messages, enqueue one terminal message, and wait for its TCP write.""" + sent_event = threading.Event() + with self._queue_lock: + self._high_send_queue.clear() + self._normal_send_queue.clear() + self._high_send_queue.append((msg_type, data_dict, sent_event)) + self._send_event.set() + return bool(sent_event.wait(max(0, int(timeout_ms)) / 1000.0)) + + def safe_terminal_send_and_wait(self, data_dict, msg_type=2, timeout_ms=30000): + """Cancel ordinary 4G waits and replace queued work with one terminal message.""" + sent_event = threading.Event() + result = {"sent": False} + self._terminal_send_event.set() + with self._queue_lock: + self._high_send_queue.clear() + self._normal_send_queue.clear() + self._high_send_queue.append((msg_type, data_dict, sent_event, "terminal", result)) + self._send_event.set() + completed = sent_event.wait(max(0, int(timeout_ms)) / 1000.0) + return bool(completed and result["sent"]) + def connect_server(self): """ 连接到服务器(自动选择WiFi或4G) @@ -1114,8 +1138,12 @@ class NetworkManager: return False try: for _ in range(max_retries): + if self._terminal_send_event.is_set(): + return False cmd = f'AT+MIPSEND={link_id},{len(data)}' if ">" not in hardware_manager.at_client.send(cmd, ">", 2000): + if self._terminal_send_event.is_set(): + return False time.sleep_ms(50) continue @@ -1130,14 +1158,73 @@ class NetworkManager: hardware_manager.uart4g.write(b"\x1A") with hardware_manager.at_client._q_lock: hardware_manager.at_client._rx = b"" - r = hardware_manager.at_client.send("", "OK", 8000) + r = hardware_manager.at_client.send( + "", "OK", 8000, abort_event=self._terminal_send_event + ) if ("SEND OK" in r) or ("OK" in r) or ("+MIPSEND" in r): return True + if self._terminal_send_event.is_set(): + return False time.sleep_ms(50) return False finally: self._uart4g_lock.release() + def _tcp_send_terminal_raw(self, data: bytes) -> bool: + if not self._tcp_connected: + return False + if self._network_type == "wifi": + return self._tcp_send_raw_via_wifi(data, max_retries=1) + if self._network_type != "4g": + return False + + link_id = getattr(config, "TCP_LINK_ID", 0) + lock_timeout_sec = float( + getattr(config, "CHARGING_4G_UART_LOCK_TIMEOUT_SEC", 2.5) + ) + prompt_timeout_ms = int( + getattr(config, "CHARGING_4G_PROMPT_TIMEOUT_MS", 1500) + ) + confirm_timeout_ms = int( + getattr(config, "CHARGING_4G_CONFIRM_TIMEOUT_MS", 1000) + ) + lock_start_ms = time.ticks_ms() + if not self._uart4g_lock.acquire(timeout=max(0.0, lock_timeout_sec)): + self.logger.warning( + f"[CHARGE-4G] uart_lock timeout timeout_sec={lock_timeout_sec}" + ) + return False + try: + lock_elapsed_ms = abs(time.ticks_diff(time.ticks_ms(), lock_start_ms)) + cmd = f'AT+MIPSEND={link_id},{len(data)}' + prompt_start_ms = time.ticks_ms() + if ">" not in hardware_manager.at_client.send( + cmd, ">", max(0, prompt_timeout_ms)): + prompt_elapsed_ms = abs(time.ticks_diff(time.ticks_ms(), prompt_start_ms)) + self.logger.warning( + f"[CHARGE-4G] prompt failed lock_ms={lock_elapsed_ms} " + f"prompt_ms={prompt_elapsed_ms}" + ) + return False + prompt_elapsed_ms = abs(time.ticks_diff(time.ticks_ms(), prompt_start_ms)) + confirm_start_ms = time.ticks_ms() + r = hardware_manager.at_client.send_raw_and_wait( + data, + expect="OK", + timeout_ms=max(0, confirm_timeout_ms), + suffix=b"\x1A", + ) + confirm_elapsed_ms = abs(time.ticks_diff(time.ticks_ms(), confirm_start_ms)) + sent = ("SEND OK" in r) or ("OK" in r) or ("+MIPSEND" in r) + self.logger.warning( + f"[CHARGE-4G] send_done lock_ms={lock_elapsed_ms} " + f"prompt_ms={prompt_elapsed_ms} confirm_ms={confirm_elapsed_ms} " + f"sent={sent}" + ) + return sent + finally: + self._uart4g_lock.release() + def _configure_ssl_before_connect(self, link_id: int) -> bool: """按手册:MSSLCFG(auth) -> (可选) MSSLCERTWR -> MSSLCFG(cert) -> MIPCFG(ssl)""" ssl_id = getattr(config, "SSL_ID", 1) @@ -1874,12 +1961,25 @@ class NetworkManager: pending_cleared = False last_heartbeat_ack_time = time.ticks_ms() last_heartbeat_send_time = time.ticks_ms() + last_wifi_sta_check_time = time.ticks_ms() while True: # 如果底层连接已断开,尽快跳出内层循环触发重连/重选网络 if not self._tcp_connected: break + if self._network_type == "wifi": + now_ms = time.ticks_ms() + if abs(time.ticks_diff(now_ms, last_wifi_sta_check_time)) >= 1000: + last_wifi_sta_check_time = now_ms + if not wifi_manager.is_sta_associated(): + self.logger.warning( + "[WIFI-TCP] STA disconnected; leave WiFi session and reselect network" + ) + wifi_manager.disconnect_wifi() + self._tcp_connected = False + break + # OTA 期间暂停 TCP 活动 try: from ota_manager import ota_manager @@ -2312,8 +2412,23 @@ class NetworkManager: if item: msg_type, data_dict = item[:2] sent_event = item[2] if len(item) > 2 else None + item_is_terminal = len(item) > 3 and item[3] == "terminal" + terminal_result = item[4] if item_is_terminal and len(item) > 4 else None pkt = self._netcore.make_packet(msg_type, data_dict) - if not self.tcp_send_raw(pkt): + send_ok = ( + self._tcp_send_terminal_raw(pkt) + if item_is_terminal + else self.tcp_send_raw(pkt) + ) + if not send_ok: + if item_is_terminal: + if terminal_result is not None: + terminal_result["sent"] = False + if sent_event is not None: + sent_event.set() + break + if self._terminal_send_event.is_set(): + continue # 发送失败:将消息放回队首(队列满则丢弃) with self.get_queue_lock(): if item_is_high: @@ -2329,6 +2444,8 @@ class NetworkManager: pass break if sent_event is not None: + if terminal_result is not None: + terminal_result["sent"] = True sent_event.set() # 发送激光校准结果 diff --git a/power.py b/power.py index c25e9c2..8afa4d4 100644 --- a/power.py +++ b/power.py @@ -179,6 +179,11 @@ def charging_shutdown_monitor(): if current_ma < -abs(threshold_ma): confirm_count += 1 + if logger: + logger.warning( + f"[CHARGE-TIMING] sample tick_ms={maix_time.ticks_ms()} " + f"current={current_ma:.1f}mA confirm={confirm_count}/{confirm_required}" + ) if logger: logger.info( f"[CHARGE] INA226 充电电流 {current_ma:.1f}mA " @@ -209,12 +214,30 @@ def charging_shutdown_monitor(): 0, int(getattr(config, "CHARGING_NOTIFY_TIMEOUT_MS", 30000)), ) - notification_sent = network_manager.safe_enqueue_and_wait( + notify_start_ms = maix_time.ticks_ms() + if logger: + logger.warning( + f"[CHARGE-TIMING] enqueue_start tick_ms={notify_start_ms} " + f"network={network_manager.network_type} " + f"tcp_connected={network_manager.tcp_connected} " + f"timeout_ms={notify_timeout_ms}" + ) + notification_sent = network_manager.safe_terminal_send_and_wait( {"poweroff": "充电中"}, 2, - high=True, timeout_ms=notify_timeout_ms, ) + notify_end_ms = maix_time.ticks_ms() + notify_elapsed_ms = abs( + maix_time.ticks_diff(notify_end_ms, notify_start_ms) + ) + if logger: + logger.warning( + f"[CHARGE-TIMING] enqueue_done tick_ms={notify_end_ms} " + f"elapsed_ms={notify_elapsed_ms} sent={bool(notification_sent)} " + f"network={network_manager.network_type} " + f"tcp_connected={network_manager.tcp_connected}" + ) if notification_sent: if logger: logger.info("[CHARGE] 充电状态已发送到服务器") diff --git a/t11_v2.15.33_20260812_145253.zip b/t11_v2.15.33_20260812_145253.zip new file mode 100644 index 0000000..2899927 Binary files /dev/null and b/t11_v2.15.33_20260812_145253.zip differ diff --git a/version.md b/version.md index 8116534..c43d71e 100644 --- a/version.md +++ b/version.md @@ -36,3 +36,4 @@ # 2.15.24 空改测试 # 2.15.25 修复整合后关机失败和ota格式更新问题 # 2.15.26 +# 2.15.33 26-8-12 14:03 修改充4g电关机时间 修复切换网络卡住bug diff --git a/version.py b/version.py index f2c7323..22ba88d 100644 --- a/version.py +++ b/version.py @@ -4,6 +4,6 @@ 应用版本号 每次 OTA 更新时,只需要更新这个文件中的版本号 """ -VERSION = '2.15.31' +VERSION = '2.15.33' diff --git a/wifi.py b/wifi.py index 4dc63f4..943a011 100644 --- a/wifi.py +++ b/wifi.py @@ -549,15 +549,20 @@ class WiFiManager: on_poor_quality_callback: WiFi质量差时的回调函数 """ with self._wifi_quality_lock: - if self._wifi_quality_monitor_thread is not None and self._wifi_quality_monitor_thread.is_alive(): + current_thread = self._wifi_quality_monitor_thread + current_stop_event = self._wifi_quality_stop_event + if (current_thread is not None and current_thread.is_alive() + and not current_stop_event.is_set()): self.logger.warning("[WiFi Monitor] 监测线程已在运行") return self._network_type_callback = network_type_callback self._on_poor_quality_callback = on_poor_quality_callback - self._wifi_quality_stop_event.clear() + stop_event = threading.Event() + self._wifi_quality_stop_event = stop_event self._wifi_quality_monitor_thread = threading.Thread( target=self._quality_monitor_loop, + args=(stop_event,), daemon=True, name="wifi_quality_monitor" ) @@ -568,13 +573,14 @@ class WiFiManager: """停止 WiFi 质量监测线程""" with self._wifi_quality_lock: t = self._wifi_quality_monitor_thread + stop_event = self._wifi_quality_stop_event if t is None: return if not t.is_alive(): self._wifi_quality_monitor_thread = None return - self._wifi_quality_stop_event.set() + stop_event.set() try: t.join(timeout=2.0) except Exception as e: @@ -588,12 +594,12 @@ class WiFiManager: self._wifi_quality_monitor_thread = None self.logger.info("[WiFi Monitor] 已停止后台监测线程") - def _quality_monitor_loop(self): + def _quality_monitor_loop(self, stop_event): """ WiFi 质量监测循环(后台线程) 每 5 秒检查 STA 关联状态和 RSSI,发现断链或质量差则触发切换 """ - while not self._wifi_quality_stop_event.is_set(): + while not stop_event.is_set(): try: # 只在 WiFi 连接时才测量 network_type = self._network_type_callback() @@ -631,7 +637,8 @@ class WiFiManager: self.logger.warning("[WiFi Monitor] 质量差,切换前快速重试 2 次(每次间隔1秒)") for retry_idx in range(2): - time.sleep_ms(1000) + if stop_event.wait(1.0): + return reachable2 = self.is_sta_associated() rtt2 = None rssi2 = self._get_wifi_rssi_dbm() @@ -660,7 +667,7 @@ class WiFiManager: self._on_poor_quality_callback() # 休眠 5 秒 - time.sleep(5) + stop_event.wait(5.0) except Exception as e: self.logger.error(f"[WiFi Monitor] 监测异常:{e}")