diff --git a/app.yaml b/app.yaml index 0a379e7..60ebd40 100644 --- a/app.yaml +++ b/app.yaml @@ -1,6 +1,6 @@ id: t11 name: t11 -version: 2.15.3 +version: 2.15.6 author: t11 icon: '' desc: t11 diff --git a/config.py b/config.py index 9dfb6b3..a1e5195 100644 --- a/config.py +++ b/config.py @@ -309,7 +309,7 @@ LASER_THICKNESS = 1 LASER_LENGTH = 2 # ==================== 图像保存配置 ==================== -SAVE_IMAGE_ENABLED = False # 是否保存图像(True=保存,False=不保存) +SAVE_IMAGE_ENABLED = True # 是否保存图像(True=保存,False=不保存) PHOTO_DIR = "/root/phot" # 照片存储目录 MAX_IMAGES = 1000 # Stage2 调试目录(默认 PHOTO_DIR/stage2_roi)内 JPEG 最多保留张数;None 表示与 MAX_IMAGES 相同 diff --git a/network.py b/network.py index e845308..2aabb42 100644 --- a/network.py +++ b/network.py @@ -8,7 +8,7 @@ import json import re from math import e import struct -from maix import time,network,err +from maix import time, network, err import hmac import hashlib import ujson @@ -24,7 +24,6 @@ from wifi import wifi_manager import subprocess - def _wifi_tls_would_block(exc): """ 非阻塞 TLS 下 recv/send 常抛出 WANT_READ / WANT_WRITE(或等价文案), @@ -37,8 +36,8 @@ def _wifi_tls_would_block(exc): if _ssl is not None and isinstance(exc, _ssl.SSLError): err = getattr(exc, "errno", None) if err in ( - getattr(_ssl, "SSL_ERROR_WANT_READ", 2), - getattr(_ssl, "SSL_ERROR_WANT_WRITE", 3), + getattr(_ssl, "SSL_ERROR_WANT_READ", 2), + getattr(_ssl, "SSL_ERROR_WANT_WRITE", 3), ): return True msg = str(exc).lower() @@ -50,17 +49,17 @@ def _wifi_tls_would_block(exc): class NetworkManager: """网络通信管理器(单例)""" _instance = None - + def __new__(cls): if cls._instance is None: cls._instance = super(NetworkManager, cls).__new__(cls) cls._instance._initialized = False return cls._instance - + def __init__(self): if self._initialized: return - + # 私有状态 self._tcp_connected = False self._high_send_queue = [] @@ -72,19 +71,20 @@ class NetworkManager: self._password = None self._raw_line_data = [] self._manual_trigger_flag = False - + # 网络类型状态 self._network_type = None # "wifi" 或 "4G" 或 None # 本次上电曾因 WiFi 质量差切换到 4G 后,直至关机不再改回 WiFi self._session_force_4g = False - + self._initialized = True # 导入 archery_netcore 模块,并检查是否存在 parse_packet 和 make_packet 函数 try: import archery_netcore as _netcore self._netcore = _netcore - if hasattr(self._netcore, "parse_packet") and hasattr(self._netcore, "make_packet") and hasattr(self._netcore, "actions_for_inner_cmd"): + if hasattr(self._netcore, "parse_packet") and hasattr(self._netcore, "make_packet") and hasattr( + self._netcore, "actions_for_inner_cmd"): print("[NET] archery_netcore found") else: print("[NET] archery_netcore not found parse_packet or make_packet") @@ -96,50 +96,50 @@ class NetworkManager: # 服务器相关 self._server_ip = self._netcore.get_config().get("SERVER_IP") self._server_port = self._netcore.get_config().get("SERVER_PORT") - + # ==================== 状态访问(只读属性)==================== - + @property def logger(self): """获取 logger 对象""" return logger_manager.logger - + @property def tcp_connected(self): """TCP连接状态""" return self._tcp_connected - + @property def device_id(self): """设备ID""" return self._device_id - + @property def password(self): """密码""" return self._password - + @property def has_pending_messages(self): """是否有待发送消息""" with self._queue_lock: return len(self._high_send_queue) > 0 or len(self._normal_send_queue) > 0 - + @property def manual_trigger_flag(self): """手动触发标志""" return self._manual_trigger_flag - + @property def network_type(self): """当前使用的网络类型("wifi" 或 "4g")""" return self._network_type - + @property def wifi_connected(self): """WiFi是否已连接""" return wifi_manager.wifi_connected - + @property def wifi_ip(self): """WiFi IP地址""" @@ -147,11 +147,10 @@ class NetworkManager: # ==================== 内部状态管理方法 ==================== - def set_manual_trigger(self, value=True): """设置手动触发标志(公共方法)""" self._manual_trigger_flag = value - + def clear_manual_trigger(self): """清除手动触发标志(公共方法)""" self._manual_trigger_flag = False @@ -159,12 +158,12 @@ class NetworkManager: def _set_tcp_connected(self, connected): """设置TCP连接状态(内部方法)""" self._tcp_connected = connected - + def _set_device_info(self, device_id, password): """设置设备信息(内部方法)""" self._device_id = device_id self._password = password - + def _enqueue(self, item, high=False): """线程安全地加入队列(内部方法)""" with self._queue_lock: @@ -173,7 +172,7 @@ class NetworkManager: else: self._normal_send_queue.append(item) self._send_event.set() - + def _dequeue(self): """线程安全地从队列取出(内部方法)""" with self._queue_lock: @@ -182,27 +181,28 @@ class NetworkManager: elif self._normal_send_queue: return self._normal_send_queue.pop(0) return None - + def _set_raw_line_data(self, data): """设置原始行数据(内部方法)""" self._raw_line_data = data - + def _get_raw_line_data(self): """获取原始行数据(内部方法)""" return self._raw_line_data - + def get_uart_lock(self): """获取UART锁(用于with语句)""" return self._uart4g_lock - + def get_queue_lock(self): """获取队列锁(用于with语句)""" return self._queue_lock - + # ==================== 业务方法 ==================== - + def read_device_id(self): """从 /device_key 文件读取设备唯一 ID,失败则使用默认值""" + def _set_password_for_device_id(device_id): if getattr(config, "USE_TCP_SSL", False): iccid = self.get_4g_mccid() @@ -224,24 +224,25 @@ class NetworkManager: return device_id except Exception as e: self.logger.error(f"[ERROR] 无法读取 /device_key: {e}") - + # 使用默认值 default_id = "DEFAULT_DEVICE_ID" self._device_id = default_id _set_password_for_device_id(default_id) return default_id - + # ==================== WiFi 管理方法(委托给 wifi_manager)==================== - + def is_wifi_connected(self): """检查WiFi是否已连接""" return wifi_manager.is_wifi_connected() - + def connect_wifi(self, ssid, password, verify_host=None, verify_port=None, persist=True, timeout_s=20): """ 连接 Wi-Fi:委托 ``wifi_manager.connect_wifi``。 未指定 ``verify_host``/``verify_port`` 时,可达性校验使用本管理器配置的 ``_server_ip``/``_server_port``。 """ + def _verify(ip: str): v_host = verify_host if verify_host is not None else self._server_ip v_port = verify_port if verify_port is not None else self._server_port @@ -261,7 +262,7 @@ class NetworkManager: persist=persist, timeout_s=timeout_s, ) - + def is_server_reachable(self, host, port=80, timeout=5): """检查目标主机端口是否可达(用于网络检测)""" try: @@ -274,17 +275,17 @@ class NetworkManager: except Exception as e: self.logger.warning(f"[NET] 无法连接 {host}:{port} - {e}") return False - + # ==================== 网络选择策略 ==================== - + def _get_wifi_rssi_dbm(self): """获取 WiFi 信号强度(委托给 wifi_manager)""" return wifi_manager._get_wifi_rssi_dbm() - + def _measure_wifi_tcp_rtt_ms(self, host, port, samples=3, per_sample_timeout_ms=900): """测量 WiFi TCP RTT(委托给 wifi_manager)""" return wifi_manager._measure_wifi_tcp_rtt_ms(host, port, samples, per_sample_timeout_ms) - + def _is_wifi_quality_bad(self, wifi_rtt_ms, wifi_rssi_dbm): """判断 WiFi 质量是否差(委托给 wifi_manager)""" return wifi_manager._is_wifi_quality_bad(wifi_rtt_ms, wifi_rssi_dbm) @@ -438,7 +439,7 @@ class NetworkManager: """锁定本次会话为 4G(直到关机,期间不再回切 WiFi)""" self._session_force_4g = True self._network_type = "4g" - + def select_network(self, prefer_wifi=None): """ 自动选择网络(WiFi优先) @@ -457,7 +458,7 @@ class NetworkManager: self.logger.info("[NET] 会话锁定 4G:继续使用 4G(跳过 WiFi 质量评估)") self._network_type = "4g" return "4g" - + host = self._server_ip port = self._server_port @@ -507,7 +508,7 @@ class NetworkManager: # 3) 两者都不可用 self.logger.error("[NET] WiFi 与 4G 均不可用") return None - + def _start_wifi_quality_monitor(self): """ 启动 WiFi 质量后台监测线程(委托给 wifi_manager) @@ -516,31 +517,31 @@ class NetworkManager: network_type_callback=lambda: self._network_type, on_poor_quality_callback=self._switch_to_4g_due_to_poor_wifi ) - + def _stop_wifi_quality_monitor(self): """停止 WiFi 质量监测线程(委托给 wifi_manager)""" wifi_manager.stop_quality_monitor() - + def get_wifi_quality_status(self): """获取当前 WiFi 质量状态(委托给 wifi_manager)""" return wifi_manager.get_wifi_quality_status() - + def _switch_to_4g_due_to_poor_wifi(self): """ 由于 WiFi 质量差,切换到 4G 网络 """ self.logger.info("[WiFi->4G] 开始切换到 4G 网络") - + # 1. 标记本次上电强制使用 4G self._session_force_4g = True - + # 2. 关闭 WiFi socket wifi_manager.disconnect_wifi() - + # 3. 重置连接状态 self._tcp_connected = False self._network_type = None # 清空,让 select_network 重新选择 - + # 4. 检查 4G 是否可用 if self.is_4g_available(): self._network_type = "4g" @@ -552,7 +553,6 @@ class NetworkManager: self._session_force_4g = False return False - def _cmd200_detect_laser(self): """后台线程执行 cmd200 激光检测,避免阻塞主循环""" from laser_manager import laser_manager @@ -598,8 +598,24 @@ class NetworkManager: err.check_raise(e, "connect wifi failed") if self.logger: self.logger.info(f"[ota] Connect success, got ip{w.get_ip()}") + self.safe_enqueue( + { + "cmd": 300, + "result": "ota start...", + "wifi": w.get_ip(), + }, + 2, + ) subprocess.run( ["sh", "/maixapp/apps/t11/ota_curl.sh", ota_res_url]) + self.safe_enqueue( + { + "cmd": 300, + "result": "success", + "wifi": w.get_ip(), + }, + 2, + ) except Exception as e: self.logger.error(f"[ota] cmd300 失败: {e}") self.safe_enqueue( @@ -614,9 +630,6 @@ class NetworkManager: def safe_enqueue(self, data_dict, msg_type=2, high=False): """线程安全地将消息加入队列(公共方法)""" self._enqueue((msg_type, data_dict), high) - - - def connect_server(self): """ @@ -632,21 +645,21 @@ class NetworkManager: elif self._network_type == "4g": return True # 4G连接状态由AT命令维护 return False - + # 自动选择网络 network_type = self.select_network() if not network_type: return False - + self.logger.info(f"连接到服务器,使用{network_type.upper()}...") - + # 根据网络类型建立TCP连接 if network_type == "wifi": return self._connect_tcp_via_wifi() elif network_type == "4g": return self._connect_tcp_via_4g() return False - + def _wrap_wifi_tls(self, plain_sock, hostname): """ 在已建立的 TCP socket 上做 TLS(WiFi 走主机 ssl 库;4G 仍用模组 AT+SSL)。 @@ -698,7 +711,7 @@ class NetworkManager: # 创建TCP socket wifi_manager.wifi_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) wifi_manager.wifi_socket.settimeout(5.0) # 5秒超时 - + # 连接到服务器 use_ssl = getattr(config, "USE_TCP_SSL", False) host = self._server_ip @@ -708,35 +721,35 @@ class NetworkManager: if use_ssl: wifi_manager.wifi_socket = self._wrap_wifi_tls(wifi_manager.wifi_socket, host) - + # 设置非阻塞模式(用于接收数据) wifi_manager.wifi_socket.setblocking(False) # 加快消息发送 wifi_manager.wifi_socket.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1) - + self._tcp_connected = True if use_ssl: self.logger.info("[WIFI-TCP] TLS 连接已建立") else: self.logger.info("[WIFI-TCP] TCP 连接已建立") - + # 启动 WiFi 质量后台检测 self._start_wifi_quality_monitor() - + return True - + except Exception as e: self.logger.error(f"[WIFI-TCP] 连接失败: {e}") - + if wifi_manager.wifi_socket: try: wifi_manager.wifi_socket.close() except: pass wifi_manager.wifi_socket = None - + return False - + def _connect_tcp_via_4g(self): """通过4G模块建立TCP连接(支持按手册绑定 SSL)""" link_id = getattr(config, "TCP_LINK_ID", 0) @@ -768,7 +781,7 @@ class NetworkManager: self._tcp_connected = True return True return False - + def _check_wifi_connection(self): """检查WiFi TCP连接是否仍然有效""" if not wifi_manager.wifi_socket: @@ -803,7 +816,7 @@ class NetworkManager: err = getattr(e, "errno", None) if err in (11, 35, 10035): # EAGAIN/EWOULDBLOCK on linux/mac/win return True - + # 某些平台会把"无数据可读/超时"抛成 socket.timeout / TimeoutError,errno 可能为 None, # 这不代表断线:视为 benign,交给真正的 send/recv 去判定断线。 if (err is None) and (("timed out" in str(e).lower()) or isinstance(e, (TimeoutError, socket.timeout))): @@ -812,7 +825,7 @@ class NetworkManager: except: pass return True - + # 某些嵌入式 socket 实现可能不支持 MSG_PEEK/MSG_DONTWAIT,或返回 EINVAL/ENOTSUP。 # 这种情况不代表断线:选择"无法检测但不判死",交给真正的 send/recv 去触发断线处理。 # 常见:EINVAL(22), ENOTSUP(95), EOPNOTSUPP(95), EINTR(4) @@ -822,13 +835,13 @@ class NetworkManager: except: pass return True - + # 记录真实错误,便于定位 try: self.logger.error(f"[WIFI-TCP] conncheck failed errno={err}: {e}") except: pass - + # 明确的"连接不可用"错误才判定断线并清理 # 常见:ENOTCONN(107), ECONNRESET(104), EPIPE(32), EBADF(9) if err in (9, 32, 104, 107, 10054, 10057): @@ -839,7 +852,7 @@ class NetworkManager: wifi_manager.wifi_socket = None self._tcp_connected = False return False - + # socket已断开或不可用,清理 try: wifi_manager.wifi_socket.close() @@ -856,25 +869,25 @@ class NetworkManager: wifi_manager.wifi_socket = None self._tcp_connected = False return False - + def disconnect_server(self): """断开TCP连接""" if self._tcp_connected: self.logger.info("与服务器断开链接") - + if self._network_type == "wifi": self._disconnect_tcp_via_wifi() elif self._network_type == "4g": self._disconnect_tcp_via_4g() - + self._tcp_connected = False self._network_type = None - + def _disconnect_tcp_via_wifi(self): """断开 WiFi TCP 连接并停止监测""" # 关闭wifi检测 self._stop_wifi_quality_monitor() - + # 再关闭 socket with wifi_manager.wifi_socket_lock: if wifi_manager.wifi_socket: @@ -883,7 +896,7 @@ class NetworkManager: except: pass wifi_manager.wifi_socket = None - + def _disconnect_tcp_via_4g(self): link_id = getattr(config, "TCP_LINK_ID", 0) if not self._uart4g_lock.acquire(timeout=2000): @@ -894,7 +907,6 @@ class NetworkManager: finally: self._uart4g_lock.release() - def tcp_send_raw(self, data: bytes, max_retries=2) -> bool: """ 统一的TCP发送接口(自动选择WiFi或4G) @@ -908,7 +920,7 @@ class NetworkManager: """ if not self._tcp_connected: return False - + # 根据网络类型选择发送方式 if self._network_type == "wifi": # 先快速校验 WiFi socket 是否仍有效,避免卡在半开连接上 @@ -921,12 +933,12 @@ class NetworkManager: else: self.logger.error("[NET] 未选择网络类型,无法发送数据") return False - + def _tcp_send_raw_via_wifi(self, data: bytes, max_retries=2) -> bool: """通过WiFi socket发送TCP数据""" if not wifi_manager.wifi_socket: return False - + with wifi_manager.wifi_socket_lock: for attempt in range(max_retries): try: @@ -942,18 +954,18 @@ class NetworkManager: raise if sent == 0: # socket连接已断开 - self.logger.warning(f"[WIFI-TCP] 发送失败,socket已断开(尝试 {attempt+1}/{max_retries})") + self.logger.warning(f"[WIFI-TCP] 发送失败,socket已断开(尝试 {attempt + 1}/{max_retries})") raise OSError("wifi socket closed (send returned 0)") total_sent += sent - + if total_sent == len(data): return True - + # 发送不完整,重试 time.sleep_ms(50) - + except OSError as e: - self.logger.error(f"[WIFI-TCP] 发送异常: {e}(尝试 {attempt+1}/{max_retries})") + self.logger.error(f"[WIFI-TCP] 发送异常: {e}(尝试 {attempt + 1}/{max_retries})") # 发送异常通常意味着连接已不可用,主动关闭以触发重连 try: wifi_manager.wifi_socket.close() @@ -963,7 +975,7 @@ class NetworkManager: self._tcp_connected = False return False except Exception as e: - self.logger.error(f"[WIFI-TCP] 未知错误: {e}(尝试 {attempt+1}/{max_retries})") + self.logger.error(f"[WIFI-TCP] 未知错误: {e}(尝试 {attempt + 1}/{max_retries})") try: wifi_manager.wifi_socket.close() except: @@ -971,9 +983,9 @@ class NetworkManager: wifi_manager.wifi_socket = None self._tcp_connected = False return False - + return False - + def _tcp_send_raw_via_4g(self, data: bytes, max_retries=2) -> bool: link_id = getattr(config, "TCP_LINK_ID", 0) if not self._uart4g_lock.acquire(timeout=2000): @@ -1047,13 +1059,12 @@ class NetworkManager: return False else: self.logger.info(f"[4G-TCP] MSSLCHECK response: {r}") - + r = hardware_manager.at_client.send(f'AT+MSSLLIST=1', "OK", 3000) self.logger.info(f"[4G-TCP] AT+MSSLLIST=1 response: {r}") r = hardware_manager.at_client.send(f'AT+MSSLCERTRD="{cert_filename}"', "OK", 3000) self.logger.info(f"[4G-TCP] AT+MSSLCERTRD=\"{cert_filename}\" response: {r}") - # 3) 引用根证书 r = hardware_manager.at_client.send(f'AT+MSSLCFG="cert",{ssl_id},"{cert_filename}"', "OK", 3000) @@ -1064,7 +1075,7 @@ class NetworkManager: # 4) 绑定 TCP 通道到 ssl_id,并启用 r = hardware_manager.at_client.send(f'AT+MIPCFG="ssl",{link_id},{ssl_id},1', "OK", 3000) - self.logger.info(f"[4G-TCP] AT+MIPCFG=\"ssl\",{link_id},{ssl_id},1 response: {r}") + self.logger.info(f"[4G-TCP] AT+MIPCFG=\"ssl\",{link_id},{ssl_id},1 response: {r}") if "OK" not in r: self.logger.error(f"[4G-TCP] MIPCFG(ssl) failed, response: {r}") return False @@ -1083,13 +1094,13 @@ class NetworkManager: """ if not wifi_manager.wifi_socket: return b"" - + try: # 这里保持 socket 为非阻塞模式(连接时已 setblocking(False))。 # 不要反复 settimeout(),否则会把 socket 切回"阻塞+超时",并导致 conncheck 误报 timed out。 data = wifi_manager.wifi_socket.recv(4096) # 每次最多接收4KB(无数据会抛 BlockingIOError) return data - + except BlockingIOError: # 无数据可读是正常的 return b"" @@ -1112,7 +1123,8 @@ class NetworkManager: self.logger.error(f"[WIFI-TCP] 接收数据异常: {e}") return b"" - def _upload_log_file(self, upload_url, wifi_ssid=None, wifi_password=None, include_rotated=True, max_files=None, archive_format="tgz"): + def _upload_log_file(self, upload_url, wifi_ssid=None, wifi_password=None, include_rotated=True, max_files=None, + archive_format="tgz"): """上传日志文件到指定URL Args: @@ -1130,14 +1142,14 @@ class NetworkManager: import shutil from datetime import datetime import glob - + try: # 检查 WiFi 连接状态,如果未连接则尝试连接 if not self.is_wifi_connected(): if wifi_ssid and wifi_password: self.logger.info(f"[LOG_UPLOAD] WiFi 未连接,尝试连接 WiFi: {wifi_ssid}") self.safe_enqueue({"result": "log_upload_connecting_wifi", "ssid": wifi_ssid}, 2) - + # 连接前先把“目标上传 URL”作为可达性验证目标,只有验证通过才落盘保存 SSID/PASS try: from urllib.parse import urlparse @@ -1161,7 +1173,7 @@ class NetworkManager: "detail": error }, 2) return - + self.logger.info(f"[LOG_UPLOAD] WiFi 连接成功,IP: {ip}") else: self.logger.warning("[LOG_UPLOAD] WiFi 未连接且未提供 WiFi 凭证,无法上传日志") @@ -1173,17 +1185,17 @@ class NetworkManager: return else: self.logger.info("[LOG_UPLOAD] WiFi 已连接,跳过连接步骤") - + self.logger.info(f"[LOG_UPLOAD] 开始上传日志文件...") - + # 获取日志文件路径 log_file_path = config.LOG_FILE # /maixapp/apps/t11/app.log - + if not os.path.exists(log_file_path): self.logger.error(f"[LOG_UPLOAD] 日志文件不存在: {log_file_path}") self.safe_enqueue({"result": "log_upload_failed", "reason": "log_file_not_found"}, 2) return - + # 生成带时间戳的文件名(归档) timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") device_id = self._device_id or "unknown" @@ -1245,7 +1257,8 @@ class NetworkManager: staged_paths.append(dst) except Exception as e: self.logger.error(f"[LOG_UPLOAD] 复制日志快照失败: {e}") - self.safe_enqueue({"result": "log_upload_failed", "reason": "snapshot_failed", "detail": str(e)[:100]}, 2) + self.safe_enqueue({"result": "log_upload_failed", "reason": "snapshot_failed", "detail": str(e)[:100]}, + 2) try: shutil.rmtree(staging_dir) except: @@ -1273,7 +1286,8 @@ class NetworkManager: self.logger.info(f"[LOG_UPLOAD] 日志压缩包已生成: {archive_path}") except Exception as e: self.logger.error(f"[LOG_UPLOAD] 打包压缩失败: {e}") - self.safe_enqueue({"result": "log_upload_failed", "reason": "archive_failed", "detail": str(e)[:100]}, 2) + self.safe_enqueue({"result": "log_upload_failed", "reason": "archive_failed", "detail": str(e)[:100]}, + 2) try: shutil.rmtree(staging_dir) except: @@ -1294,26 +1308,26 @@ class NetworkManager: with open(archive_path, 'rb') as f: mime = "application/gzip" if archive_format == "tgz" else "application/zip" files = {'file': (archive_filename, f, mime)} - + # 添加额外的头部信息 headers = { 'User-Agent': 'Archery-Device/1.0', 'X-Device-ID': device_id, } - + # 如果是 ngrok-free.dev,添加绕过警告页面的头 if 'ngrok-free.dev' in upload_url or 'ngrok.io' in upload_url: headers['ngrok-skip-browser-warning'] = 'true' - + self.logger.info(f"[LOG_UPLOAD] 正在上传到: {upload_url}") - + # 禁用 SSL 警告(用于自签名证书或 SSL 兼容性问题) import urllib3 urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) - + # 发送请求,verify=False 跳过 SSL 证书验证(解决 MaixCAM SSL 兼容性问题) response = requests.post(upload_url, files=files, headers=headers, timeout=60, verify=False) - + if response.status_code in (200, 201, 204): self.logger.info(f"[LOG_UPLOAD] 上传成功! 状态码: {response.status_code}") self.safe_enqueue({ @@ -1322,20 +1336,21 @@ class NetworkManager: "status_code": response.status_code }, 2) else: - self.logger.error(f"[LOG_UPLOAD] 上传失败! 状态码: {response.status_code}, 响应: {response.text[:200]}") + self.logger.error( + f"[LOG_UPLOAD] 上传失败! 状态码: {response.status_code}, 响应: {response.text[:200]}") self.safe_enqueue({ "result": "log_upload_failed", "reason": f"http_{response.status_code}", "detail": response.text[:100] }, 2) - + # 清理临时文件 try: os.remove(archive_path) self.logger.debug(f"[LOG_UPLOAD] 临时文件已删除: {archive_path}") except Exception as e: self.logger.warning(f"[LOG_UPLOAD] 删除临时文件失败: {e}") - + except requests.exceptions.Timeout: self.logger.error("[LOG_UPLOAD] 上传超时") self.safe_enqueue({"result": "log_upload_failed", "reason": "timeout"}, 2) @@ -1458,7 +1473,8 @@ class NetworkManager: except Exception as e: return None, f"prepare_exception: {e}" - def _upload_log_file_v2(self, upload_url, upload_token, key, outlink="", include_rotated=True, max_files=None, archive_format="tgz"): + def _upload_log_file_v2(self, upload_url, upload_token, key, outlink="", include_rotated=True, max_files=None, + archive_format="tgz"): """上传日志到 Qiniu(支持 WiFi 和 4G 双路径) 流程:准备日志归档 -> 自动检测网络 -> WiFi(requests) 或 4G(AT命令) 上传 @@ -1681,20 +1697,20 @@ class NetworkManager: def tcp_main(self): """TCP 主通信循环:登录、心跳、处理指令、发送数据""" import _thread - + self.logger.info("[NET] TCP主线程启动") - + send_hartbeat_fail_count = 0 last_charging_check = 0 CHARGING_CHECK_INTERVAL = 5000 # 5秒检查一次充电状态 - + while True: try: # 检查充电状态(每5秒检查一次) current_time = time.ticks_ms() if current_time - last_charging_check > CHARGING_CHECK_INTERVAL: last_charging_check = current_time - + # OTA 期间不要 connect/登录/心跳/发送 try: from ota_manager import ota_manager @@ -1713,10 +1729,10 @@ class NetworkManager: # 发送登录包 vol_val = get_bus_voltage() login_data = { - "deviceId": self.device_id, - "password": self.password, - "version": config.APP_VERSION, - "vol": vol_val, + "deviceId": self.device_id, + "password": self.password, + "version": config.APP_VERSION, + "vol": vol_val, "vol_per": voltage_to_percent(vol_val) } iccid_pending_marker = self._maybe_add_iccid_to_login(login_data) @@ -1736,7 +1752,7 @@ class NetworkManager: pending_cleared = False last_heartbeat_ack_time = time.ticks_ms() last_heartbeat_send_time = time.ticks_ms() - + while True: # 如果底层连接已断开,尽快跳出内层循环触发重连/重选网络 if not self._tcp_connected: @@ -1790,16 +1806,17 @@ class NetworkManager: link_id, payload = item else: link_id, payload = 0, item - + if not logged_in: try: - self.logger.debug(f"[TCP] rx link={link_id} len={len(payload)} head={payload[:12].hex()}") + self.logger.debug( + f"[TCP] rx link={link_id} len={len(payload)} head={payload[:12].hex()}") except: pass - + # msg_type, body = self.parse_packet(payload) msg_type, body = self._netcore.parse_packet(payload) - + # 处理登录响应 if not logged_in and msg_type == 1: if body and body.get("cmd") == 1 and body.get("data") == "登录成功": @@ -1819,26 +1836,28 @@ class NetworkManager: pending_obj = json.load(f) except: pending_obj = {} - self.safe_enqueue({"result": "ota_ok", "url": pending_obj.get("url", "")}, 2) + self.safe_enqueue({"result": "ota_ok", "url": pending_obj.get("url", "")}, + 2) self.logger.info("[OTA] 已上报 ota_ok,等待心跳确认后删除 pending") except Exception as e: self.logger.error(f"[OTA] ota_ok 上报失败: {e}") else: _rx_login_fail = True break - + # 处理心跳 ACK elif logged_in and msg_type == 4: last_heartbeat_ack_time = time.ticks_ms() self.logger.debug("✅ 收到心跳确认") - + # 处理命令40(分片下载) elif logged_in and msg_type == 40: if isinstance(body, dict): t = body.get('t', 0) v = body.get('v') # 如果是第一个分片,清空之前的缓存 - if len(self._raw_line_data) == 0 or (len(self._raw_line_data) > 0 and self._raw_line_data[0].get('v') != v): + if len(self._raw_line_data) == 0 or ( + len(self._raw_line_data) > 0 and self._raw_line_data[0].get('v') != v): self._raw_line_data.clear() # 或者更简单:每次收到命令40时,如果版本号不同,清空缓存 if len(self._raw_line_data) > 0: @@ -1855,13 +1874,13 @@ class NetworkManager: file.write("\n".join(stock_array)) ota_manager.apply_ota_and_reboot(None, local_filename) else: - self.safe_enqueue({'data':{'l': len(self._raw_line_data), 'v': v}, 'cmd': 41}) + self.safe_enqueue({'data': {'l': len(self._raw_line_data), 'v': v}, 'cmd': 41}) self.logger.info(f"已下载{len(self._raw_line_data)} 全部:{t} 版本:{v}") - + elif logged_in and msg_type == 100: self.logger.info(f"[IMAGE_UPLOAD] 收到图片上传命令 {body}") if isinstance(body, dict): - + upload_url = body.get("uploadUrl") upload_token = body.get("token") shoot_id = body.get("shootId") @@ -1872,7 +1891,8 @@ class NetworkManager: # 验证必需字段 if not upload_url or not upload_token or not shoot_id: self.logger.error("[IMAGE_UPLOAD] 缺少必需参数: uploadUrl, token 或 shootId") - self.safe_enqueue({"result": "image_upload_failed", "reason": "missing_params"}, 2) + self.safe_enqueue({"result": "image_upload_failed", "reason": "missing_params"}, + 2) else: self.logger.info(f"[IMAGE_UPLOAD] 收到图片上传命令,shootId: {shoot_id}") # 查找文件名中包含 shoot_id 的图片文件(文件名格式:shot_{shoot_id}_*.bmp) @@ -1893,15 +1913,19 @@ class NetworkManager: reverse=True ) target_image = os.path.join(photo_dir, matched_images[0]) - self.logger.info(f"[IMAGE_UPLOAD] 找到匹配shootId的图片: {matched_images[0]}") + self.logger.info( + f"[IMAGE_UPLOAD] 找到匹配shootId的图片: {matched_images[0]}") else: - self.logger.warning(f"[IMAGE_UPLOAD] 未找到包含shootId={shoot_id}的图片文件") + self.logger.warning( + f"[IMAGE_UPLOAD] 未找到包含shootId={shoot_id}的图片文件") except Exception as e: self.logger.error(f"[IMAGE_UPLOAD] 查找图片失败: {e}") if not target_image: self.logger.error(f"[IMAGE_UPLOAD] 未找到shootId={shoot_id}对应的图片文件") - self.safe_enqueue({"result": "image_upload_failed", "reason": "no_image_found", "shootId": shoot_id}, 2) + self.safe_enqueue( + {"result": "image_upload_failed", "reason": "no_image_found", + "shootId": shoot_id}, 2) else: # 构建上传key ext = os.path.splitext(target_image)[1].lower() @@ -1933,14 +1957,16 @@ class NetworkManager: # 验证必需字段 if not upload_url or not upload_token or not key: self.logger.error("[LOG_UPLOAD] 缺少必需参数: uploadUrl, token 或 key") - self.safe_enqueue({"result": "log_upload_failed", "reason": "missing_params"}, 2) + self.safe_enqueue({"result": "log_upload_failed", "reason": "missing_params"}, + 2) else: self.logger.info(f"[LOG_UPLOAD] 收到日志上传命令,key: {key}") # 在新线程中执行上传,避免阻塞主循环 import _thread _thread.start_new_thread( self._upload_log_file_v2, - (upload_url, upload_token, key, outlink, include_rotated, max_files, archive_format) + (upload_url, upload_token, key, outlink, include_rotated, max_files, + archive_format) ) # 立即返回已入队确认 self.safe_enqueue({"result": "log_upload_queued"}, 2) @@ -1988,7 +2014,7 @@ class NetworkManager: if not laser_manager.calibration_active: laser_manager.turn_on_laser() time.sleep_ms(100) - hardware_manager.stop_idle_timer() # 停表 + hardware_manager.stop_idle_timer() # 停表 if not config.HARDCODE_LASER_POINT: laser_manager.start_calibration() self.safe_enqueue({"result": "calibrating"}, 2) @@ -1999,7 +2025,7 @@ class NetworkManager: from laser_manager import laser_manager laser_manager.turn_off_laser() laser_manager.stop_calibration() - hardware_manager.start_idle_timer() # 开表 + hardware_manager.start_idle_timer() # 开表 self.safe_enqueue({"result": "laser_off"}, 2) elif inner_cmd == 4: # 上报电量 voltage = get_bus_voltage() @@ -2016,19 +2042,19 @@ class NetworkManager: password = inner_data.get("password") ota_url = inner_data.get("url") mode = (inner_data.get("mode") or "").strip().lower() - + if not ota_url: self.logger.error("ota missing_url") self.safe_enqueue({"result": "missing_url"}, 2) _rx_skip_tcp_iteration = True break - + from ota_manager import ota_manager if ota_manager.update_thread_started: self.safe_enqueue({"result": "update_already_started"}, 2) _rx_skip_tcp_iteration = True break - + # 自动判断模式:如果没有明确指定,根据WiFi连接状态和凭证决定 if mode not in ("4g", "wifi"): self.logger.info("ota missing mode, auto-detecting...") @@ -2040,13 +2066,15 @@ class NetworkManager: # 只有同时满足:WiFi已连接 且 提供了WiFi凭证,才使用WiFi if self.is_wifi_connected() and ssid and password: mode = "wifi" - self.logger.info("ota auto-selected: wifi (WiFi connected and credentials provided)") + self.logger.info( + "ota auto-selected: wifi (WiFi connected and credentials provided)") else: mode = "4g" - self.logger.info("ota auto-selected: 4g (WiFi not available or no credentials)") - + self.logger.info( + "ota auto-selected: 4g (WiFi not available or no credentials)") + hardware_manager.stop_idle_timer() # 停表,注意OTA停表之后,就没有再开表,因为OTA后面会重启,会重新开表 - + if mode == "4g": ota_manager._set_ota_url(ota_url) # 记录 OTA URL,供命令7使用 ota_manager._start_update_thread() @@ -2059,10 +2087,12 @@ class NetworkManager: self.logger.info(f"ssid: {ssid}") self.logger.info(f"password: {password}") ota_manager._start_update_thread() - _thread.start_new_thread(ota_manager.handle_wifi_and_update, (ssid, password, ota_url)) + _thread.start_new_thread(ota_manager.handle_wifi_and_update, + (ssid, password, ota_url)) elif inner_cmd == 6: try: - ip = os.popen("ifconfig wlan0 2>/dev/null | grep 'inet ' | awk '{print $2}'").read().strip() + ip = os.popen( + "ifconfig wlan0 2>/dev/null | grep 'inet ' | awk '{print $2}'").read().strip() ip = ip if ip else "no_ip" except: ip = "error_getting_ip" @@ -2070,16 +2100,18 @@ class NetworkManager: elif inner_cmd == 44: # 读 4G 本机号码(AT+CNUM) cnum = self.get_4g_phone_number() self.logger.info(f"4G 本机号码: {cnum}") - self.safe_enqueue({"result": "cnum", "number": cnum if cnum is not None else ""}, 2) + self.safe_enqueue( + {"result": "cnum", "number": cnum if cnum is not None else ""}, 2) elif inner_cmd == 45: # 读 MCCID(AT+MCCID) mccid = self.get_4g_mccid() self.logger.info(f"4G MCCID: {mccid}") - self.safe_enqueue({"result": "mccid", "mccid": mccid if mccid is not None else ""}, 2) + self.safe_enqueue( + {"result": "mccid", "mccid": mccid if mccid is not None else ""}, 2) elif inner_cmd == 41: self.logger.info(f"[TEST] 收到TCP射箭触发命令, {time.time()}") self._manual_trigger_flag = True self.safe_enqueue({"result": "trigger_ack"}, 2) - hardware_manager.start_idle_timer() # 重新计时 + hardware_manager.start_idle_timer() # 重新计时 elif inner_cmd == 42: # 关机命令 self.logger.info("[SHUTDOWN] 收到TCP关机命令,准备关机...") self.safe_enqueue({"result": "shutdown_ack"}, 2) @@ -2106,19 +2138,21 @@ class NetworkManager: include_rotated = inner_data.get("include_rotated", True) max_files = inner_data.get("max_files") archive_format = inner_data.get("archive", "tgz") # tgz 或 zip - + hardware_manager.start_idle_timer() # 重新计时 - + if not upload_url: self.logger.error("[LOG_UPLOAD] 缺少 url 参数") - self.safe_enqueue({"result": "log_upload_failed", "reason": "missing_url"}, 2) + self.safe_enqueue({"result": "log_upload_failed", "reason": "missing_url"}, + 2) else: self.logger.info(f"[LOG_UPLOAD] 收到日志上传命令,目标URL: {upload_url}") # 在新线程中执行上传,避免阻塞主循环 import _thread _thread.start_new_thread( self._upload_log_file, - (upload_url, wifi_ssid, wifi_password, include_rotated, max_files, archive_format) + (upload_url, wifi_ssid, wifi_password, include_rotated, max_files, + archive_format) ) elif inner_cmd == 200: self.logger.info("[LASER] cmd200 在后台线程执行检测") @@ -2128,7 +2162,7 @@ class NetworkManager: self.logger.info("[New Ota] cmd300 在后台线程执行OTA") import _thread _thread.start_new_thread(self._cmd300_ota, (data_obj,)) - else: # data的结构不是 dict + else: # data的结构不是 dict self.logger.info(f"[NET] body={body}, {time.time()}") else: self.logger.info(f"[NET] 未知数据 {body}, {time.time()}") @@ -2181,8 +2215,9 @@ class NetworkManager: current_time = time.ticks_ms() if logged_in and current_time - last_heartbeat_send_time > config.HEARTBEAT_INTERVAL * 1000: vol_val = get_bus_voltage() - if not self.tcp_send_raw(self._netcore.make_packet(4, {"vol": vol_val, "vol_per": voltage_to_percent(vol_val)})): - # if not self.tcp_send_raw(self.make_packet(4, {"vol": vol_val, "vol_per": voltage_to_percent(vol_val)})): + if not self.tcp_send_raw( + self._netcore.make_packet(4, {"vol": vol_val, "vol_per": voltage_to_percent(vol_val)})): + # if not self.tcp_send_raw(self.make_packet(4, {"vol": vol_val, "vol_per": voltage_to_percent(vol_val)})): send_hartbeat_fail_count += 1 # 短暂波动可能导致一次发送失败:连续失败达到阈值才重连,避免重连风暴 self.logger.error(f"心跳发送失败({send_hartbeat_fail_count}/3),准备重试") @@ -2228,7 +2263,7 @@ class NetworkManager: self._tcp_connected = False self.logger.error("连接异常,2秒后重连...") time.sleep_ms(2000) - + except Exception as e: # TCP主循环的顶层异常捕获,防止线程静默退出 self.logger.error(f"[NET] TCP主循环异常: {e}") @@ -2241,38 +2276,44 @@ class NetworkManager: # 创建全局单例实例 network_manager = NetworkManager() + # ==================== 向后兼容的函数接口 ==================== def tcp_main(): """TCP主循环(向后兼容接口)""" return network_manager.tcp_main() + def read_device_id(): """读取设备ID(向后兼容接口)""" return network_manager.read_device_id() + def safe_enqueue(data_dict, msg_type=2, high=False): """线程安全地加入队列(向后兼容接口)""" return network_manager.safe_enqueue(data_dict, msg_type, high) + def connect_server(): """连接服务器(向后兼容接口)""" return network_manager.connect_server() + def disconnet_server(): """断开服务器连接(向后兼容接口)""" return network_manager.disconnect_server() + def is_wifi_connected(): """检查WiFi是否已连接(向后兼容接口)""" return network_manager.is_wifi_connected() + def connect_wifi(ssid, password): """连接WiFi(向后兼容接口)""" return network_manager.connect_wifi(ssid, password) + def is_server_reachable(host, port=80, timeout=5): """检查服务器是否可达(向后兼容接口)""" return network_manager.is_server_reachable(host, port, timeout) - - diff --git a/version.md b/version.md index cfa8b2c..e5ed277 100644 --- a/version.md +++ b/version.md @@ -14,4 +14,6 @@ # 1.2.13 修改wifi连接 # 1.2.14 修改了icc登录部分 # 2.15.3 新版本ota,去除ai算环数方法 - +# 2.15.4 更新版本号 +# 2.15.5 打印ota进度 +# 2.15.6 更新版本号 diff --git a/version.py b/version.py index a6d9b32..8562a3a 100644 --- a/version.py +++ b/version.py @@ -4,6 +4,6 @@ 应用版本号 每次 OTA 更新时,只需要更新这个文件中的版本号 """ -VERSION = '2.15.3' +VERSION = '2.15.6'