diff --git a/app.yaml b/app.yaml index 3cecb59..c54e599 100644 --- a/app.yaml +++ b/app.yaml @@ -1,6 +1,6 @@ id: t11 name: t11 -version: 3.0.5 +version: 3.1.15 author: t11 icon: '' desc: t11 @@ -29,6 +29,7 @@ files: - shoot_manager.py - shot_id_generator.py - target_roi_yolo.py + - tcp_messages_pb2.py - time_sync.py - triangle_positions.json - triangle_target.py diff --git a/archery_netcore.cpython-311-riscv64-linux-gnu.so b/archery_netcore.cpython-311-riscv64-linux-gnu.so index 6c1ca03..187485c 100644 Binary files a/archery_netcore.cpython-311-riscv64-linux-gnu.so and b/archery_netcore.cpython-311-riscv64-linux-gnu.so differ diff --git a/config.py b/config.py index 85474ad..c0dff24 100644 --- a/config.py +++ b/config.py @@ -50,7 +50,7 @@ WIFI_CONFIG_AP_IP = "192.168.66.1" # 与 MaixPy Wifi.start_ap 默认一 # ===== TCP over SSL(TLS) 配置 ===== USE_TCP_SSL = True # True=按手册走 MSSLCFG/MIPCFG 绑定 SSL TCP_LINK_ID = 2 # -TCP_SSL_PORT = 50006 # TLS 端口(不一定必须 443,以服务器为准) +TCP_SSL_PORT = 50007 # TLS 端口(不一定必须 443,以服务器为准) # SSL profile SSL_ID = 1 # ssl_id=1 @@ -362,7 +362,7 @@ PIN_MAPPINGS = { } # ==================== 电源配置 ==================== -AUTO_POWER_OFF_IN_SECONDS = 10 * 60 # 自动关机时间(秒),0表示不自动关机 +AUTO_POWER_OFF_IN_SECONDS = 100 * 60 # 自动关机时间(秒),0表示不自动关机 # 充电时自动关机暂时禁用;需要恢复时改为 True。 CHARGING_AUTO_POWER_OFF_ENABLED = False diff --git a/main.py b/main.py index 1822dfc..4480e66 100644 --- a/main.py +++ b/main.py @@ -13,7 +13,9 @@ from maix import camera, display, image, app, time, uart, pinmap, i2c from maix.peripheral import adc import _thread import os +import sys import json +import shutil import time as wall_time # 导入新模块 @@ -126,7 +128,7 @@ def cmd_str(): # 1. 初始化日志系统(WARNING级别,不打印/写入INFO和DEBUG日志,提高执行流畅度) import logging - logger_manager.init_logging(log_level=logging.WARNING) + logger_manager.init_logging(log_level=logging.DEBUG) logger = logger_manager.logger # 补充:因为初始化的时候,激光会亮,先关了它 @@ -254,6 +256,55 @@ def cmd_str(): # 4. 初始化设备ID(network_manager 内部会自动设置 device_id 和 password) network_manager.read_device_id() + # 4.1 检查是否有 OTA 待更新文件(从临时目录移动到实际目录) + staging_dir = f"{config.APP_DIR}/ota_staging" + if os.path.exists(staging_dir): + try: + moved_count = 0 + for root, dirs, files in os.walk(staging_dir): + for f in files: + src = os.path.join(root, f) + rel = os.path.relpath(src, staging_dir) + dest = os.path.join(config.APP_DIR, rel) + dest_dir = os.path.dirname(dest) + if dest_dir: + try: + os.makedirs(dest_dir, exist_ok=True) + except: + pass + try: + shutil.copy2(src, dest) + moved_count += 1 + except Exception as e: + if logger: + logger.error(f"[OTA] 移动文件失败 {rel}: {e}") + # 删除临时目录 + try: + shutil.rmtree(staging_dir, ignore_errors=True) + except: + pass + if logger: + logger.info(f"[OTA] 已从临时目录更新 {moved_count} 个文件,重启应用...") + # 清理硬件资源,然后重启应用 + try: + laser_manager.turn_off_laser() + except: + pass + try: + camera_manager.release() + except: + pass + try: + os.sync() + except: + pass + import sys + os.execv(sys.executable, [sys.executable, os.path.join(config.APP_DIR, "main.py")]) + return + except Exception as e: + if logger: + logger.error(f"[OTA] 处理临时目录失败: {e}") + # 5. 创建照片存储目录(如果启用图像保存或检测失败时强制保存) if config.SAVE_IMAGE_ENABLED or getattr(config, "SAVE_IMAGE_ON_FAILURE", False): photo_dir = config.PHOTO_DIR @@ -290,6 +341,7 @@ def cmd_str(): trigger_adc_val = 0 # 触发时的气压值,气压需降回此值以下才能再次触发 # 读取一次ADC初始值,防止开机时传感器已有压力导致误触发 enable_check = True + _should_reboot = False try: last_adc_val = hardware_manager.adc_obj.read() except Exception: @@ -345,6 +397,14 @@ def cmd_str(): time.sleep_ms(250) continue + # OTA 完成后需要重启,从主循环退出(由启动时 staging 检测处理重启) + if network_manager.ota_restart_pending: + network_manager.ota_restart_pending = False + _should_reboot = True + if logger: + logger.info("[MAIN] OTA重启标志已设置,退出主循环...") + break + # 不在 OTA 状态下,检测是否空闲足够长,自动关机 # print(f"[MAIN] 空闲时间: {hardware_manager.get_idle_time_in_sec() }秒") # print(f"配置关机时间:{config.AUTO_POWER_OFF_IN_SECONDS} 秒") @@ -430,10 +490,62 @@ def cmd_str(): _flush_pressure_buf("exception") except: pass - time.sleep_ms(1000) # 等待1秒后继续 - - + time.sleep_ms(1000) # 等待1秒后 continue + # 主循环退出后,如果是由 OTA 触发的,移动 staging 文件并重启应用 + if _should_reboot: + staging_dir = f"{config.APP_DIR}/ota_staging" + if os.path.exists(staging_dir): + try: + moved_count = 0 + for root, dirs, files in os.walk(staging_dir): + for f in files: + src = os.path.join(root, f) + rel = os.path.relpath(src, staging_dir) + dest = os.path.join(config.APP_DIR, rel) + dest_dir = os.path.dirname(dest) + if dest_dir: + try: + os.makedirs(dest_dir, exist_ok=True) + except: + pass + try: + shutil.copy2(src, dest) + moved_count += 1 + except Exception as e: + if logger: + logger.error(f"[OTA] 移动文件失败 {rel}: {e}") + try: + shutil.rmtree(staging_dir, ignore_errors=True) + except: + pass + if logger: + logger.info(f"[MAIN] OTA 更新完成,已应用 {moved_count} 个文件,重启应用...") + except Exception as e: + if logger: + logger.error(f"[OTA] 处理 staging 目录失败: {e}") + else: + if logger: + logger.info("[MAIN] OTA 更新完成,重启应用...") + # 清理硬件资源,然后重启应用(不重启设备) + try: + laser_manager.turn_off_laser() + except: + pass + try: + camera_manager.release() + except: + pass + try: + hardware_manager.stop_idle_timer() + except: + pass + try: + os.sync() + except: + pass + import sys + os.execv(sys.executable, [sys.executable, os.path.join(config.APP_DIR, "main.py")]) # 主程序入口 diff --git a/network.py b/network.py index 66b77df..8777486 100644 --- a/network.py +++ b/network.py @@ -13,6 +13,7 @@ import hmac import hashlib import ujson import os +import sys import threading import socket import config @@ -21,15 +22,13 @@ from hardware import hardware_manager from power import get_bus_voltage, voltage_to_percent, is_charging from logger_manager import logger_manager from wifi import wifi_manager -import subprocess -# protobuf 支持 +# protobuf 支持(纯 proto 协议,必须可用) try: import tcp_messages_pb2 as pb - _HAS_PROTO = True except ImportError: - _HAS_PROTO = False print("[NET] tcp_messages_pb2 not found, protobuf disabled") + raise def _wifi_tls_would_block(exc): @@ -77,11 +76,17 @@ class NetworkManager: self._uart4g_lock = threading.Lock() self._device_id = None self._password = None - self._raw_line_data = [] + self._manual_trigger_flag = False - # protobuf 协议支持 - self._use_proto = _HAS_PROTO # 默认启用 proto(如果可用) + # OTA 防重复:上次 OTA 完成时间戳,30秒内不重复 OTA + self._last_ota_time = 0 + self._ota_cooldown_sec = 30 + + # OTA 重启标志:OTA线程设置,主循环检测到后从主循环退出再重启 + self.ota_restart_pending = False + + # protobuf 协议(纯 proto,无 JSON 兼容) # 限制并发命令线程数 self._cmd_thread_lock = threading.Lock() @@ -201,13 +206,7 @@ class NetworkManager: 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语句)""" @@ -626,53 +625,113 @@ class NetworkManager: except Exception as e: self.logger.error(f"[LASER] cmd200 检测异常: {e}") + def _cmd5_ota(self, ota_url): + """后台线程执行 cmd5 OTA""" + hardware_manager.start_idle_timer() + self.logger.info(f"[Ota] cmd5 开始OTA: {ota_url}") + self.safe_enqueue({"result": "ota start..."}, 2) + + try: + from ota_manager import ota_manager + ok, msg = ota_manager.perform_ota(ota_url) + if ok: + self.safe_enqueue({"result": "success"}, 2) + time.sleep_ms(500) + os.execv(sys.executable, [sys.executable, os.path.join(config.APP_DIR, "main.py")]) + else: + self.logger.error(f"[ota] cmd5 失败: {msg}") + self.safe_enqueue({"result": "ota fail", "reason": msg}, 2) + except Exception as e: + self.logger.error(f"[ota] cmd5 异常: {e}") + self.safe_enqueue({"result": "ota fail", "reason": str(e)}, 2) + def _cmd300_ota(self, data_obj): - """后台线程执行 cmd300 OTA,避免阻塞主循环""" + """后台线程执行 cmd300 OTA,避免阻塞主循环 + 流程:检查WiFi → 下载ZIP → 解压覆盖项目 → 重启程序 + """ hardware_manager.start_idle_timer() inner_data = data_obj.get("data", {}) if isinstance(data_obj, dict) else {} self.logger.info(f"[New Ota] cmd300 , data: {inner_data}") - ssid = inner_data.get("ssid") - password = inner_data.get("password") ota_res_url = inner_data.get("url") - try: - for _f in ("/etc/wpa_supplicant.conf", "/boot/wpa_supplicant.conf", "/boot/wifi.ssid", "/boot/wifi.pass"): + + if not ota_res_url: + self.logger.error("[ota] cmd300 缺少 url 参数") + self.safe_enqueue({"cmd": 300, "result": "ota fail", "reason": "missing url"}, 2) + return + + # OTA 冷却期检查:防止服务器重复下发导致无限 OTA 循环 + now = time.time() + if self._last_ota_time > 0: + elapsed = int(now - self._last_ota_time) + if elapsed < self._ota_cooldown_sec: + remaining = self._ota_cooldown_sec - elapsed + self.logger.warning(f"[ota] cmd300 冷却期内,跳过 (剩余 {remaining}s)") try: - os.remove(_f) - except OSError: + pkt = self._make_send_packet(2, {"cmd": 300, "result": "ota skip", "reason": f"cooldown {remaining}s"}) + self.tcp_send_raw(pkt) + except Exception: pass - w = network.wifi.Wifi() - e = w.connect(ssid, password, wait=True, timeout=15) - 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( - { + return + + if not wifi_manager.is_wifi_connected(): + self.logger.warning("[ota] cmd300 当前未连接WiFi,拒绝OTA") + self.safe_enqueue({"cmd": 300, "result": "ota fail", "reason": "wifi not connected"}, 2) + return + + self.logger.info(f"[ota] WiFi已连接,开始OTA: {ota_res_url}") + self.safe_enqueue({"cmd": 300, "result": "ota start..."}, 2) + + def _ota_progress(phase, progress): + """OTA进度回调,通过tcp_send_raw直接发送(绕过被暂停的发送队列)""" + try: + if not self._tcp_connected: + self.logger.warning(f"[ota] 进度发送跳过: tcp未连接 phase={phase} progress={progress}") + return + pkt = self._make_send_packet(2, { "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, - ) + "result": f"ota {phase}", + "progress": progress, + "phase": phase, + }) + ok = self.tcp_send_raw(pkt) + if not ok: + self.logger.warning(f"[ota] 进度发送失败: phase={phase} progress={progress}") + except Exception as e: + self.logger.error(f"[ota] 发送进度异常: {e}") + + try: + from ota_manager import ota_manager + ok, msg = ota_manager.perform_ota(ota_res_url, progress_callback=_ota_progress) + self._last_ota_time = time.time() + if ok: + self.logger.info("[ota] OTA成功,准备重启程序...") + # 直接通过tcp发送success,不走发送队列(主循环可能未drain) + try: + pkt = self._make_send_packet(2, {"cmd": 300, "result": "success", "progress": 51, "phase": "rebooting"}) + ok_send = self.tcp_send_raw(pkt) + self.logger.info(f"[ota] success包发送结果: {ok_send}, tcp_connected={self._tcp_connected}") + except Exception as e: + self.logger.error(f"[ota] 发送success失败: {e}") + # 设置重启标志,由主循环检测到后从主循环退出再重启 + # (后台线程调 os.execv 会导致 ISP 线程残留,新进程摄像头初始化失败) + self.logger.info("[ota] 设置重启标志,等待主循环退出...") + self.ota_restart_pending = True + else: + self.logger.error(f"[ota] cmd300 失败: {msg}") + self._last_ota_time = time.time() + try: + pkt = self._make_send_packet(2, {"cmd": 300, "result": "ota fail", "reason": msg}) + self.tcp_send_raw(pkt) + except Exception as e: + self.logger.error(f"[ota] 发送失败结果异常: {e}") except Exception as e: - self.logger.error(f"[ota] cmd300 失败: {e}") - self.safe_enqueue( - { - "cmd": 300, - "result": "ota fail", - "reason": str(e), - }, - 2, - ) + self.logger.error(f"[ota] cmd300 异常: {e}") + self._last_ota_time = time.time() + try: + pkt = self._make_send_packet(2, {"cmd": 300, "result": "ota fail", "reason": str(e)}) + self.tcp_send_raw(pkt) + except Exception as ex: + self.logger.error(f"[ota] 发送失败结果异常: {ex}") def _cmd600_conn_wifi(self, data_obj): hardware_manager.start_idle_timer() @@ -680,6 +739,7 @@ class NetworkManager: self.logger.info(f"[conn wifi] cmd600 , data: {inner_data}") ssid = inner_data.get("ssid") password = inner_data.get("password") + prev_network_type = self._network_type # 停止旧的WiFi质量监测(无论当前是WiFi还是4G连接) self._stop_wifi_quality_monitor() try: @@ -689,18 +749,10 @@ 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=5) 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": 600, - "result": "success", - "wifi": w.get_ip(), - }, - 2, - ) self._session_force_4g = False self.disconnect_server() self._tcp_connected = False @@ -708,102 +760,175 @@ class NetworkManager: self.logger.info("[conn wifi] WiFi已连接,等待主循环重新登录") except Exception as e: self.logger.error(f"cmd600 失败: {e}") - self.safe_enqueue( - { - "cmd": 600, - "result": "conn fail", - "reason": str(e), - }, - 2, - ) - self._switch_to_4g_due_to_poor_wifi() + # 同步发送失败结果(旧连接仍存活时直接发送) + if prev_network_type == "4g": + pkt = self._make_send_packet(2, {"cmd": 600, "result": "conn fail", "reason": str(e)}) + self.tcp_send_raw(pkt) + else: + self.safe_enqueue( + { + "cmd": 600, + "result": "conn fail", + "reason": str(e), + }, + 2, + ) + # 当前是4G在线,旧连接未断,无需切换;当前是WiFi,旧WiFi已被w.connect()断开,需回退4G + if prev_network_type == "wifi": + self._switch_to_4g_due_to_poor_wifi() def safe_enqueue(self, data_dict, msg_type=2, high=False): """线程安全地将消息加入队列(公共方法)""" self._enqueue((msg_type, data_dict), high) def _make_send_packet(self, msg_type, data_dict): - """根据协议模式构造发送数据包""" - if self._use_proto and _HAS_PROTO: - return self._make_proto_packet(msg_type, data_dict) - return self._netcore.make_packet(msg_type, data_dict) + """使用 protobuf 构造发送数据包""" + return self._make_proto_packet(msg_type, data_dict) + + def _build_logic_body(self, cmd, data_dict): + """根据 cmd 构造对应的 LogicBody oneof payload""" + d = data_dict.get("data", data_dict) if isinstance(data_dict.get("data"), dict) else data_dict + + if cmd == 1: + return pb.LogicBody( + cmd=cmd, + shoot_data=pb.ShootData( + shot_id=d.get("shot_id", ""), + x=d.get("x", 0.0), + y=d.get("y", 0.0), + r=d.get("r", 0.0), + d=d.get("d", 0.0), + adc=d.get("adc", 0.0), + target_class=str(d.get("target_class", "")), + target_class_confidence=d.get("target_class_confidence", 0.0), + d_laser=d.get("d_laser", 0.0), + d_laser_quality=d.get("d_laser_quality", 0.0), + m=d.get("m", ""), + laser_method=d.get("laser_method", ""), + target_x=d.get("target_x", 0.0), + target_y=d.get("target_y", 0.0), + offset_method=d.get("offset_method", ""), + distance_method=d.get("distance_method", ""), + ) + ) + elif cmd == 4: + return pb.LogicBody( + cmd=cmd, + battery_report=pb.BatteryReport( + battery=d.get("battery", 0.0), + voltage=d.get("voltage", 0.0), + net_type=d.get("netType", ""), + charging=d.get("charging", False), + ) + ) + elif cmd == 200: + return pb.LogicBody( + cmd=cmd, + center_point_result=pb.CenterPointResult( + result=d.get("result", ""), + x=d.get("x", 0.0), + y=d.get("y", 0.0), + ) + ) + elif cmd == 201: + return pb.LogicBody( + cmd=cmd, + center_point_set=pb.CenterPointSet( + x=d.get("x", 0.0), + y=d.get("y", 0.0), + ) + ) + + elif cmd == 300: + return pb.LogicBody( + cmd=cmd, + ota_result=pb.OtaResult( + result=d.get("result", ""), + url=d.get("wifi", ""), + progress=d.get("progress", 0), + phase=d.get("phase", ""), + ) + ) + elif cmd == 700: + return pb.LogicBody( + cmd=cmd, + charging_report=pb.ChargingReport(), + ) + else: + result_str = d.get("result", "") + if isinstance(result_str, dict): + import ujson + result_str = ujson.dumps(result_str) + elif not isinstance(result_str, str): + result_str = str(result_str) + return pb.LogicBody( + cmd=cmd, + generic_result=pb.GenericResult(result=result_str), + ) def _make_proto_packet(self, msg_type, data_dict): """使用 protobuf 序列化构造数据包""" - try: - if msg_type == 1: - # 登录消息 - msg = pb.LoginRequest( - device_id=data_dict.get("deviceId", ""), - password=data_dict.get("password", ""), - if_admin=data_dict.get("ifAdmin", False), - version=data_dict.get("version", ""), - vol=data_dict.get("vol", 0), - vol_per=data_dict.get("vol_per", 0), - iccid=data_dict.get("iccid", ""), - ) - elif msg_type == 4: - # 心跳消息 - msg = pb.Heartbeat( - t=data_dict.get("t", 0), - vol=data_dict.get("vol", 0), - vol_per=data_dict.get("vol_per", 0), - ) - elif msg_type == 2: - # 业务逻辑消息 - cmd = data_dict.get("cmd", 0) - inner_data = {k: v for k, v in data_dict.items() if k != "cmd"} - data_bytes = json.dumps(inner_data).encode("utf-8") if inner_data else b"" - msg = pb.LogicBody(cmd=cmd, data=data_bytes) - else: - # 其他消息类型,回退到 JSON - return self._netcore.make_packet(msg_type, data_dict) + if msg_type == 1: + msg = pb.LoginRequest( + device_id=data_dict.get("deviceId", ""), + password=data_dict.get("password", ""), + if_admin=data_dict.get("ifAdmin", False), + version=data_dict.get("version", ""), + vol=data_dict.get("vol", 0), + vol_per=data_dict.get("vol_per", 0), + iccid=data_dict.get("iccid", ""), + ) + elif msg_type == 4: + msg = pb.Heartbeat( + t=data_dict.get("t", 0), + vol=data_dict.get("vol", 0), + vol_per=data_dict.get("vol_per", 0), + ) + elif msg_type == 2: + cmd = data_dict.get("cmd", 0) + msg = self._build_logic_body(cmd, data_dict) + else: + return b"" - body_bytes = msg.SerializeToString() - return self._netcore.make_packet_pb(msg_type, body_bytes) - except Exception as e: - self.logger.error(f"[NET] protobuf 序列化失败,回退到 JSON: {e}") - return self._netcore.make_packet(msg_type, data_dict) + body_bytes = msg.SerializeToString() + return self._netcore.make_packet_pb(msg_type, body_bytes) def _parse_recv(self, payload): """解析接收的数据包,返回 (msg_type, body_dict)""" - if self._use_proto and _HAS_PROTO: - msg_type, body_bytes = self._netcore.parse_packet_raw(payload) - if msg_type is None: - return None, None - try: - body_dict = self._parse_proto_body(msg_type, body_bytes) - return msg_type, body_dict - except Exception as e: - self.logger.error(f"[NET] protobuf 反序列化失败: {e}") - # 回退到 JSON 解析 - return self._netcore.parse_packet(payload) - else: - return self._netcore.parse_packet(payload) + msg_type, body_bytes = self._netcore.parse_packet_raw(payload) + if msg_type is None: + return None, None + try: + body_dict = self._parse_proto_body(msg_type, body_bytes) + return msg_type, body_dict + except Exception as e: + self.logger.error(f"[NET] protobuf 反序列化失败: {e}") + return None, None def _parse_proto_body(self, msg_type, body_bytes): """将 protobuf body bytes 反序列化为 dict""" if msg_type == 1: msg = pb.LoginResponse() msg.ParseFromString(body_bytes) - return {"cmd": msg.cmd, "data": msg.data} + return {"cmd": msg.code, "data": msg.msg} elif msg_type == 4: - # 心跳 ACK 通常无 body return {} elif msg_type == 2: msg = pb.LogicBody() msg.ParseFromString(body_bytes) result = {"cmd": msg.cmd} - if msg.data: - try: - result["data"] = json.loads(msg.data.decode("utf-8")) - except: - result["data"] = {"raw": msg.data.hex()} + payload_name = msg.WhichOneof('payload') + if payload_name: + payload_msg = getattr(msg, payload_name) + data = {} + for field in payload_msg.DESCRIPTOR.fields: + val = getattr(payload_msg, field.name) + if isinstance(val, bytes): + val = val.hex() + data[field.name] = val + result["data"] = data return result - elif msg_type == 40: - msg = pb.OtaFragment() - msg.ParseFromString(body_bytes) - return {"l": msg.l, "d": msg.d, "t": msg.t, "v": msg.v} + elif msg_type == 100: msg = pb.ImageUploadCommand() msg.ParseFromString(body_bytes) @@ -813,11 +938,7 @@ class NetworkManager: msg.ParseFromString(body_bytes) return {"uploadUrl": msg.upload_url, "token": msg.token, "key": msg.key, "outlink": msg.outlink, "archive": msg.archive} else: - # 未知类型,尝试 JSON 解析 - try: - return json.loads(body_bytes.decode("utf-8")) - except: - return {"raw": body_bytes.hex()} + return {"raw": body_bytes.hex()} def connect_server(self): """ @@ -1941,6 +2062,11 @@ class NetworkManager: time.sleep_ms(200) continue + # OTA 完成后需要重启,从主循环退出(由 main.py 执行重启) + if self.ota_restart_pending: + self.logger.info("[ota] 主循环退出,准备重启...") + break + if not self.connect_server(): time.sleep_ms(1000) continue @@ -1950,7 +2076,7 @@ class NetworkManager: login_data = { "deviceId": self.device_id, "password": self.password, - "version": config.APP_VERSION + ("+proto" if self._use_proto else ""), + "version": config.APP_VERSION, "vol": vol_val, "vol_per": voltage_to_percent(vol_val) } @@ -1987,6 +2113,10 @@ class NetworkManager: time.sleep_ms(200) continue + # OTA 完成后需要重启,跳出内层循环 + if self.ota_restart_pending: + break + # 接收数据(根据网络类型选择接收方式) # WiFi 粘包:一次 recv 可能含多条完整包;也可能缓冲里已有完整包但本轮 recv 超时为空 rx_items = [] @@ -2037,7 +2167,7 @@ class NetworkManager: # 处理登录响应 if not logged_in and msg_type == 1: - if body and body.get("cmd") == 1 and body.get("data") == "登录成功": + if body and body.get("cmd") == 0 and body.get("data") == "登录成功": logged_in = True last_heartbeat_ack_time = time.ticks_ms() self.logger.info("登录成功") @@ -2068,32 +2198,7 @@ class NetworkManager: 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): - self._raw_line_data.clear() - # 或者更简单:每次收到命令40时,如果版本号不同,清空缓存 - if len(self._raw_line_data) > 0: - first_v = self._raw_line_data[0].get('v') - if first_v and first_v != v: - self._raw_line_data.clear() - self._raw_line_data.append(body) - if len(self._raw_line_data) >= int(t): - self.logger.info(f"下载完成") - from ota_manager import ota_manager - stock_array = list(map(lambda x: x.get('d'), self._raw_line_data)) - local_filename = config.LOCAL_FILENAME - with open(local_filename, 'w', encoding='utf-8') as file: - 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.logger.info(f"已下载{len(self._raw_line_data)} 全部:{t} 版本:{v}") + elif logged_in and msg_type == 100: self.logger.info(f"[IMAGE_UPLOAD] 收到图片上传命令 {body}") @@ -2186,215 +2291,151 @@ class NetworkManager: ) # 立即返回已入队确认 self.safe_enqueue({"result": "log_upload_queued"}, 2) - elif logged_in and msg_type == 201: - if self.logger: - self.logger.info(f"[LASER] cmd201:{body}") - raw_x = body.get("x") - raw_y = body.get("y") - try: - from laser_manager import laser_manager - ix, iy = laser_manager.set_hardcoded_laser_point( - raw_x, raw_y - ) - self.safe_enqueue( - { - "cmd": 201, - "result": "laser_point_set", - "x": ix, - "y": iy, - }, - 2, - ) - self.logger.info( - f"[LASER] cmd201 硬编码激光点=({ix}, {iy})" - ) - except Exception as e: - self.logger.error(f"[LASER] cmd201 失败: {e}") - self.safe_enqueue( - { - "cmd": 201, - "result": "laser_point_set_failed", - "reason": str(e), - }, - 2, - ) - hardware_manager.start_idle_timer() - # 处理业务指令 + # 处理业务指令(纯 proto: cmd 在 body 顶层) elif logged_in and isinstance(body, dict): - inner_cmd = None - data_obj = body.get("data") - if isinstance(data_obj, dict): - inner_cmd = data_obj.get("cmd") - if inner_cmd == 2: # 开启激光并校准 - from laser_manager import laser_manager - if not laser_manager.calibration_active: - laser_manager.turn_on_laser() - time.sleep_ms(100) - hardware_manager.stop_idle_timer() # 停表 - if not config.HARDCODE_LASER_POINT: - laser_manager.start_calibration() - self.safe_enqueue({"result": "calibrating"}, 2) - else: - # 写死的逻辑,不需要校准激光点 - self.safe_enqueue({"result": "laser pos set by hard code"}, 2) - elif inner_cmd == 3: # 关闭激光 - from laser_manager import laser_manager - laser_manager.turn_off_laser() - laser_manager.stop_calibration() - hardware_manager.start_idle_timer() # 开表 - self.safe_enqueue({"result": "laser_off"}, 2) - elif inner_cmd == 4: # 上报电量 - voltage = get_bus_voltage() - battery_percent = voltage_to_percent(voltage) - battery_data = { - "battery": battery_percent, - "voltage": round(float(voltage), 3), - "netType": self.network_type, - } - self.safe_enqueue(battery_data, 2) - self.logger.info(f"电量上报: {battery_percent}% 充电: {is_charging()}") - if getattr(config, "CHARGING_AUTO_POWER_OFF_ENABLED", False) and is_charging(): - self.safe_enqueue( - { - "cmd": 700, - }, - 2, - ) - elif inner_cmd == 700: - self.logger.warning("服务器下发关机!!!") - exit(-1) - elif inner_cmd == 5: # OTA 升级 - inner_data = data_obj.get("data", {}) if isinstance(data_obj, dict) else {} - ssid = inner_data.get("ssid") - 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...") - # 若本次会话已锁定 4G,则 OTA 自动也走 4G,避免后续回切导致体验不一致 - if self._session_force_4g: - mode = "4g" - self.logger.info("ota auto-selected: 4g (session locked on 4g)") - else: - # 只有同时满足: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)") - else: - mode = "4g" - 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() - self._spawn_cmd_thread(ota_manager.direct_ota_download_via_4g, (ota_url,)) - else: # mode == "wifi" - if not ssid or not password: - self.logger.error("ota wifi mode requires ssid and password") - self.safe_enqueue({"result": "missing_ssid_or_password"}, 2) - else: - self.logger.info(f"ssid: {ssid}") - self.logger.info(f"password: {password}") - ota_manager._start_update_thread() - self._spawn_cmd_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 = ip if ip else "no_ip" - except: - ip = "error_getting_ip" - self.safe_enqueue({"result": "current_ip", "ip": ip}, 2) - 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) - 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) - 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() # 重新计时 - elif inner_cmd == 42: # 关机命令 - self.logger.info("[SHUTDOWN] 收到TCP关机命令,准备关机...") - self.safe_enqueue({"result": "shutdown_ack"}, 2) - time.sleep_ms(1000) - self.disconnect_server() - # 尝试关闭4G模块 - try: - with self.get_uart_lock(): - hardware_manager.at_client.send("AT+CFUN=0", "OK", 5000) - except: - pass - time.sleep_ms(2000) - os.system("sync") # 刷新文件系统缓存到磁盘,防止数据丢失 - time.sleep_ms(500) - # os.system("poweroff") - hardware_manager.power_off() - return - elif inner_cmd == 43: # 上传日志命令 - # 格式: {"cmd":43, "data":{"ssid":"xxx","password":"xxx","url":"xxx", ...}} - inner_data = data_obj.get("data", {}) - upload_url = inner_data.get("url") - wifi_ssid = inner_data.get("ssid") - wifi_password = inner_data.get("password") - 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) + cmd = body.get("cmd") + data_obj = body.get("data") or {} + if cmd == 2: # AimRequest 开启激光并校准 + from laser_manager import laser_manager + if not laser_manager.calibration_active: + laser_manager.turn_on_laser() + time.sleep_ms(100) + hardware_manager.stop_idle_timer() + if not config.HARDCODE_LASER_POINT: + laser_manager.start_calibration() + self.safe_enqueue({"result": "calibrating"}, 2) else: - self.logger.info(f"[LOG_UPLOAD] 收到日志上传命令,目标URL: {upload_url}") - # 在新线程中执行上传,避免阻塞主循环 + self.safe_enqueue({"result": "laser pos set by hard code"}, 2) + elif cmd == 3: # CloseAimRequest 关闭激光 + from laser_manager import laser_manager + laser_manager.turn_off_laser() + laser_manager.stop_calibration() + hardware_manager.start_idle_timer() + self.safe_enqueue({"result": "laser_off"}, 2) + elif cmd == 4: # GetBatteryRequest 上报电量 + voltage = get_bus_voltage() + battery_percent = voltage_to_percent(voltage) + charging = is_charging() + self.safe_enqueue({ + "cmd": 4, + "battery": battery_percent, + "voltage": round(float(voltage), 3), + "netType": self.network_type, + "charging": charging, + }, 2) + self.logger.info(f"电量上报: {battery_percent}% 充电: {charging}") + elif cmd == 700: + self.logger.warning("服务器下发关机!!!") + exit(-1) + elif cmd == 5: # OtaRequest OTA 升级 + ota_url = data_obj.get("url", "") + + 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 + + if not wifi_manager.is_wifi_connected(): + self.logger.warning("[ota] cmd5 当前未连接WiFi,拒绝OTA") + self.safe_enqueue({"result": "ota fail", "reason": "wifi not connected"}, 2) + _rx_skip_tcp_iteration = True + break + + hardware_manager.stop_idle_timer() + self._spawn_cmd_thread(self._cmd5_ota, (ota_url,)) + elif cmd == 41: # Ota4gSubCodeRequest 射箭触发 + 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() + elif cmd == 42: # ShutdownCommand 关机命令 + self.logger.info("[SHUTDOWN] 收到TCP关机命令,准备关机...") + self.safe_enqueue({"result": "shutdown_ack"}, 2) + time.sleep_ms(1000) + self.disconnect_server() + try: + with self.get_uart_lock(): + hardware_manager.at_client.send("AT+CFUN=0", "OK", 5000) + except: + pass + time.sleep_ms(2000) + os.system("sync") + time.sleep_ms(500) + hardware_manager.power_off() + return + elif cmd == 44: # 读 4G 本机号码 + 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) + elif cmd == 45: # 读 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) + elif cmd == 43: # 上传日志命令 + upload_url = data_obj.get("url") + wifi_ssid = data_obj.get("ssid") + wifi_password = data_obj.get("password") + include_rotated = data_obj.get("include_rotated", True) + max_files = data_obj.get("max_files") + archive_format = data_obj.get("archive", "tgz") + + 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) + else: + self.logger.info(f"[LOG_UPLOAD] 收到日志上传命令,目标URL: {upload_url}") self._spawn_cmd_thread( self._upload_log_file, (upload_url, wifi_ssid, wifi_password, include_rotated, max_files, archive_format) ) - elif inner_cmd == 200: - self.logger.info("[LASER] cmd200 在后台线程执行检测") - self._spawn_cmd_thread(self._cmd200_detect_laser, ()) - elif inner_cmd == 300: - self.logger.info("[New Ota] cmd300 在后台线程执行OTA") - self._spawn_cmd_thread(self._cmd300_ota, (data_obj,)) - elif inner_cmd == 600: - self.logger.info("[conn wifi] cmd600 在后台线程执行连接wifi: {data_obj}") - self._spawn_cmd_thread(self._cmd600_conn_wifi, (data_obj,)) - elif inner_cmd == 601: - pass - else: # data的结构不是 dict + elif cmd == 200: # GenericResult "init_center_point" 触发激光检测 + self.logger.info("[LASER] cmd200 在后台线程执行检测") + self._spawn_cmd_thread(self._cmd200_detect_laser, ()) + elif cmd == 201: # SetCenterPointRequest 设置中心点 + if self.logger: + self.logger.info(f"[LASER] cmd201:{body}") + raw_x = data_obj.get("x") + raw_y = data_obj.get("y") + try: + from laser_manager import laser_manager + ix, iy = laser_manager.set_hardcoded_laser_point(raw_x, raw_y) + self.safe_enqueue({ + "cmd": 201, + "result": "laser_point_set", + "x": ix, + "y": iy, + }, 2) + self.logger.info(f"[LASER] cmd201 硬编码激光点=({ix}, {iy})") + except Exception as e: + self.logger.error(f"[LASER] cmd201 失败: {e}") + self.safe_enqueue({ + "cmd": 201, + "result": "laser_point_set_failed", + "reason": str(e), + }, 2) + hardware_manager.start_idle_timer() + elif cmd == 300: # OtaRequest 新版OTA + self.logger.info("[New Ota] cmd300 在后台线程执行OTA") + self._spawn_cmd_thread(self._cmd300_ota, ({"data": data_obj},)) + elif cmd == 600: # WifiConnectRequest 连接wifi + self.logger.info(f"[conn wifi] cmd600 在后台线程执行连接wifi: {data_obj}") + self._spawn_cmd_thread(self._cmd600_conn_wifi, ({"data": data_obj},)) + elif cmd == 601: + pass + else: self.logger.info(f"[NET] body={body}, {time.time()}") - else: - self.logger.info(f"[NET] 未知数据 {body}, {time.time()}") if _rx_login_fail: break if _rx_skip_tcp_iteration: diff --git a/ota_manager.py b/ota_manager.py index 1421c5e..b5a2e47 100644 --- a/ota_manager.py +++ b/ota_manager.py @@ -2,1250 +2,239 @@ # -*- coding: utf-8 -*- """ OTA管理器模块 -提供OTA升级的状态管理和主要功能封装 +流程:下载ZIP → 解压覆盖项目 → 重启应用程序 """ import binascii import hashlib -import re import threading import os -import json import shutil -from urllib.parse import urlparse, unquote import requests -from maix import time import config -from hardware import hardware_manager -from network import network_manager from logger_manager import logger_manager -from power import get_bus_voltage, voltage_to_percent - - -# 延迟导入避免循环依赖 -# from network import network_manager class OTAManager: """OTA升级管理器(单例)""" _instance = None - + def __new__(cls): if cls._instance is None: cls._instance = super(OTAManager, cls).__new__(cls) cls._instance._initialized = False return cls._instance - + def __init__(self): if self._initialized: return - - # 私有状态 - self._update_thread_started = False self._ota_in_progress = 0 self._ota_url = None - self._ota_mode = None self._lock = threading.Lock() - self._is_https = False self._initialized = True - - # ==================== 状态访问(只读属性)==================== - + @property def logger(self): - """获取 logger 对象""" return logger_manager.logger - - @property - def update_thread_started(self): - """OTA线程是否已启动""" - return self._update_thread_started - + @property def ota_in_progress(self): - """OTA是否正在进行""" with self._lock: return self._ota_in_progress > 0 - + + @property + def update_thread_started(self): + return self._ota_in_progress > 0 + @property def ota_url(self): - """当前OTA URL""" return self._ota_url - - @property - def ota_mode(self): - """当前OTA模式""" - return self._ota_mode - - # ==================== 内部状态管理方法 ==================== - - def _start_update_thread(self): - """标记OTA线程已启动(内部方法)""" - with self._lock: - if self._update_thread_started: - return False - self._update_thread_started = True - return True - - def _stop_update_thread(self): - """标记OTA线程已停止(内部方法)""" - with self._lock: - self._update_thread_started = False - - def _begin_ota(self, url=None, mode=None): - """开始OTA(增加计数,内部方法)""" + + def _begin_ota(self, url=None): with self._lock: self._ota_in_progress += 1 if url: self._ota_url = url - if mode: - self._ota_mode = mode - + def _end_ota(self): - """结束OTA(减少计数,内部方法)""" with self._lock: self._ota_in_progress = max(0, self._ota_in_progress - 1) - + def _set_ota_url(self, url): - """设置OTA URL(内部方法)""" with self._lock: self._ota_url = url - - def _set_ota_mode(self, mode): - """设置OTA模式(内部方法)""" + + def _start_update_thread(self): with self._lock: - self._ota_mode = mode + if self._ota_in_progress > 0: + return False + self._ota_in_progress += 1 + return True + def _stop_update_thread(self): + self._end_ota() - def is_archive_file(self, filename): + # ==================== 核心方法 ==================== + + def perform_ota(self, url, progress_callback=None): """ - 检查文件是否是ZIP压缩包(通过扩展名判断) - 约定:上传的代码要么是ZIP压缩包(.zip),要么是直接的PY文件(.py) - - Returns: - (is_archive, archive_type): (True/False, 'zip'/None) - """ - if not os.path.exists(filename): - return False, None - - filename_lower = filename.lower() - if filename_lower.endswith('.zip'): - self.logger.info(f"[EXTRACT] 检测到ZIP文件(扩展名: .zip)") - return True, 'zip' - - self.logger.info(f"[EXTRACT] 不是ZIP格式(扩展名: {os.path.splitext(filename)[1] or '无'})") - return False, None - - def extract_zip_archive(self, archive_path, extract_to_dir=None, target_file=None): - """ - 使用系统 unzip 命令解压ZIP文件 + 完整OTA流程:下载ZIP → 解压覆盖项目 + 调用方负责重启程序(os.execv) Args: - archive_path: ZIP文件路径 - extract_to_dir: 解压到的目录(如果为None,解压到压缩包所在目录) - target_file: 目标文件名(如'main.py'),如果指定,只提取该文件;None表示解压所有文件 + url: 固件下载地址 + progress_callback: 进度回调 fn(phase, progress),phase="downloading"/"installing",progress=0-100 Returns: - (success, extracted_dir): 成功则返回(True, 解压目录路径),失败返回(False, None) + (success: bool, message: str) """ - if extract_to_dir is None: - extract_to_dir = os.path.dirname(archive_path) or '/tmp' - - self.logger.info(f"[EXTRACT] 开始解压ZIP文件: {archive_path}") + if not url: + return False, "missing_url" + self._begin_ota(url) try: - os.makedirs(extract_to_dir, exist_ok=True) + tmp_path = f"{config.APP_DIR}/ota_tmp.zip" - if target_file: - cmd = f"unzip -q -o '{archive_path}' '{target_file}' -d '{extract_to_dir}' 2>&1" - else: - cmd = f"unzip -q -o '{archive_path}' -d '{extract_to_dir}' 2>&1" + self.logger.info(f"[OTA] 开始下载: {url}") + if progress_callback: + progress_callback("downloading", 0) + ok, msg = self._download_zip(url, tmp_path, progress_callback) + if not ok: + self.logger.error(f"[OTA] 下载失败: {msg}") + return False, msg + self.logger.info(f"[OTA] 下载完成: {msg}") + if progress_callback: + progress_callback("downloading", 50) - result = os.system(cmd) - - if result != 0: - self.logger.warning(f"[EXTRACT] 直接解压目标文件失败,尝试解压所有文件...") - cmd_all = f"unzip -q -o '{archive_path}' -d '{extract_to_dir}' 2>&1" - result_all = os.system(cmd_all) - - if result_all != 0: - self.logger.error(f"[EXTRACT] 解压失败,退出码: {result_all}") - return False, None - - return True, extract_to_dir + self.logger.info("[OTA] 开始应用更新...") + if progress_callback: + progress_callback("installing", 50) + ok, msg = self._apply_update(tmp_path, progress_callback) + if not ok: + self.logger.error(f"[OTA] 应用更新失败: {msg}") + return False, msg + self.logger.info(f"[OTA] 更新应用成功,共更新 {msg} 个文件") + if progress_callback: + progress_callback("installing", 51) + return True, "success" except Exception as e: - self.logger.error(f"[EXTRACT] 解压过程出错: {e}") - return False, None + self.logger.error(f"[OTA] 异常: {e}") + return False, str(e) + finally: + self._end_ota() - def apply_ota_and_reboot(self, ota_url=None, downloaded_file=None): + def _download_zip(self, url, save_path, progress_callback=None): """ - OTA 文件下载成功后: - 1. 备份应用目录中的所有代码文件 - 2. 如果是ZIP则解压,如果是单个文件则直接使用 - 3. 复制/覆盖所有更新文件到应用目录 - 4. 重启设备 + 下载ZIP文件(流式分块下载,支持进度回调) Args: - ota_url: OTA URL(用于记录) - downloaded_file: 下载的文件路径(如果为None,使用默认的main_tmp.py) + url: 下载地址 + save_path: 保存路径 + progress_callback: 进度回调 fn(phase, progress),progress=0-80 + + Returns: + (success: bool, message: str) """ - - # 在调用前设置状态 - if ota_url: - self._set_ota_url(ota_url) - - if downloaded_file is None: - downloaded_file = config.LOCAL_FILENAME - - ota_pending = f"{config.APP_DIR}/ota_pending.json" - - self.logger.info(f"[OTA] 准备应用OTA更新,下载文件: {downloaded_file}") - try: - if not os.path.exists(downloaded_file): - self.logger.error(f"[OTA] 错误:{downloaded_file} 不存在") - return False + response = requests.get(url, timeout=120, stream=True) + response.raise_for_status() - # ====== 第一步:如果是 AEAD 加密包,先解密成临时 zip(再走原有 unzip 流程) ====== - downloaded_file_original = downloaded_file - decrypted_tmp_zip = None - try: - magic = b"AROTAE1" # must match packager/C++ side - is_enc_ext = downloaded_file.lower().endswith((".enc", ".zip.enc")) - is_enc_magic = False - try: - with open(downloaded_file, "rb") as f: - head = f.read(len(magic)) - is_enc_magic = (head == magic) - except Exception: - is_enc_magic = False + total_size = int(response.headers.get('Content-Length', 0)) + chunk_size = 8192 + downloaded = 0 + md5_hash = hashlib.md5() - if is_enc_ext or is_enc_magic: - # Choose output zip path (same dir) - tmp_zip = downloaded_file - if tmp_zip.lower().endswith(".zip.enc"): - tmp_zip = tmp_zip[:-4] # remove ".enc" -> ".zip" - elif tmp_zip.lower().endswith(".enc"): - tmp_zip = tmp_zip[:-4] - if not tmp_zip.lower().endswith(".zip"): - tmp_zip = tmp_zip + ".zip" - else: - tmp_zip = tmp_zip + ".zip" + with open(save_path, 'wb') as f: + for chunk in response.iter_content(chunk_size=chunk_size): + if chunk: + f.write(chunk) + md5_hash.update(chunk) + downloaded += len(chunk) + if progress_callback and total_size > 0: + percent = min(int(downloaded / total_size * 50), 49) + progress_callback("downloading", percent) - decrypted_tmp_zip = tmp_zip - - # Remove stale tmp if exists - try: - if os.path.exists(decrypted_tmp_zip): - os.remove(decrypted_tmp_zip) - except Exception: - pass - - self.logger.info(f"[OTA] 检测到加密包,开始解密: {downloaded_file} -> {decrypted_tmp_zip}") - ok = False - try: - core = getattr(network_manager, "_netcore", None) - if core and hasattr(core, "decrypt_ota_file"): - ok = bool(core.decrypt_ota_file(downloaded_file, decrypted_tmp_zip)) - else: - import archery_netcore as _netcore - ok = bool(_netcore.decrypt_ota_file(downloaded_file, decrypted_tmp_zip)) - except Exception as e: - self.logger.error(f"[OTA] 解密异常: {e}") - ok = False - - if not ok or (not os.path.exists(decrypted_tmp_zip)): - self.logger.error("[OTA] 解密失败,终止更新") - return False - - downloaded_file = decrypted_tmp_zip - self.logger.info(f"[OTA] 解密成功,后续使用明文ZIP: {downloaded_file}") - except Exception as e: - self.logger.error(f"[OTA] 解密流程异常: {e}") - return False - - # 备份 - backup_base = config.BACKUP_BASE - backup_dir = None - - try: - os.makedirs(backup_base, exist_ok=True) - - counter_file = os.path.join(backup_base, ".counter") - try: - if os.path.exists(counter_file): - with open(counter_file, 'r') as f: - counter = int(f.read().strip()) + 1 - else: - counter = 1 - - with open(counter_file, 'w') as f: - f.write(str(counter)) - - backup_dir = os.path.join(backup_base, f"backup_{counter:04d}") - self.logger.info(f"[OTA] 使用备份目录: {backup_dir} (第{counter}次OTA)") - except Exception as e: - self.logger.error(f"[OTA] 生成备份目录名失败: {e},使用默认目录") - backup_dir = os.path.join(backup_base, "backup_0000") - - # 清理旧备份 - try: - backup_dirs = [] - for item in os.listdir(backup_base): - if item == ".counter": - continue - item_path = os.path.join(backup_base, item) - if os.path.isdir(item_path) and item.startswith("backup_"): - try: - dir_num_str = item.replace("backup_", "") - dir_num = int(dir_num_str) - backup_dirs.append((item, dir_num, item_path)) - except: - pass - - backup_dirs.sort(key=lambda x: x[1], reverse=True) - if len(backup_dirs) > config.MAX_BACKUPS: - for item, dir_num, item_path in backup_dirs[config.MAX_BACKUPS:]: - try: - shutil.rmtree(item_path, ignore_errors=True) - self.logger.info(f"[OTA] 已删除旧备份: {item}") - except Exception as e: - self.logger.warning(f"[OTA] 删除旧备份失败: {e}") - except Exception as e: - self.logger.warning(f"[OTA] 清理旧备份时出错: {e}") - - os.makedirs(backup_dir, exist_ok=True) - - exclude_patterns = ['.pyc', '__pycache__', '.log', 'backups', 'ota_extract', '.bak', 'download'] - backed_up_files = [] - - if os.path.exists(config.APP_DIR): - for root, dirs, files in os.walk(config.APP_DIR): - dirs[:] = [d for d in dirs if not any(ex in d for ex in exclude_patterns)] - - for f in files: - if any(ex in f for ex in exclude_patterns): - continue - - source_path = os.path.join(root, f) - rel_path = os.path.relpath(source_path, config.APP_DIR) - backup_path = os.path.join(backup_dir, rel_path) - - backup_parent = os.path.dirname(backup_path) - if backup_parent != backup_dir: - os.makedirs(backup_parent, exist_ok=True) - - try: - shutil.copy2(source_path, backup_path) - backed_up_files.append(rel_path) - except Exception as e: - self.logger.error(f"[OTA] 备份 {rel_path} 失败: {e}") - - if backed_up_files: - self.logger.info(f"[OTA] 总共备份了 {len(backed_up_files)} 个文件到 {backup_dir}") - else: - self.logger.warning(f"[OTA] 没有备份任何文件") - - except Exception as e: - self.logger.error(f"[OTA] 备份过程出错: {e}") - if not backup_dir: - backup_dir = None - - # 检查是否是ZIP压缩包 - is_archive, archive_type = self.is_archive_file(downloaded_file) - files_to_copy = [] - - if is_archive and archive_type == 'zip': - # 在解压前验证ZIP文件完整性 - try: - with open(downloaded_file, "rb") as f: - zip_header = f.read(4) - if zip_header[:2] != b'PK': - self.logger.error(f"[OTA] ZIP文件头验证失败: {zip_header.hex()}") - return False - file_size = os.path.getsize(downloaded_file) - self.logger.info(f"[OTA] ZIP文件验证通过: 大小={file_size} bytes, 头={zip_header.hex()}") - except Exception as e: - self.logger.error(f"[OTA] ZIP文件验证异常: {e}") - return False - - self.logger.info(f"[OTA] 检测到ZIP压缩包,开始解压...") - extract_dir = "/tmp/ota_extract" - try: - os.makedirs(extract_dir, exist_ok=True) - except: - extract_dir = f"{config.APP_DIR}/ota_extract" - os.makedirs(extract_dir, exist_ok=True) - - success, extracted_dir = self.extract_zip_archive( - downloaded_file, - extract_to_dir=extract_dir, - target_file=None - ) - - if success and extracted_dir and os.path.exists(extracted_dir): - for root, dirs, files in os.walk(extracted_dir): - for f in files: - source_path = os.path.join(root, f) - rel_path = os.path.relpath(source_path, extracted_dir) - files_to_copy.append((source_path, rel_path)) - - if files_to_copy: - self.logger.info(f"[OTA] 解压成功,共 {len(files_to_copy)} 个文件") - else: - self.logger.error(f"[OTA] 解压成功但未找到任何文件") - return False - else: - self.logger.error(f"[OTA] 解压失败") - return False - else: - # 单个文件更新:从下载的文件名推断目标文件名 - filename = os.path.basename(downloaded_file) - - # 如果下载的文件是 main_tmp.py,目标应该是 main.py - # 如果下载的文件是 main.py,目标也是 main.py - # 其他文件名,保持原样 - if filename == "main_tmp.py": - target_rel_path = "main.py" - else: - target_rel_path = filename - - files_to_copy = [(downloaded_file, target_rel_path)] - self.logger.info(f"[OTA] 单个文件更新: {downloaded_file} -> {target_rel_path}") - - # 复制文件 - if not files_to_copy: - self.logger.error(f"[OTA] 没有文件需要复制") - return False - - copied_files = [] - for source_path, rel_path in files_to_copy: - dest_path = os.path.join(config.APP_DIR, rel_path) - - # 检查源文件和目标文件是否是同一个文件(避免复制到自身) - if os.path.abspath(source_path) == os.path.abspath(dest_path): - self.logger.warning(f"[OTA] 源文件和目标文件相同,跳过复制: {rel_path} (文件已在正确位置)") - copied_files.append(rel_path) - continue - - dest_dir = os.path.dirname(dest_path) - if dest_dir: - try: - os.makedirs(dest_dir, exist_ok=True) - except Exception: - pass - - try: - shutil.copy2(source_path, dest_path) - copied_files.append(rel_path) - self.logger.info(f"[OTA] 已复制: {rel_path}") - except Exception as e: - self.logger.error(f"[OTA] 复制 {rel_path} 失败: {e}") - return False - - if copied_files: - self.logger.info(f"[OTA] 成功复制 {len(copied_files)} 个文件到应用目录") - - # 确保写入磁盘 try: os.sync() except: pass - time.sleep_ms(500) - # 写入 pending - try: - pending_obj = { - "ts": 0, # MaixPy time 模块没有 time() 函数,使用 0 - "url": ota_url or "", - "downloaded_file": downloaded_file, - "was_archive": is_archive, - "archive_type": archive_type if is_archive else None, - "backup_dir": backup_dir, - "updated_files": copied_files, - "restart_count": 0, - "max_restarts": 3, - } - with open(ota_pending, "w", encoding="utf-8") as f: - json.dump(pending_obj, f) - try: - os.sync() - except: - pass - except Exception as e: - self.logger.error(f"[OTA] 写入 ota_pending 失败: {e}") - - # 通知服务器(延迟导入避免循环导入) - from network import safe_enqueue - safe_enqueue({"result": "ota_applied_rebooting", "files": copied_files}, 2) - time.sleep_ms(1000) - - # 清理临时解压目录 - if is_archive and 'extract_dir' in locals(): - try: - if os.path.exists(extract_dir): - shutil.rmtree(extract_dir, ignore_errors=True) - self.logger.info(f"[OTA] 已清理临时解压目录: {extract_dir}") - except Exception as e: - self.logger.warning(f"[OTA] 清理临时目录失败(可忽略): {e}") - - # 清理下载文件 - try: - # 删除下载的文件(可能包含:原始加密包 + 临时明文zip) - files_to_remove = [] - try: - if 'downloaded_file_original' in locals() and downloaded_file_original: - files_to_remove.append(downloaded_file_original) - except Exception: - pass - try: - if 'decrypted_tmp_zip' in locals() and decrypted_tmp_zip: - files_to_remove.append(decrypted_tmp_zip) - except Exception: - pass - # 兼容:如果变量不存在,至少清理当前 downloaded_file - if not files_to_remove: - files_to_remove = [downloaded_file] - - removed_any = False - for fp in list(dict.fromkeys(files_to_remove)): - try: - if fp and os.path.exists(fp): - os.remove(fp) - removed_any = True - self.logger.info(f"[OTA] 已删除下载文件: {fp}") - except Exception as e: - self.logger.warning(f"[OTA] 删除下载文件失败(可忽略): {e}") - - # 尝试删除时间戳目录(如果为空) - try: - download_dir = os.path.dirname(files_to_remove[0] if files_to_remove else downloaded_file) - if download_dir.startswith("/tmp/download/"): - # 检查时间戳目录是否为空 - if os.path.exists(download_dir): - try: - files_in_dir = os.listdir(download_dir) - if not files_in_dir: - os.rmdir(download_dir) - self.logger.info(f"[OTA] 已删除空时间戳目录: {download_dir}") - except Exception as e: - self.logger.debug(f"[OTA] 删除时间戳目录失败(可忽略): {e}") - except Exception as e: - self.logger.debug(f"[OTA] 清理时间戳目录时出错(可忽略): {e}") - except Exception as e: - self.logger.warning(f"[OTA] 清理下载文件时出错(可忽略): {e}") - - # 重启设备 - self.logger.info("[OTA] 准备重启设备...") - os.system("reboot") - - return True - - except Exception as e: - self.logger.error(f"[OTA] apply_ota_and_reboot 异常: {e}") - import traceback - self.logger.error(traceback.format_exc()) - return False - - def get_download_timestamp_dir(self): - """ - 获取下载目录(带时间戳),格式:/tmp/download/YYYYMMDD_HHMMSS - 使用时间戳而不是日期,避免跨天问题 - - Returns: - 下载目录路径 - """ - try: - # 尝试从系统获取时间戳 - try: - # 方法1:使用系统 date 命令(精确到秒) - timestamp_str = os.popen("date +%Y%m%d_%H%M%S 2>/dev/null").read().strip() - if timestamp_str and len(timestamp_str) == 15: # YYYYMMDD_HHMMSS = 15字符 - timestamp_dir = timestamp_str - else: - raise ValueError("date command failed") - except: - # 方法2:使用 Python datetime(如果系统时间已同步) - try: - from datetime import datetime - now = datetime.now() - timestamp_dir = now.strftime("%Y%m%d_%H%M%S") - except: - # 方法3:如果都失败,使用默认时间戳 - timestamp_dir = "00000000_000000" - - download_base = "/tmp/download" - download_dir = f"{download_base}/{timestamp_dir}" - - # 确保目录存在 - try: - os.makedirs(download_dir, exist_ok=True) - except Exception as e: - self.logger.warning(f"[OTA] 创建下载目录失败: {e},使用基础目录") - download_dir = download_base - try: - os.makedirs(download_dir, exist_ok=True) - except: - pass - - return download_dir - except Exception as e: - self.logger.error(f"[OTA] 获取下载目录失败: {e},使用默认目录") - return "/tmp/download" - - def get_filename_from_url(self, url, default_name="main_tmp"): - """ - 从URL中提取文件名和扩展名,保存到带时间戳的下载目录 - - Args: - url: 下载URL - default_name: 如果无法从URL提取文件名,使用的默认名称 - - Returns: - 完整的文件路径,例如: "/tmp/download/20250108_143025/main.zip" - """ - try: - # 获取下载目录(带时间戳) - download_dir = self.get_download_timestamp_dir() - - parsed = urlparse(url) - path = parsed.path - filename = os.path.basename(path) - filename = unquote(filename) - - # 如果从URL提取到了文件名(无论是否有扩展名),都使用该文件名 - if filename and filename.strip(): - return f"{download_dir}/{filename}" - else: - # 只有在完全无法提取文件名时,才使用默认名称 - return f"{download_dir}/{default_name}" - except Exception as e: - self.logger.error(f"[OTA] 从URL提取文件名失败: {e},使用默认文件名") - download_dir = self.get_download_timestamp_dir() - return f"{download_dir}/{default_name}" - - def download_file_via_wifi(self, url, filename): - """从指定 URL 下载文件,根据文件类型自动选择文本或二进制模式,并支持MD5校验""" - try: - self.logger.info(f"正在从 {url} 下载文件...") - response = requests.get(url) - response.raise_for_status() - - # 从响应头中提取MD5(如果服务器提供) md5_b64_expected = None if 'Content-Md5' in response.headers: md5_b64_expected = response.headers['Content-Md5'].strip() - self.logger.info(f"[DOWNLOAD] 服务器提供了MD5校验值: {md5_b64_expected}") - - # 根据文件扩展名判断是否为二进制文件 - filename_lower = filename.lower() - is_binary = filename_lower.endswith(('.zip', '.zip.enc', '.enc', '.bin', '.tar', '.gz', '.exe', '.dll', '.so', '.dylib')) - - if is_binary: - # 二进制文件:使用二进制模式写入 - data = response.content - with open(filename, 'wb') as file: - file.write(data) - # 强制刷新到磁盘 - try: - os.sync() - except: - pass - self.logger.info(f"[DOWNLOAD] 使用二进制模式下载: {filename}, 大小: {len(data)} bytes") - else: - # 文本文件:使用文本模式写入 - response.encoding = 'utf-8' - with open(filename, 'w', encoding='utf-8') as file: - file.write(response.text) - self.logger.info(f"[DOWNLOAD] 使用文本模式下载: {filename}") - - # MD5 校验(如果服务器提供了MD5值) - if md5_b64_expected and hashlib is not None: - try: - with open(filename, "rb") as f: - file_data = f.read() - digest = hashlib.md5(file_data).digest() - md5_b64_got = binascii.b2a_base64(digest).decode().strip() - - if md5_b64_got != md5_b64_expected: - self.logger.error(f"[DOWNLOAD] MD5校验失败: 期望={md5_b64_expected}, 实际={md5_b64_got}") - return f"下载失败!MD5校验失败: 期望={md5_b64_expected}, 实际={md5_b64_got}" - else: - self.logger.info(f"[DOWNLOAD] MD5校验通过: {md5_b64_got}") - except Exception as e: - self.logger.warning(f"[DOWNLOAD] MD5校验过程出错: {e}") - # MD5校验出错时,如果是二进制文件(特别是ZIP),应该失败 - if is_binary: - return f"下载失败!MD5校验异常: {e}" - elif is_binary and not md5_b64_expected: - # 二进制文件(特别是ZIP)建议有MD5校验 - self.logger.warning(f"[DOWNLOAD] 警告: 服务器未提供MD5校验值,无法验证文件完整性") - - return f"下载成功!文件已保存为: {filename}" + if md5_b64_expected: + md5_b64_got = binascii.b2a_base64(md5_hash.digest()).decode().strip() + if md5_b64_got != md5_b64_expected: + return False, f"MD5校验失败" + self.logger.info("[OTA] MD5校验通过") + + return True, f"size={downloaded}" except requests.exceptions.RequestException as e: - return f"下载失败!网络请求错误: {e}" + return False, f"网络错误: {e}" except OSError as e: - return f"下载失败!文件写入错误: {e}" - except Exception as e: - return f"下载失败!发生未知错误: {e}" + return False, f"写入错误: {e}" - # def direct_ota_download(self, ota_url): - # """直接执行 OTA 下载(假设已有网络)""" - - # self._set_ota_url(ota_url) - # self._start_update_thread() - - # try: - # if not ota_url: - # from network import safe_enqueue - # safe_enqueue({"result": "ota_failed", "reason": "missing_url"}, 2) - # return - - # parsed_url = urlparse(ota_url) - # host = parsed_url.hostname - # port = parsed_url.port or (443 if parsed_url.scheme == 'https' else 80) - - # if not network_manager.is_server_reachable(host, port, timeout=8): - # from network import safe_enqueue - # safe_enqueue({"result": "ota_failed", "reason": f"无法连接 {host}:{port}"}, 2) - # return - - # downloaded_filename = self.get_filename_from_url(ota_url, default_name="main_tmp") - # self.logger.info(f"[OTA] 下载文件将保存为: {downloaded_filename}") - # self.logger.info(f"[OTA] 开始下载: {ota_url}") - # result_msg = self.download_file(ota_url, downloaded_filename) - # self.logger.info(f"[OTA] {result_msg}") - - # if "成功" in result_msg or "下载成功" in result_msg: - # if self.apply_ota_and_reboot(ota_url, downloaded_filename): - # return - # else: - # from network import safe_enqueue - # safe_enqueue({"result": result_msg}, 2) - - # except Exception as e: - # error_msg = f"OTA 异常: {str(e)}" - # self.logger.error(error_msg) - # from network import safe_enqueue - # safe_enqueue({"result": "ota_failed", "reason": error_msg}, 2) - # finally: - # self._stop_update_thread() - - def download_file_via_4g(self, url, filename, - total_timeout_ms=600000, - retries=3, - debug=False): + def _apply_update(self, zip_path, progress_callback=None): """ - ML307R HTTP 下载(更稳的"固定小块 Range 顺序下载",基于main109.py): - - 只依赖 +MHTTPURC:"header"/"content"(不依赖 MHTTPREAD/cached) - - 每次只请求一个小块 Range(默认 10240B),失败就重试同一块,必要时缩小块大小 - - 每个 chunk 都重新 MHTTPCREATE/MHTTPREQUEST,避免卡在"206 header 但不吐 content"的坏状态 - - 使用二进制模式下载,确保文件完整性 + 解压ZIP到临时目录,重启后由主程序移动到实际目录 + + Returns: + (success: bool, message: str) """ - from urllib.parse import urlparse - from hardware import hardware_manager - - # 小块策略(与main109.py保持一致) - CHUNK_MAX = 10240 - CHUNK_MIN = 128 - CHUNK_RETRIES = 12 - FRAG_SIZE = 1024 - FRAG_DELAY = 10 - - t_func0 = time.ticks_ms() - - parsed = urlparse(url) - host = parsed.hostname - # MHTTPREQUEST 的路径必须包含 query(七牛/ OSS 签名、token 多在 ? 后),否则易 403/HTML,header 无 CL → no_header_or_total - path = parsed.path or "/" - if parsed.query: - path = f"{path}?{parsed.query}" - if parsed.fragment: - path = f"{path}#{parsed.fragment}" - if not host: - return False, "bad_url (no host)" - - # 很多 ML307R 的 MHTTP 对 https 不稳定;对已知域名做降级 - - if isinstance(url, str) and url.startswith("https://static.shelingxingqiu.com/"): - base_url = "http://static.shelingxingqiu.com" - self._is_https = False - else: - base_url = f"http://{host}" - self._is_https = False - self.logger.info(f"base_url: {base_url}, self._is_https: {self._is_https}") - # logger removed - use self.logger instead - - def _log(*a): - if debug: - self.logger.debug(" ".join(str(x) for x in a)) - - def _pwr_log(prefix=""): - """debug 用:输出电压/电量""" - if not debug: - return - try: - v = get_bus_voltage() - p = voltage_to_percent(v) - self.logger.debug(f"[PWR]{prefix} v={v:.3f}V p={p}%") - except Exception as e: - try: - self.logger.debug(f"[PWR]{prefix} read_failed: {e}") - except: - pass - - def _clear_http_events(): - if hardware_manager.at_client: - while hardware_manager.at_client.pop_http_event() is not None: - pass - - def _parse_httpid(resp: str): - m = re.search(r"\+MHTTPCREATE:\s*(\d+)", resp) - return int(m.group(1)) if m else None - - def _get_ip(): - r = hardware_manager.at_client.send("AT+CGPADDR=1", "OK", 3000) - m = re.search(r'\+CGPADDR:\s*1,"([^"]+)"', r) - return m.group(1) if m else "" - - def _ensure_pdp(): - ip = _get_ip() - if ip and ip != "0.0.0.0": - return True, ip - hardware_manager.at_client.send("AT+MIPCALL=1,1", "OK", 15000) - for _ in range(10): - ip = _get_ip() - if ip and ip != "0.0.0.0": - return True, ip - time.sleep(1) - return False, ip - - def _extract_hdr_fields(hdr_text: str): - mlen = re.search(r"Content-Length:\s*(\d+)", hdr_text, re.IGNORECASE) - clen = int(mlen.group(1)) if mlen else None - mmd5 = re.search(r"Content-Md5:\s*([A-Za-z0-9+/=]+)", hdr_text, re.IGNORECASE) - md5_b64 = mmd5.group(1).strip() if mmd5 else None - return clen, md5_b64 - - def _extract_content_range(hdr_text: str): - m = re.search(r"Content-Range:\s*bytes\s*(\d+)\s*-\s*(\d+)\s*/\s*(\d+)", hdr_text, re.IGNORECASE) - if not m: - return None, None, None - try: - return int(m.group(1)), int(m.group(2)), int(m.group(3)) - except: - return None, None, None - - def _hard_reset_http(): - """模块进入"坏状态"时的保守清场""" - _clear_http_events() - for i in range(0, 6): - try: - hardware_manager.at_client.send(f"AT+MHTTPDEL={i}", "OK", 1200) - except: - pass - _clear_http_events() - - def _create_httpid(full_reset=False): - _clear_http_events() - if hardware_manager.at_client: - hardware_manager.at_client.flush() - if full_reset: - _hard_reset_http() - resp = hardware_manager.at_client.send(f'AT+MHTTPCREATE="{base_url}"', "OK", 8000) - hid = _parse_httpid(resp) - if self._is_https: - resp = hardware_manager.at_client.send(f'AT+MHTTPCFG="ssl",{hid},1,1', "OK", 2000) - if "ERROR" in resp or "CME ERROR" in resp: - self.logger.error(f"MHTTPCFG SSL failed: {resp}") - # 尝试https 降级到http - downgraded_base_url = base_url.replace("https://", "http://") - resp = hardware_manager.at_client.send(f'AT+MHTTPCREATE="{downgraded_base_url}"', "OK", 8000) - hid = _parse_httpid(resp) - - return hid, resp - - def _fetch_range_into_buf(start, want_len, out_buf, full_reset=False): - """ - 请求 Range [start, start+want_len),写入 out_buf(bytearray,长度=want_len) - 返回 (ok, msg, total_len, md5_b64, got_len) - """ - end_incl = start + want_len - 1 - hid, cresp = _create_httpid(full_reset=full_reset) - if hid is None: - return False, f"MHTTPCREATE failed: {cresp}", None, None, 0 - - # 降低 URC 压力(分片/延迟) - hardware_manager.at_client.send(f'AT+MHTTPCFG="fragment",{hid},{FRAG_SIZE},{FRAG_DELAY}', "OK", 1500) - # 设置 Range header(inclusive) - hardware_manager.at_client.send(f'AT+MHTTPCFG="header",{hid},"Range: bytes={start}-{end_incl}"', "OK", 3000) - - req = hardware_manager.at_client.send(f'AT+MHTTPREQUEST={hid},1,0,"{path}"', "OK", 15000) - if "ERROR" in req or "CME ERROR" in req: - hardware_manager.at_client.send(f"AT+MHTTPDEL={hid}", "OK", 2000) - return False, f"MHTTPREQUEST failed: {req}", None, None, 0 - - # 等 header + content - hdr_text = None - hdr_accum = "" - code = None - resp_total = None - total_len = None - md5_b64 = None - - got_ranges = set() - last_sum = 0 - t0 = time.ticks_ms() - timeout_ms = 9000 - logged_hdr = False - - while time.ticks_ms() - t0 < timeout_ms: - ev = hardware_manager.at_client.pop_http_event() if hardware_manager.at_client else None - if not ev: - time.sleep_ms(5) - continue - - if ev[0] == "header": - _, ehid, ecode, ehdr = ev - if ehid != hid: - continue - code = ecode - hdr_text = ehdr - if ehdr: - hdr_accum = (hdr_accum + "\n" + ehdr) if hdr_accum else ehdr - - resp_total_tmp, md5_tmp = _extract_hdr_fields(hdr_accum) - if md5_tmp: - md5_b64 = md5_tmp - cr_s, cr_e, cr_total = _extract_content_range(hdr_accum) - if cr_total is not None: - total_len = cr_total - if resp_total_tmp is not None: - resp_total = resp_total_tmp - elif resp_total is None and (cr_s is not None) and (cr_e is not None) and (cr_e >= cr_s): - resp_total = (cr_e - cr_s + 1) - if (not logged_hdr) and (resp_total is not None or total_len is not None): - _log(f"[HDR] id={hid} code={code} clen={resp_total} cr={cr_s}-{cr_e}/{cr_total}") - logged_hdr = True - continue - - if ev[0] == "content": - _, ehid, _total, _sum, _cur, payload = ev - if ehid != hid: - continue - if resp_total is None: - resp_total = _total - if resp_total is None or resp_total <= 0: - continue - start_rel = _sum - _cur - end_rel = _sum - if start_rel < 0 or start_rel >= resp_total: - continue - if end_rel > resp_total: - end_rel = resp_total - actual_len = min(len(payload), end_rel - start_rel) - if actual_len <= 0: - continue - out_buf[start_rel:start_rel + actual_len] = payload[:actual_len] - got_ranges.add((start_rel, start_rel + actual_len)) - if _sum > last_sum: - last_sum = _sum - if debug and (last_sum >= resp_total or (last_sum % 512 == 0)): - _log(f"[CHUNK] {start}+{last_sum}/{resp_total}") - - if last_sum >= resp_total: - break - - # 清理实例(快路径:只删当前 hid) - try: - hardware_manager.at_client.send(f"AT+MHTTPDEL={hid}", "OK", 2000) - except: - pass - - if resp_total is None: - return False, "no_header_or_total", total_len, md5_b64, 0 - - # 计算实际填充长度 - merged = sorted(got_ranges) - merged2 = [] - for s, e in merged: - if not merged2 or s > merged2[-1][1]: - merged2.append((s, e)) - else: - merged2[-1] = (merged2[-1][0], max(merged2[-1][1], e)) - filled = sum(e - s for s, e in merged2) - - if filled < resp_total: - return False, f"incomplete_chunk got={filled} expected={resp_total} code={code}", total_len, md5_b64, filled - - got_len = resp_total - return True, "OK", total_len, md5_b64, got_len + if not os.path.exists(zip_path): + return False, f"文件不存在: {zip_path}" try: - self._begin_ota() + with open(zip_path, "rb") as f: + header = f.read(4) + if header[:2] != b'PK': + return False, f"不是ZIP文件: {header.hex()}" + except Exception as e: + return False, f"读取ZIP失败: {e}" + + staging_dir = f"{config.APP_DIR}/ota_staging" + try: + os.makedirs(staging_dir, exist_ok=True) except: pass - from network import network_manager - with network_manager.get_uart_lock(): - try: - ok_pdp, ip = _ensure_pdp() - if not ok_pdp: - return False, f"PDP not ready (ip={ip})" + try: + self.logger.info(f"[OTA] 开始解压: {zip_path} -> {staging_dir}") + ret = os.system(f"unzip -q -o '{zip_path}' -d '{staging_dir}' 2>&1") + if ret != 0: + return False, f"解压失败: exit={ret}" + self.logger.info("[OTA] 解压完成") + except Exception as e: + return False, f"解压异常: {e}" - # 先清空旧事件,避免串台 - _clear_http_events() + file_count = 0 + for _, _, files in os.walk(staging_dir): + file_count += len(files) - # 为了支持随机写入,先创建空文件 - try: - with open(filename, "wb") as f: - f.write(b"") - except Exception as e: - return False, f"open_file_failed: {e}" - - total_len = None - expect_md5_b64 = None - - offset = 0 - chunk = CHUNK_MAX - t_start = time.ticks_ms() - last_progress_ms = t_start - STALL_TIMEOUT_MS = 60000 - last_pwr_ms = t_start - _pwr_log(prefix=" ota_start") - bad_http_state = 0 - - while True: - now = time.ticks_ms() - if debug and time.ticks_diff(now, last_pwr_ms) >= 5000: - last_pwr_ms = now - _pwr_log(prefix=f" off={offset}/{total_len or '?'}") - if time.ticks_diff(now, t_start) > total_timeout_ms: - return False, f"timeout overall after {total_timeout_ms}ms offset={offset} total={total_len}" - - if time.ticks_diff(now, last_progress_ms) > STALL_TIMEOUT_MS: - return False, f"timeout stalled {STALL_TIMEOUT_MS}ms offset={offset} total={total_len}" - - if total_len is not None and offset >= total_len: - break - - want = chunk - if total_len is not None: - remain = total_len - offset - if remain <= 0: - break - if want > remain: - want = remain - - # 本 chunk 的 buffer(长度=want) - buf = bytearray(want) - - success = False - last_err = "unknown" - md5_seen = None - got_len = 0 - for k in range(1, CHUNK_RETRIES + 1): - do_full_reset = (bad_http_state >= 2) - ok, msg, tlen, md5_b64, got = _fetch_range_into_buf(offset, want, buf, full_reset=do_full_reset) - last_err = msg - if tlen is not None and total_len is None: - total_len = tlen - if md5_b64 and not expect_md5_b64: - expect_md5_b64 = md5_b64 - if ok: - success = True - got_len = got - bad_http_state = 0 - break - - try: - if ("no_header_or_total" in msg) or ("MHTTPREQUEST failed" in msg) or ("MHTTPCREATE failed" in msg): - bad_http_state += 1 - else: - bad_http_state = max(0, bad_http_state - 1) - except: - pass - - if chunk > CHUNK_MIN: - chunk = max(CHUNK_MIN, chunk // 2) - want = min(chunk, want) - buf = bytearray(want) - _log(f"[RETRY] off={offset} want={want} try={k} err={msg}") - _pwr_log(prefix=f" retry{k} off={offset}") - time.sleep_ms(120) - - if not success: - return False, f"chunk_failed off={offset} want={want} err={last_err} total={total_len}" - - # 写入文件(二进制模式) - try: - with open(filename, "r+b") as f: - f.seek(offset) - f.write(bytes(buf)) - except Exception as e: - return False, f"write_failed off={offset}: {e}" - - offset += len(buf) - last_progress_ms = time.ticks_ms() - chunk = CHUNK_MAX - if debug: - _log(f"[OK] offset={offset}/{total_len or '?'}") - - # MD5 校验 - if expect_md5_b64 and hashlib is not None: - try: - with open(filename, "rb") as f: - data = f.read() - digest = hashlib.md5(data).digest() - got_b64 = binascii.b2a_base64(digest).decode().strip() - if got_b64 != expect_md5_b64: - return False, f"md5_mismatch got={got_b64} expected={expect_md5_b64}" - self.logger.debug(f"[4G-DL] MD5 verified: {got_b64}") - except Exception as e: - return False, f"md5_check_failed: {e}" - - t_cost = time.ticks_diff(time.ticks_ms(), t_func0) - self.logger.info(f"[4G-DL] download complete: size={offset} ip={ip} cost_ms={t_cost}") - return True, f"OK size={offset} ip={ip} cost_ms={t_cost}" - - finally: - self._end_ota() - - def direct_ota_download_via_4g(self, ota_url): - """通过 4G 模块下载 OTA(不需要 Wi-Fi)""" - self._set_ota_url(ota_url) - self._set_ota_mode("4g") - self._start_update_thread() - # 延迟导入避免循环依赖 - from network import safe_enqueue + if file_count == 0: + return False, "ZIP中无文件" try: - t_ota0 = time.ticks_ms() - if not ota_url: - safe_enqueue({"result": "ota_failed", "reason": "missing_url"}, 2) - return - - # OTA 全程暂停 TCP(避免心跳/重连抢占 uart4g_lock,导致 server 断链 + HTTP URC 更容易丢) - self._begin_ota() - - # 主动断开 AT TCP,减少 +MIPURC 噪声干扰 HTTP URC 下载 - from network import network_manager - network_manager.disconnect_server() - try: - with network_manager.get_uart_lock(): - hardware_manager.at_client.send("AT+MIPCLOSE=0", "OK", 1500) - except: - pass - - # 从URL中提取文件名(保留原始扩展名) - downloaded_filename = self.get_filename_from_url(ota_url, default_name="main_tmp") - self.logger.info(f"[OTA-4G] 下载文件将保存为: {downloaded_filename}") - - self.logger.info(f"[OTA-4G] 开始通过 4G 下载: {ota_url}") - # 重要说明: - # - AT+MDIALUP / RNDIS 是"USB 主机拨号上网"模式,在不少 ML307R 固件上会占用/切换内部网络栈, - # 从而导致 AT+MIPOPEN / +MIPURC 这套 TCP 连接无法工作(你会看到一直"连接到服务器...")。 - # - 这个设备当前 4G 是走 UART + AT Socket(MIPOPEN),并没有把 4G 变成系统网卡(如 ppp0)。 - # 因此这里不再自动拨号/改路由;只有当系统本来就有 default route(例如 eth0 已联网)时,才尝试走 requests 下载。 - - msg_sys = "" - try: - import power - v = power.get_bus_voltage() - p = power.voltage_to_percent(v) - self.logger.info(f"[OTA-4G][PWR] before_urc v={v:.3f}V p={p}%") - except Exception as e: - self.logger.error(f"[OTA-4G][PWR] before_urc read_failed: {e}") - - t_dl0 = time.ticks_ms() - success, msg = self.download_file_via_4g(ota_url, downloaded_filename, debug=True) - t_dl_cost = time.ticks_diff(time.ticks_ms(), t_dl0) - self.logger.info(f"[OTA-4G] {msg}") - self.logger.info(f"[OTA-4G] download_cost_ms={t_dl_cost}") - - if success and "OK" in msg: - if self.apply_ota_and_reboot(ota_url, downloaded_filename): - return - else: - safe_enqueue({"result": msg_sys or msg}, 2) - - except Exception as e: - error_msg = f"OTA-4G 异常: {str(e)}" - self.logger.error(error_msg) - safe_enqueue({"result": "ota_failed", "reason": error_msg}, 2) - finally: - # 总耗时(注意:若成功并 reboot,这行可能来不及打印) - try: - t_cost = time.ticks_diff(time.ticks_ms(), t_ota0) - self.logger.info(f"[OTA-4G] total_cost_ms={t_cost}") - except: - pass - self._stop_update_thread() - # 对应上面的 _begin_ota() - self._end_ota() - - def handle_wifi_and_update(self, ssid, password, ota_url): - """在子线程中执行 Wi-Fi 连接 + OTA 更新流程""" - self._set_ota_url(ota_url) - self._set_ota_mode("wifi") - self._start_update_thread() - # 延迟导入避免循环导入 - from network import network_manager, safe_enqueue + os.sync() + except: + pass try: - # 与 4G 一致:OTA 期间暂停主循环 / 心跳等 - self._begin_ota() - if not ota_url: - safe_enqueue({"result": "ota_failed", "reason": "missing_url"}, 2) - return - from urllib.parse import urlparse - parsed_url = urlparse(ota_url) - host = parsed_url.hostname - port = parsed_url.port or (443 if parsed_url.scheme == 'https' else 80) + os.remove(zip_path) + except: + pass - # 先连接 WiFi,并把 OTA host:port 作为“可达性验证目标” - # 只有连接成功 + 可访问 OTA 地址,才会把 SSID/PASS 落盘到 /boot/ - ip, error = network_manager.connect_wifi( - ssid, - password, - verify_host=host, - verify_port=port, - persist=True, - ) - if error: - safe_enqueue({"result": "wifi_failed", "error": error}, 2) - return - safe_enqueue({"result": "wifi_connected", "ip": ip}, 2) + self.logger.info(f"[OTA] 已解压 {file_count} 个文件到临时目录,重启后生效") + return True, file_count - downloaded_filename = self.get_filename_from_url(ota_url, default_name="main_tmp") - self.logger.info(f"[OTA] 下载文件将保存为: {downloaded_filename}") - - self.logger.info(f"[NET] 已确认可访问 {host}:{port},开始下载...") - result = self.download_file_via_wifi(ota_url, downloaded_filename) - self.logger.info(result) - - if "成功" in result or "下载成功" in result: - if self.apply_ota_and_reboot(ota_url, downloaded_filename): - return - else: - safe_enqueue({"result": result}, 2) - except Exception as e: - err_msg = f"下载失败: {str(e)}" - safe_enqueue({"result": err_msg}, 2) - self.logger.error(err_msg) - finally: - self._stop_update_thread() - self._end_ota() # 与 4G 一致 - print("[UPDATE] 更新线程执行完毕,即将退出。") - def restore_from_backup(self, backup_dir_path=None): """ 从备份目录恢复所有文件到应用目录 Args: - backup_dir_path: 备份目录路径,如果为None,自动查找最新的备份目录 + backup_dir_path: 备份目录路径,None则自动查找最新备份 Returns: bool: 是否成功恢复 @@ -1265,19 +254,17 @@ class OTAManager: item_path = os.path.join(backup_base, item) if os.path.isdir(item_path) and item.startswith("backup_"): try: - dir_num_str = item.replace("backup_", "") - dir_num = int(dir_num_str) + dir_num = int(item.replace("backup_", "")) backup_dirs.append((item, dir_num)) except: pass if not backup_dirs: - self.logger.error(f"[RESTORE] 没有找到备份目录") + self.logger.error("[RESTORE] 没有找到备份目录") return False backup_dirs.sort(key=lambda x: x[1], reverse=True) - latest_backup = backup_dirs[0][0] - backup_dir_path = os.path.join(backup_base, latest_backup) + backup_dir_path = os.path.join(backup_base, backup_dirs[0][0]) if not os.path.exists(backup_dir_path): self.logger.error(f"[RESTORE] 备份目录不存在: {backup_dir_path}") @@ -1288,26 +275,23 @@ class OTAManager: restored_files = [] for root, dirs, files in os.walk(backup_dir_path): for f in files: - source_path = os.path.join(root, f) - rel_path = os.path.relpath(source_path, backup_dir_path) - dest_path = os.path.join(config.APP_DIR, rel_path) - - dest_dir = os.path.dirname(dest_path) + src = os.path.join(root, f) + rel = os.path.relpath(src, backup_dir_path) + dest = os.path.join(config.APP_DIR, rel) + dest_dir = os.path.dirname(dest) if dest_dir: os.makedirs(dest_dir, exist_ok=True) - try: - shutil.copy2(source_path, dest_path) - restored_files.append(rel_path) - self.logger.info(f"[RESTORE] 已恢复: {rel_path}") + shutil.copy2(src, dest) + restored_files.append(rel) except Exception as e: - self.logger.error(f"[RESTORE] 恢复 {rel_path} 失败: {e}") + self.logger.error(f"[RESTORE] 恢复 {rel} 失败: {e}") if restored_files: self.logger.info(f"[RESTORE] 成功恢复 {len(restored_files)} 个文件") return True else: - self.logger.info(f"[RESTORE] 没有文件被恢复") + self.logger.info("[RESTORE] 没有文件被恢复") return False except Exception as e: @@ -1315,29 +299,19 @@ class OTAManager: return False -# 创建全局单例实例 +# 全局单例 ota_manager = OTAManager() -# ==================== 向后兼容的函数接口 ==================== -# 这些函数会更新 ota_manager 的状态,并调用实际实现 +# ==================== 向后兼容接口 ==================== def apply_ota_and_reboot(ota_url=None, downloaded_file=None): - """应用OTA并重启(向后兼容接口)""" - return ota_manager.apply_ota_and_reboot(ota_url, downloaded_file) - -def direct_ota_download(ota_url): - """直接执行OTA下载(向后兼容接口)""" - return ota_manager.direct_ota_download(ota_url) + return ota_manager.perform_ota(ota_url) def direct_ota_download_via_4g(ota_url): - """通过4G模块下载OTA(向后兼容接口)""" - return ota_manager.direct_ota_download_via_4g(ota_url) + return ota_manager.perform_ota(ota_url) def handle_wifi_and_update(ssid, password, ota_url): - """处理WiFi连接并更新(向后兼容接口)""" - return ota_manager.handle_wifi_and_update(ssid, password, ota_url) + return ota_manager.perform_ota(ota_url) def restore_from_backup(backup_dir_path=None): - """从备份恢复(向后兼容接口)""" return ota_manager.restore_from_backup(backup_dir_path) - diff --git a/tcp_messages_pb2.py b/tcp_messages_pb2.py index 7ffd3ce..1f02081 100644 --- a/tcp_messages_pb2.py +++ b/tcp_messages_pb2.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- # Generated by the protocol buffer compiler. DO NOT EDIT! -# source: tcp_messages.proto +# source: proto/tcp_messages.proto """Generated protocol buffer code.""" from google.protobuf import descriptor as _descriptor from google.protobuf import descriptor_pool as _descriptor_pool @@ -14,7 +14,7 @@ _sym_db = _symbol_database.Default() -DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x12tcp_messages.proto\x12\x03tcp\"\x83\x01\n\x0cLoginRequest\x12\x11\n\tdevice_id\x18\x01 \x01(\t\x12\x10\n\x08password\x18\x02 \x01(\t\x12\x10\n\x08if_admin\x18\x03 \x01(\x08\x12\x0f\n\x07version\x18\x04 \x01(\t\x12\x0b\n\x03vol\x18\x05 \x01(\x01\x12\x0f\n\x07vol_per\x18\x06 \x01(\x01\x12\r\n\x05iccid\x18\x07 \x01(\t\"*\n\rLoginResponse\x12\x0b\n\x03\x63md\x18\x01 \x01(\r\x12\x0c\n\x04\x64\x61ta\x18\x02 \x01(\t\"4\n\tHeartbeat\x12\t\n\x01t\x18\x01 \x01(\x03\x12\x0b\n\x03vol\x18\x02 \x01(\x01\x12\x0f\n\x07vol_per\x18\x03 \x01(\x01\"&\n\tLogicBody\x12\x0b\n\x03\x63md\x18\x01 \x01(\r\x12\x0c\n\x04\x64\x61ta\x18\x02 \x01(\x0c\")\n\x0cResponseBody\x12\x0b\n\x03\x63md\x18\x01 \x01(\r\x12\x0c\n\x04\x64\x61ta\x18\x02 \x01(\x0c\"@\n\x0bMonitorBody\x12\x11\n\tdevice_id\x18\x01 \x01(\t\x12\x0c\n\x04\x64\x61ta\x18\x02 \x01(\x0c\x12\x10\n\x08msg_type\x18\x03 \x01(\r\",\n\x16MonitorDevicesResponse\x12\x12\n\ndevice_ids\x18\x01 \x03(\t\"9\n\x0bOtaFragment\x12\t\n\x01l\x18\x01 \x01(\x05\x12\t\n\x01\x64\x18\x02 \x01(\t\x12\t\n\x01t\x18\x03 \x01(\x05\x12\t\n\x01v\x18\x04 \x01(\t\"\xab\x02\n\tShootData\x12\x0f\n\x07shot_id\x18\x01 \x01(\t\x12\t\n\x01x\x18\x02 \x01(\x01\x12\t\n\x01y\x18\x03 \x01(\x01\x12\t\n\x01r\x18\x04 \x01(\x01\x12\t\n\x01\x64\x18\x05 \x01(\x01\x12\x0b\n\x03\x61\x64\x63\x18\x06 \x01(\x01\x12\x14\n\x0ctarget_class\x18\x07 \x01(\t\x12\x1f\n\x17target_class_confidence\x18\x08 \x01(\x01\x12\x0f\n\x07\x64_laser\x18\t \x01(\x01\x12\x17\n\x0f\x64_laser_quality\x18\n \x01(\x01\x12\t\n\x01m\x18\x0b \x01(\t\x12\x14\n\x0claser_method\x18\x0c \x01(\t\x12\x10\n\x08target_x\x18\r \x01(\x01\x12\x10\n\x08target_y\x18\x0e \x01(\x01\x12\x15\n\roffset_method\x18\x0f \x01(\t\x12\x17\n\x0f\x64istance_method\x18\x10 \x01(\t\"!\n\nShootEvent\x12\x13\n\x0bshoot_event\x18\x01 \x01(\t\"C\n\rBatteryReport\x12\x0f\n\x07\x62\x61ttery\x18\x01 \x01(\x01\x12\x0f\n\x07voltage\x18\x02 \x01(\x01\x12\x10\n\x08net_type\x18\x03 \x01(\t\"9\n\x11\x43\x65nterPointResult\x12\x0e\n\x06result\x18\x01 \x01(\t\x12\t\n\x01x\x18\x02 \x01(\x01\x12\t\n\x01y\x18\x03 \x01(\x01\"3\n\x0e\x43\x65nterPointSet\x12\x0b\n\x03\x63md\x18\x01 \x01(\r\x12\t\n\x01x\x18\x02 \x01(\x01\x12\t\n\x01y\x18\x03 \x01(\x01\"(\n\tOtaResult\x12\x0e\n\x06result\x18\x01 \x01(\t\x12\x0b\n\x03url\x18\x02 \x01(\t\"\x1f\n\rGenericResult\x12\x0e\n\x06result\x18\x01 \x01(\t\"&\n\x08IpReport\x12\x0e\n\x06result\x18\x01 \x01(\t\x12\n\n\x02ip\x18\x02 \x01(\t\"Z\n\x12ImageUploadCommand\x12\x12\n\nupload_url\x18\x01 \x01(\t\x12\r\n\x05token\x18\x02 \x01(\t\x12\x10\n\x08shoot_id\x18\x03 \x01(\t\x12\x0f\n\x07outlink\x18\x04 \x01(\t\"d\n\x10LogUploadCommand\x12\x12\n\nupload_url\x18\x01 \x01(\t\x12\r\n\x05token\x18\x02 \x01(\t\x12\x0b\n\x03key\x18\x03 \x01(\t\x12\x0f\n\x07outlink\x18\x04 \x01(\t\x12\x0f\n\x07\x61rchive\x18\x05 \x01(\t\"K\n\x0eOtaRequestData\x12\x0c\n\x04ssid\x18\x01 \x01(\t\x12\x10\n\x08password\x18\x02 \x01(\t\x12\x0b\n\x03url\x18\x03 \x01(\t\x12\x0c\n\x04mode\x18\x04 \x01(\t\"1\n\x0fWifiConnectData\x12\x0c\n\x04ssid\x18\x01 \x01(\t\x12\x10\n\x08password\x18\x02 \x01(\t\"_\n\x11ImageUploadResult\x12\x0e\n\x06result\x18\x01 \x01(\t\x12\x10\n\x08shoot_id\x18\x02 \x01(\t\x12\x0b\n\x03key\x18\x03 \x01(\t\x12\x0e\n\x06reason\x18\x04 \x01(\t\x12\x0b\n\x03via\x18\x05 \x01(\t\"v\n\x0fLogUploadResult\x12\x0e\n\x06result\x18\x01 \x01(\t\x12\x0e\n\x06reason\x18\x02 \x01(\t\x12\x10\n\x08\x66ilename\x18\x03 \x01(\t\x12\x13\n\x0bstatus_code\x18\x04 \x01(\x05\x12\x0c\n\x04ssid\x18\x05 \x01(\t\x12\x0e\n\x06\x64\x65tail\x18\x06 \x01(\t\"J\n\x14\x42\x61tteryQueryResponse\x12\x0f\n\x07\x62\x61ttery\x18\x01 \x01(\x01\x12\x0f\n\x07voltage\x18\x02 \x01(\x01\x12\x10\n\x08net_type\x18\x03 \x01(\t\"\'\n\x0fOta4gSubCodeReq\x12\t\n\x01l\x18\x01 \x01(\x05\x12\t\n\x01v\x18\x02 \x01(\t\"5\n\x10Ota4gSubCodeResp\x12\x0b\n\x03\x63md\x18\x01 \x01(\r\x12\t\n\x01l\x18\x02 \x01(\x05\x12\t\n\x01v\x18\x03 \x01(\t\"\x1e\n\x0fShutdownCommand\x12\x0b\n\x03\x63md\x18\x01 \x01(\r\" \n\x0c\x41utoShutdown\x12\x10\n\x08poweroff\x18\x01 \x01(\tBB\n\rcom.shoot.tcpZ1git.shelingxingqiu.com/shoot-tcp-server/proto;tcpb\x06proto3') +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x18proto/tcp_messages.proto\x12\x03tcp\"\x83\x01\n\x0cLoginRequest\x12\x11\n\tdevice_id\x18\x01 \x01(\t\x12\x10\n\x08password\x18\x02 \x01(\t\x12\x10\n\x08if_admin\x18\x03 \x01(\x08\x12\x0f\n\x07version\x18\x04 \x01(\t\x12\x0b\n\x03vol\x18\x05 \x01(\x01\x12\x0f\n\x07vol_per\x18\x06 \x01(\x01\x12\r\n\x05iccid\x18\x07 \x01(\t\"*\n\rLoginResponse\x12\x0c\n\x04\x63ode\x18\x01 \x01(\r\x12\x0b\n\x03msg\x18\x02 \x01(\t\"4\n\tHeartbeat\x12\t\n\x01t\x18\x01 \x01(\x03\x12\x0b\n\x03vol\x18\x02 \x01(\x01\x12\x0f\n\x07vol_per\x18\x03 \x01(\x01\"\xb3\n\n\tLogicBody\x12\x0b\n\x03\x63md\x18\x01 \x01(\r\x12$\n\nshoot_data\x18\n \x01(\x0b\x32\x0e.tcp.ShootDataH\x00\x12&\n\x0bshoot_event\x18\x0b \x01(\x0b\x32\x0f.tcp.ShootEventH\x00\x12,\n\x0e\x62\x61ttery_report\x18\x0c \x01(\x0b\x32\x12.tcp.BatteryReportH\x00\x12&\n\x0b\x61im_request\x18\r \x01(\x0b\x32\x0f.tcp.AimRequestH\x00\x12\x31\n\x11\x63lose_aim_request\x18\x0e \x01(\x0b\x32\x14.tcp.CloseAimRequestH\x00\x12&\n\x0bota_request\x18\x0f \x01(\x0b\x32\x0f.tcp.OtaRequestH\x00\x12:\n\x16ota4g_sub_code_request\x18\x10 \x01(\x0b\x32\x18.tcp.Ota4gSubCodeRequestH\x00\x12<\n\x17ota4g_sub_code_response\x18\x11 \x01(\x0b\x32\x19.tcp.Ota4gSubCodeResponseH\x00\x12\x37\n\x14wifi_connect_request\x18\x12 \x01(\x0b\x32\x17.tcp.WifiConnectRequestH\x00\x12\x35\n\x13\x63\x65nter_point_result\x18\x13 \x01(\x0b\x32\x16.tcp.CenterPointResultH\x00\x12/\n\x10\x63\x65nter_point_set\x18\x14 \x01(\x0b\x32\x13.tcp.CenterPointSetH\x00\x12.\n\x0f\x63harging_report\x18\x15 \x01(\x0b\x32\x13.tcp.ChargingReportH\x00\x12,\n\x0egeneric_result\x18\x16 \x01(\x0b\x32\x12.tcp.GenericResultH\x00\x12\x37\n\x14image_upload_command\x18\x17 \x01(\x0b\x32\x17.tcp.ImageUploadCommandH\x00\x12\x33\n\x12log_upload_command\x18\x18 \x01(\x0b\x32\x15.tcp.LogUploadCommandH\x00\x12\x35\n\x13image_upload_result\x18\x19 \x01(\x0b\x32\x16.tcp.ImageUploadResultH\x00\x12\x31\n\x11log_upload_result\x18\x1a \x01(\x0b\x32\x14.tcp.LogUploadResultH\x00\x12$\n\nota_result\x18\x1b \x01(\x0b\x32\x0e.tcp.OtaResultH\x00\x12)\n\x0b\x66ile_string\x18\x1c \x01(\x0b\x32\x12.tcp.FileStringMsgH\x00\x12\"\n\tip_report\x18\x1d \x01(\x0b\x32\r.tcp.IpReportH\x00\x12\x35\n\x13get_battery_request\x18\x1e \x01(\x0b\x32\x16.tcp.GetBatteryRequestH\x00\x12>\n\x18set_center_point_request\x18\x1f \x01(\x0b\x32\x1a.tcp.SetCenterPointRequestH\x00\x12\x30\n\x10shutdown_command\x18 \x01(\x0b\x32\x14.tcp.ShutdownCommandH\x00\x12\x38\n\x15get_shoot_pic_request\x18! \x01(\x0b\x32\x17.tcp.GetShootPicRequestH\x00\x12/\n\x10push_log_request\x18\" \x01(\x0b\x32\x13.tcp.PushLogRequestH\x00\x12\x35\n\x13wifi_status_request\x18# \x01(\x0b\x32\x16.tcp.WifiStatusRequestH\x00\x42\t\n\x07payload\"@\n\x0bMonitorBody\x12\x11\n\tdevice_id\x18\x01 \x01(\t\x12\x0c\n\x04\x64\x61ta\x18\x02 \x01(\x0c\x12\x10\n\x08msg_type\x18\x03 \x01(\r\",\n\x16MonitorDevicesResponse\x12\x12\n\ndevice_ids\x18\x01 \x03(\t\"9\n\x0bOtaFragment\x12\t\n\x01l\x18\x01 \x01(\x05\x12\t\n\x01\x64\x18\x02 \x01(\t\x12\t\n\x01t\x18\x03 \x01(\x05\x12\t\n\x01v\x18\x04 \x01(\t\"\xab\x02\n\tShootData\x12\x0f\n\x07shot_id\x18\x01 \x01(\t\x12\t\n\x01x\x18\x02 \x01(\x01\x12\t\n\x01y\x18\x03 \x01(\x01\x12\t\n\x01r\x18\x04 \x01(\x01\x12\t\n\x01\x64\x18\x05 \x01(\x01\x12\x0b\n\x03\x61\x64\x63\x18\x06 \x01(\x01\x12\x14\n\x0ctarget_class\x18\x07 \x01(\t\x12\x1f\n\x17target_class_confidence\x18\x08 \x01(\x01\x12\x0f\n\x07\x64_laser\x18\t \x01(\x01\x12\x17\n\x0f\x64_laser_quality\x18\n \x01(\x01\x12\t\n\x01m\x18\x0b \x01(\t\x12\x14\n\x0claser_method\x18\x0c \x01(\t\x12\x10\n\x08target_x\x18\r \x01(\x01\x12\x10\n\x08target_y\x18\x0e \x01(\x01\x12\x15\n\roffset_method\x18\x0f \x01(\t\x12\x17\n\x0f\x64istance_method\x18\x10 \x01(\t\"!\n\nShootEvent\x12\x13\n\x0bshoot_event\x18\x01 \x01(\t\"U\n\rBatteryReport\x12\x0f\n\x07\x62\x61ttery\x18\x01 \x01(\x01\x12\x0f\n\x07voltage\x18\x02 \x01(\x01\x12\x10\n\x08net_type\x18\x03 \x01(\t\x12\x10\n\x08\x63harging\x18\x04 \x01(\x08\"\x0c\n\nAimRequest\"\x11\n\x0f\x43loseAimRequest\"G\n\nOtaRequest\x12\x0c\n\x04ssid\x18\x01 \x01(\t\x12\x10\n\x08password\x18\x02 \x01(\t\x12\x0b\n\x03url\x18\x03 \x01(\t\x12\x0c\n\x04mode\x18\x04 \x01(\t\"+\n\x13Ota4gSubCodeRequest\x12\t\n\x01l\x18\x01 \x01(\x05\x12\t\n\x01v\x18\x02 \x01(\t\",\n\x14Ota4gSubCodeResponse\x12\t\n\x01l\x18\x01 \x01(\x05\x12\t\n\x01v\x18\x02 \x01(\t\"4\n\x12WifiConnectRequest\x12\x0c\n\x04ssid\x18\x01 \x01(\t\x12\x10\n\x08password\x18\x02 \x01(\t\"9\n\x11\x43\x65nterPointResult\x12\x0e\n\x06result\x18\x01 \x01(\t\x12\t\n\x01x\x18\x02 \x01(\x01\x12\t\n\x01y\x18\x03 \x01(\x01\"&\n\x0e\x43\x65nterPointSet\x12\t\n\x01x\x18\x01 \x01(\x01\x12\t\n\x01y\x18\x02 \x01(\x01\"\x10\n\x0e\x43hargingReport\"\x1f\n\rGenericResult\x12\x0e\n\x06result\x18\x01 \x01(\t\"Z\n\x12ImageUploadCommand\x12\x12\n\nupload_url\x18\x01 \x01(\t\x12\r\n\x05token\x18\x02 \x01(\t\x12\x10\n\x08shoot_id\x18\x03 \x01(\t\x12\x0f\n\x07outlink\x18\x04 \x01(\t\"d\n\x10LogUploadCommand\x12\x12\n\nupload_url\x18\x01 \x01(\t\x12\r\n\x05token\x18\x02 \x01(\t\x12\x0b\n\x03key\x18\x03 \x01(\t\x12\x0f\n\x07outlink\x18\x04 \x01(\t\x12\x0f\n\x07\x61rchive\x18\x05 \x01(\t\"_\n\x11ImageUploadResult\x12\x0e\n\x06result\x18\x01 \x01(\t\x12\x10\n\x08shoot_id\x18\x02 \x01(\t\x12\x0b\n\x03key\x18\x03 \x01(\t\x12\x0e\n\x06reason\x18\x04 \x01(\t\x12\x0b\n\x03via\x18\x05 \x01(\t\"v\n\x0fLogUploadResult\x12\x0e\n\x06result\x18\x01 \x01(\t\x12\x0e\n\x06reason\x18\x02 \x01(\t\x12\x10\n\x08\x66ilename\x18\x03 \x01(\t\x12\x13\n\x0bstatus_code\x18\x04 \x01(\x05\x12\x0c\n\x04ssid\x18\x05 \x01(\t\x12\x0e\n\x06\x64\x65tail\x18\x06 \x01(\t\"I\n\tOtaResult\x12\x0e\n\x06result\x18\x01 \x01(\t\x12\x0b\n\x03url\x18\x02 \x01(\t\x12\x10\n\x08progress\x18\x03 \x01(\x05\x12\r\n\x05phase\x18\x04 \x01(\t\";\n\rFileStringMsg\x12\t\n\x01l\x18\x01 \x01(\x05\x12\t\n\x01\x64\x18\x02 \x01(\t\x12\t\n\x01t\x18\x03 \x01(\x05\x12\t\n\x01v\x18\x04 \x01(\t\"&\n\x08IpReport\x12\x0e\n\x06result\x18\x01 \x01(\t\x12\n\n\x02ip\x18\x02 \x01(\t\"\x13\n\x11GetBatteryRequest\"-\n\x15SetCenterPointRequest\x12\t\n\x01x\x18\x01 \x01(\x01\x12\t\n\x01y\x18\x02 \x01(\x01\"\x11\n\x0fShutdownCommand\"&\n\x12GetShootPicRequest\x12\x10\n\x08shoot_id\x18\x01 \x01(\t\"\x10\n\x0ePushLogRequest\"\x13\n\x11WifiStatusRequestBB\n\rcom.shoot.tcpZ1git.shelingxingqiu.com/shoot-tcp-server/proto;tcpb\x06proto3') @@ -22,274 +22,334 @@ _LOGINREQUEST = DESCRIPTOR.message_types_by_name['LoginRequest'] _LOGINRESPONSE = DESCRIPTOR.message_types_by_name['LoginResponse'] _HEARTBEAT = DESCRIPTOR.message_types_by_name['Heartbeat'] _LOGICBODY = DESCRIPTOR.message_types_by_name['LogicBody'] -_RESPONSEBODY = DESCRIPTOR.message_types_by_name['ResponseBody'] _MONITORBODY = DESCRIPTOR.message_types_by_name['MonitorBody'] _MONITORDEVICESRESPONSE = DESCRIPTOR.message_types_by_name['MonitorDevicesResponse'] _OTAFRAGMENT = DESCRIPTOR.message_types_by_name['OtaFragment'] _SHOOTDATA = DESCRIPTOR.message_types_by_name['ShootData'] _SHOOTEVENT = DESCRIPTOR.message_types_by_name['ShootEvent'] _BATTERYREPORT = DESCRIPTOR.message_types_by_name['BatteryReport'] +_AIMREQUEST = DESCRIPTOR.message_types_by_name['AimRequest'] +_CLOSEAIMREQUEST = DESCRIPTOR.message_types_by_name['CloseAimRequest'] +_OTAREQUEST = DESCRIPTOR.message_types_by_name['OtaRequest'] +_OTA4GSUBCODEREQUEST = DESCRIPTOR.message_types_by_name['Ota4gSubCodeRequest'] +_OTA4GSUBCODERESPONSE = DESCRIPTOR.message_types_by_name['Ota4gSubCodeResponse'] +_WIFICONNECTREQUEST = DESCRIPTOR.message_types_by_name['WifiConnectRequest'] _CENTERPOINTRESULT = DESCRIPTOR.message_types_by_name['CenterPointResult'] _CENTERPOINTSET = DESCRIPTOR.message_types_by_name['CenterPointSet'] -_OTARESULT = DESCRIPTOR.message_types_by_name['OtaResult'] +_CHARGINGREPORT = DESCRIPTOR.message_types_by_name['ChargingReport'] _GENERICRESULT = DESCRIPTOR.message_types_by_name['GenericResult'] -_IPREPORT = DESCRIPTOR.message_types_by_name['IpReport'] _IMAGEUPLOADCOMMAND = DESCRIPTOR.message_types_by_name['ImageUploadCommand'] _LOGUPLOADCOMMAND = DESCRIPTOR.message_types_by_name['LogUploadCommand'] -_OTAREQUESTDATA = DESCRIPTOR.message_types_by_name['OtaRequestData'] -_WIFICONNECTDATA = DESCRIPTOR.message_types_by_name['WifiConnectData'] _IMAGEUPLOADRESULT = DESCRIPTOR.message_types_by_name['ImageUploadResult'] _LOGUPLOADRESULT = DESCRIPTOR.message_types_by_name['LogUploadResult'] -_BATTERYQUERYRESPONSE = DESCRIPTOR.message_types_by_name['BatteryQueryResponse'] -_OTA4GSUBCODEREQ = DESCRIPTOR.message_types_by_name['Ota4gSubCodeReq'] -_OTA4GSUBCODERESP = DESCRIPTOR.message_types_by_name['Ota4gSubCodeResp'] +_OTARESULT = DESCRIPTOR.message_types_by_name['OtaResult'] +_FILESTRINGMSG = DESCRIPTOR.message_types_by_name['FileStringMsg'] +_IPREPORT = DESCRIPTOR.message_types_by_name['IpReport'] +_GETBATTERYREQUEST = DESCRIPTOR.message_types_by_name['GetBatteryRequest'] +_SETCENTERPOINTREQUEST = DESCRIPTOR.message_types_by_name['SetCenterPointRequest'] _SHUTDOWNCOMMAND = DESCRIPTOR.message_types_by_name['ShutdownCommand'] -_AUTOSHUTDOWN = DESCRIPTOR.message_types_by_name['AutoShutdown'] +_GETSHOOTPICREQUEST = DESCRIPTOR.message_types_by_name['GetShootPicRequest'] +_PUSHLOGREQUEST = DESCRIPTOR.message_types_by_name['PushLogRequest'] +_WIFISTATUSREQUEST = DESCRIPTOR.message_types_by_name['WifiStatusRequest'] LoginRequest = _reflection.GeneratedProtocolMessageType('LoginRequest', (_message.Message,), { 'DESCRIPTOR' : _LOGINREQUEST, - '__module__' : 'tcp_messages_pb2' + '__module__' : 'proto.tcp_messages_pb2' # @@protoc_insertion_point(class_scope:tcp.LoginRequest) }) _sym_db.RegisterMessage(LoginRequest) LoginResponse = _reflection.GeneratedProtocolMessageType('LoginResponse', (_message.Message,), { 'DESCRIPTOR' : _LOGINRESPONSE, - '__module__' : 'tcp_messages_pb2' + '__module__' : 'proto.tcp_messages_pb2' # @@protoc_insertion_point(class_scope:tcp.LoginResponse) }) _sym_db.RegisterMessage(LoginResponse) Heartbeat = _reflection.GeneratedProtocolMessageType('Heartbeat', (_message.Message,), { 'DESCRIPTOR' : _HEARTBEAT, - '__module__' : 'tcp_messages_pb2' + '__module__' : 'proto.tcp_messages_pb2' # @@protoc_insertion_point(class_scope:tcp.Heartbeat) }) _sym_db.RegisterMessage(Heartbeat) LogicBody = _reflection.GeneratedProtocolMessageType('LogicBody', (_message.Message,), { 'DESCRIPTOR' : _LOGICBODY, - '__module__' : 'tcp_messages_pb2' + '__module__' : 'proto.tcp_messages_pb2' # @@protoc_insertion_point(class_scope:tcp.LogicBody) }) _sym_db.RegisterMessage(LogicBody) -ResponseBody = _reflection.GeneratedProtocolMessageType('ResponseBody', (_message.Message,), { - 'DESCRIPTOR' : _RESPONSEBODY, - '__module__' : 'tcp_messages_pb2' - # @@protoc_insertion_point(class_scope:tcp.ResponseBody) - }) -_sym_db.RegisterMessage(ResponseBody) - MonitorBody = _reflection.GeneratedProtocolMessageType('MonitorBody', (_message.Message,), { 'DESCRIPTOR' : _MONITORBODY, - '__module__' : 'tcp_messages_pb2' + '__module__' : 'proto.tcp_messages_pb2' # @@protoc_insertion_point(class_scope:tcp.MonitorBody) }) _sym_db.RegisterMessage(MonitorBody) MonitorDevicesResponse = _reflection.GeneratedProtocolMessageType('MonitorDevicesResponse', (_message.Message,), { 'DESCRIPTOR' : _MONITORDEVICESRESPONSE, - '__module__' : 'tcp_messages_pb2' + '__module__' : 'proto.tcp_messages_pb2' # @@protoc_insertion_point(class_scope:tcp.MonitorDevicesResponse) }) _sym_db.RegisterMessage(MonitorDevicesResponse) OtaFragment = _reflection.GeneratedProtocolMessageType('OtaFragment', (_message.Message,), { 'DESCRIPTOR' : _OTAFRAGMENT, - '__module__' : 'tcp_messages_pb2' + '__module__' : 'proto.tcp_messages_pb2' # @@protoc_insertion_point(class_scope:tcp.OtaFragment) }) _sym_db.RegisterMessage(OtaFragment) ShootData = _reflection.GeneratedProtocolMessageType('ShootData', (_message.Message,), { 'DESCRIPTOR' : _SHOOTDATA, - '__module__' : 'tcp_messages_pb2' + '__module__' : 'proto.tcp_messages_pb2' # @@protoc_insertion_point(class_scope:tcp.ShootData) }) _sym_db.RegisterMessage(ShootData) ShootEvent = _reflection.GeneratedProtocolMessageType('ShootEvent', (_message.Message,), { 'DESCRIPTOR' : _SHOOTEVENT, - '__module__' : 'tcp_messages_pb2' + '__module__' : 'proto.tcp_messages_pb2' # @@protoc_insertion_point(class_scope:tcp.ShootEvent) }) _sym_db.RegisterMessage(ShootEvent) BatteryReport = _reflection.GeneratedProtocolMessageType('BatteryReport', (_message.Message,), { 'DESCRIPTOR' : _BATTERYREPORT, - '__module__' : 'tcp_messages_pb2' + '__module__' : 'proto.tcp_messages_pb2' # @@protoc_insertion_point(class_scope:tcp.BatteryReport) }) _sym_db.RegisterMessage(BatteryReport) +AimRequest = _reflection.GeneratedProtocolMessageType('AimRequest', (_message.Message,), { + 'DESCRIPTOR' : _AIMREQUEST, + '__module__' : 'proto.tcp_messages_pb2' + # @@protoc_insertion_point(class_scope:tcp.AimRequest) + }) +_sym_db.RegisterMessage(AimRequest) + +CloseAimRequest = _reflection.GeneratedProtocolMessageType('CloseAimRequest', (_message.Message,), { + 'DESCRIPTOR' : _CLOSEAIMREQUEST, + '__module__' : 'proto.tcp_messages_pb2' + # @@protoc_insertion_point(class_scope:tcp.CloseAimRequest) + }) +_sym_db.RegisterMessage(CloseAimRequest) + +OtaRequest = _reflection.GeneratedProtocolMessageType('OtaRequest', (_message.Message,), { + 'DESCRIPTOR' : _OTAREQUEST, + '__module__' : 'proto.tcp_messages_pb2' + # @@protoc_insertion_point(class_scope:tcp.OtaRequest) + }) +_sym_db.RegisterMessage(OtaRequest) + +Ota4gSubCodeRequest = _reflection.GeneratedProtocolMessageType('Ota4gSubCodeRequest', (_message.Message,), { + 'DESCRIPTOR' : _OTA4GSUBCODEREQUEST, + '__module__' : 'proto.tcp_messages_pb2' + # @@protoc_insertion_point(class_scope:tcp.Ota4gSubCodeRequest) + }) +_sym_db.RegisterMessage(Ota4gSubCodeRequest) + +Ota4gSubCodeResponse = _reflection.GeneratedProtocolMessageType('Ota4gSubCodeResponse', (_message.Message,), { + 'DESCRIPTOR' : _OTA4GSUBCODERESPONSE, + '__module__' : 'proto.tcp_messages_pb2' + # @@protoc_insertion_point(class_scope:tcp.Ota4gSubCodeResponse) + }) +_sym_db.RegisterMessage(Ota4gSubCodeResponse) + +WifiConnectRequest = _reflection.GeneratedProtocolMessageType('WifiConnectRequest', (_message.Message,), { + 'DESCRIPTOR' : _WIFICONNECTREQUEST, + '__module__' : 'proto.tcp_messages_pb2' + # @@protoc_insertion_point(class_scope:tcp.WifiConnectRequest) + }) +_sym_db.RegisterMessage(WifiConnectRequest) + CenterPointResult = _reflection.GeneratedProtocolMessageType('CenterPointResult', (_message.Message,), { 'DESCRIPTOR' : _CENTERPOINTRESULT, - '__module__' : 'tcp_messages_pb2' + '__module__' : 'proto.tcp_messages_pb2' # @@protoc_insertion_point(class_scope:tcp.CenterPointResult) }) _sym_db.RegisterMessage(CenterPointResult) CenterPointSet = _reflection.GeneratedProtocolMessageType('CenterPointSet', (_message.Message,), { 'DESCRIPTOR' : _CENTERPOINTSET, - '__module__' : 'tcp_messages_pb2' + '__module__' : 'proto.tcp_messages_pb2' # @@protoc_insertion_point(class_scope:tcp.CenterPointSet) }) _sym_db.RegisterMessage(CenterPointSet) -OtaResult = _reflection.GeneratedProtocolMessageType('OtaResult', (_message.Message,), { - 'DESCRIPTOR' : _OTARESULT, - '__module__' : 'tcp_messages_pb2' - # @@protoc_insertion_point(class_scope:tcp.OtaResult) +ChargingReport = _reflection.GeneratedProtocolMessageType('ChargingReport', (_message.Message,), { + 'DESCRIPTOR' : _CHARGINGREPORT, + '__module__' : 'proto.tcp_messages_pb2' + # @@protoc_insertion_point(class_scope:tcp.ChargingReport) }) -_sym_db.RegisterMessage(OtaResult) +_sym_db.RegisterMessage(ChargingReport) GenericResult = _reflection.GeneratedProtocolMessageType('GenericResult', (_message.Message,), { 'DESCRIPTOR' : _GENERICRESULT, - '__module__' : 'tcp_messages_pb2' + '__module__' : 'proto.tcp_messages_pb2' # @@protoc_insertion_point(class_scope:tcp.GenericResult) }) _sym_db.RegisterMessage(GenericResult) -IpReport = _reflection.GeneratedProtocolMessageType('IpReport', (_message.Message,), { - 'DESCRIPTOR' : _IPREPORT, - '__module__' : 'tcp_messages_pb2' - # @@protoc_insertion_point(class_scope:tcp.IpReport) - }) -_sym_db.RegisterMessage(IpReport) - ImageUploadCommand = _reflection.GeneratedProtocolMessageType('ImageUploadCommand', (_message.Message,), { 'DESCRIPTOR' : _IMAGEUPLOADCOMMAND, - '__module__' : 'tcp_messages_pb2' + '__module__' : 'proto.tcp_messages_pb2' # @@protoc_insertion_point(class_scope:tcp.ImageUploadCommand) }) _sym_db.RegisterMessage(ImageUploadCommand) LogUploadCommand = _reflection.GeneratedProtocolMessageType('LogUploadCommand', (_message.Message,), { 'DESCRIPTOR' : _LOGUPLOADCOMMAND, - '__module__' : 'tcp_messages_pb2' + '__module__' : 'proto.tcp_messages_pb2' # @@protoc_insertion_point(class_scope:tcp.LogUploadCommand) }) _sym_db.RegisterMessage(LogUploadCommand) -OtaRequestData = _reflection.GeneratedProtocolMessageType('OtaRequestData', (_message.Message,), { - 'DESCRIPTOR' : _OTAREQUESTDATA, - '__module__' : 'tcp_messages_pb2' - # @@protoc_insertion_point(class_scope:tcp.OtaRequestData) - }) -_sym_db.RegisterMessage(OtaRequestData) - -WifiConnectData = _reflection.GeneratedProtocolMessageType('WifiConnectData', (_message.Message,), { - 'DESCRIPTOR' : _WIFICONNECTDATA, - '__module__' : 'tcp_messages_pb2' - # @@protoc_insertion_point(class_scope:tcp.WifiConnectData) - }) -_sym_db.RegisterMessage(WifiConnectData) - ImageUploadResult = _reflection.GeneratedProtocolMessageType('ImageUploadResult', (_message.Message,), { 'DESCRIPTOR' : _IMAGEUPLOADRESULT, - '__module__' : 'tcp_messages_pb2' + '__module__' : 'proto.tcp_messages_pb2' # @@protoc_insertion_point(class_scope:tcp.ImageUploadResult) }) _sym_db.RegisterMessage(ImageUploadResult) LogUploadResult = _reflection.GeneratedProtocolMessageType('LogUploadResult', (_message.Message,), { 'DESCRIPTOR' : _LOGUPLOADRESULT, - '__module__' : 'tcp_messages_pb2' + '__module__' : 'proto.tcp_messages_pb2' # @@protoc_insertion_point(class_scope:tcp.LogUploadResult) }) _sym_db.RegisterMessage(LogUploadResult) -BatteryQueryResponse = _reflection.GeneratedProtocolMessageType('BatteryQueryResponse', (_message.Message,), { - 'DESCRIPTOR' : _BATTERYQUERYRESPONSE, - '__module__' : 'tcp_messages_pb2' - # @@protoc_insertion_point(class_scope:tcp.BatteryQueryResponse) +OtaResult = _reflection.GeneratedProtocolMessageType('OtaResult', (_message.Message,), { + 'DESCRIPTOR' : _OTARESULT, + '__module__' : 'proto.tcp_messages_pb2' + # @@protoc_insertion_point(class_scope:tcp.OtaResult) }) -_sym_db.RegisterMessage(BatteryQueryResponse) +_sym_db.RegisterMessage(OtaResult) -Ota4gSubCodeReq = _reflection.GeneratedProtocolMessageType('Ota4gSubCodeReq', (_message.Message,), { - 'DESCRIPTOR' : _OTA4GSUBCODEREQ, - '__module__' : 'tcp_messages_pb2' - # @@protoc_insertion_point(class_scope:tcp.Ota4gSubCodeReq) +FileStringMsg = _reflection.GeneratedProtocolMessageType('FileStringMsg', (_message.Message,), { + 'DESCRIPTOR' : _FILESTRINGMSG, + '__module__' : 'proto.tcp_messages_pb2' + # @@protoc_insertion_point(class_scope:tcp.FileStringMsg) }) -_sym_db.RegisterMessage(Ota4gSubCodeReq) +_sym_db.RegisterMessage(FileStringMsg) -Ota4gSubCodeResp = _reflection.GeneratedProtocolMessageType('Ota4gSubCodeResp', (_message.Message,), { - 'DESCRIPTOR' : _OTA4GSUBCODERESP, - '__module__' : 'tcp_messages_pb2' - # @@protoc_insertion_point(class_scope:tcp.Ota4gSubCodeResp) +IpReport = _reflection.GeneratedProtocolMessageType('IpReport', (_message.Message,), { + 'DESCRIPTOR' : _IPREPORT, + '__module__' : 'proto.tcp_messages_pb2' + # @@protoc_insertion_point(class_scope:tcp.IpReport) }) -_sym_db.RegisterMessage(Ota4gSubCodeResp) +_sym_db.RegisterMessage(IpReport) + +GetBatteryRequest = _reflection.GeneratedProtocolMessageType('GetBatteryRequest', (_message.Message,), { + 'DESCRIPTOR' : _GETBATTERYREQUEST, + '__module__' : 'proto.tcp_messages_pb2' + # @@protoc_insertion_point(class_scope:tcp.GetBatteryRequest) + }) +_sym_db.RegisterMessage(GetBatteryRequest) + +SetCenterPointRequest = _reflection.GeneratedProtocolMessageType('SetCenterPointRequest', (_message.Message,), { + 'DESCRIPTOR' : _SETCENTERPOINTREQUEST, + '__module__' : 'proto.tcp_messages_pb2' + # @@protoc_insertion_point(class_scope:tcp.SetCenterPointRequest) + }) +_sym_db.RegisterMessage(SetCenterPointRequest) ShutdownCommand = _reflection.GeneratedProtocolMessageType('ShutdownCommand', (_message.Message,), { 'DESCRIPTOR' : _SHUTDOWNCOMMAND, - '__module__' : 'tcp_messages_pb2' + '__module__' : 'proto.tcp_messages_pb2' # @@protoc_insertion_point(class_scope:tcp.ShutdownCommand) }) _sym_db.RegisterMessage(ShutdownCommand) -AutoShutdown = _reflection.GeneratedProtocolMessageType('AutoShutdown', (_message.Message,), { - 'DESCRIPTOR' : _AUTOSHUTDOWN, - '__module__' : 'tcp_messages_pb2' - # @@protoc_insertion_point(class_scope:tcp.AutoShutdown) +GetShootPicRequest = _reflection.GeneratedProtocolMessageType('GetShootPicRequest', (_message.Message,), { + 'DESCRIPTOR' : _GETSHOOTPICREQUEST, + '__module__' : 'proto.tcp_messages_pb2' + # @@protoc_insertion_point(class_scope:tcp.GetShootPicRequest) }) -_sym_db.RegisterMessage(AutoShutdown) +_sym_db.RegisterMessage(GetShootPicRequest) + +PushLogRequest = _reflection.GeneratedProtocolMessageType('PushLogRequest', (_message.Message,), { + 'DESCRIPTOR' : _PUSHLOGREQUEST, + '__module__' : 'proto.tcp_messages_pb2' + # @@protoc_insertion_point(class_scope:tcp.PushLogRequest) + }) +_sym_db.RegisterMessage(PushLogRequest) + +WifiStatusRequest = _reflection.GeneratedProtocolMessageType('WifiStatusRequest', (_message.Message,), { + 'DESCRIPTOR' : _WIFISTATUSREQUEST, + '__module__' : 'proto.tcp_messages_pb2' + # @@protoc_insertion_point(class_scope:tcp.WifiStatusRequest) + }) +_sym_db.RegisterMessage(WifiStatusRequest) if _descriptor._USE_C_DESCRIPTORS == False: DESCRIPTOR._options = None DESCRIPTOR._serialized_options = b'\n\rcom.shoot.tcpZ1git.shelingxingqiu.com/shoot-tcp-server/proto;tcp' - _LOGINREQUEST._serialized_start=28 - _LOGINREQUEST._serialized_end=159 - _LOGINRESPONSE._serialized_start=161 - _LOGINRESPONSE._serialized_end=203 - _HEARTBEAT._serialized_start=205 - _HEARTBEAT._serialized_end=257 - _LOGICBODY._serialized_start=259 - _LOGICBODY._serialized_end=297 - _RESPONSEBODY._serialized_start=299 - _RESPONSEBODY._serialized_end=340 - _MONITORBODY._serialized_start=342 - _MONITORBODY._serialized_end=406 - _MONITORDEVICESRESPONSE._serialized_start=408 - _MONITORDEVICESRESPONSE._serialized_end=452 - _OTAFRAGMENT._serialized_start=454 - _OTAFRAGMENT._serialized_end=511 - _SHOOTDATA._serialized_start=514 - _SHOOTDATA._serialized_end=813 - _SHOOTEVENT._serialized_start=815 - _SHOOTEVENT._serialized_end=848 - _BATTERYREPORT._serialized_start=850 - _BATTERYREPORT._serialized_end=917 - _CENTERPOINTRESULT._serialized_start=919 - _CENTERPOINTRESULT._serialized_end=976 - _CENTERPOINTSET._serialized_start=978 - _CENTERPOINTSET._serialized_end=1029 - _OTARESULT._serialized_start=1031 - _OTARESULT._serialized_end=1071 - _GENERICRESULT._serialized_start=1073 - _GENERICRESULT._serialized_end=1104 - _IPREPORT._serialized_start=1106 - _IPREPORT._serialized_end=1144 - _IMAGEUPLOADCOMMAND._serialized_start=1146 - _IMAGEUPLOADCOMMAND._serialized_end=1236 - _LOGUPLOADCOMMAND._serialized_start=1238 - _LOGUPLOADCOMMAND._serialized_end=1338 - _OTAREQUESTDATA._serialized_start=1340 - _OTAREQUESTDATA._serialized_end=1415 - _WIFICONNECTDATA._serialized_start=1417 - _WIFICONNECTDATA._serialized_end=1466 - _IMAGEUPLOADRESULT._serialized_start=1468 - _IMAGEUPLOADRESULT._serialized_end=1563 - _LOGUPLOADRESULT._serialized_start=1565 - _LOGUPLOADRESULT._serialized_end=1683 - _BATTERYQUERYRESPONSE._serialized_start=1685 - _BATTERYQUERYRESPONSE._serialized_end=1759 - _OTA4GSUBCODEREQ._serialized_start=1761 - _OTA4GSUBCODEREQ._serialized_end=1800 - _OTA4GSUBCODERESP._serialized_start=1802 - _OTA4GSUBCODERESP._serialized_end=1855 - _SHUTDOWNCOMMAND._serialized_start=1857 - _SHUTDOWNCOMMAND._serialized_end=1887 - _AUTOSHUTDOWN._serialized_start=1889 - _AUTOSHUTDOWN._serialized_end=1921 + _LOGINREQUEST._serialized_start=34 + _LOGINREQUEST._serialized_end=165 + _LOGINRESPONSE._serialized_start=167 + _LOGINRESPONSE._serialized_end=209 + _HEARTBEAT._serialized_start=211 + _HEARTBEAT._serialized_end=263 + _LOGICBODY._serialized_start=266 + _LOGICBODY._serialized_end=1597 + _MONITORBODY._serialized_start=1599 + _MONITORBODY._serialized_end=1663 + _MONITORDEVICESRESPONSE._serialized_start=1665 + _MONITORDEVICESRESPONSE._serialized_end=1709 + _OTAFRAGMENT._serialized_start=1711 + _OTAFRAGMENT._serialized_end=1768 + _SHOOTDATA._serialized_start=1771 + _SHOOTDATA._serialized_end=2070 + _SHOOTEVENT._serialized_start=2072 + _SHOOTEVENT._serialized_end=2105 + _BATTERYREPORT._serialized_start=2107 + _BATTERYREPORT._serialized_end=2192 + _AIMREQUEST._serialized_start=2194 + _AIMREQUEST._serialized_end=2206 + _CLOSEAIMREQUEST._serialized_start=2208 + _CLOSEAIMREQUEST._serialized_end=2225 + _OTAREQUEST._serialized_start=2227 + _OTAREQUEST._serialized_end=2298 + _OTA4GSUBCODEREQUEST._serialized_start=2300 + _OTA4GSUBCODEREQUEST._serialized_end=2343 + _OTA4GSUBCODERESPONSE._serialized_start=2345 + _OTA4GSUBCODERESPONSE._serialized_end=2389 + _WIFICONNECTREQUEST._serialized_start=2391 + _WIFICONNECTREQUEST._serialized_end=2443 + _CENTERPOINTRESULT._serialized_start=2445 + _CENTERPOINTRESULT._serialized_end=2502 + _CENTERPOINTSET._serialized_start=2504 + _CENTERPOINTSET._serialized_end=2542 + _CHARGINGREPORT._serialized_start=2544 + _CHARGINGREPORT._serialized_end=2560 + _GENERICRESULT._serialized_start=2562 + _GENERICRESULT._serialized_end=2593 + _IMAGEUPLOADCOMMAND._serialized_start=2595 + _IMAGEUPLOADCOMMAND._serialized_end=2685 + _LOGUPLOADCOMMAND._serialized_start=2687 + _LOGUPLOADCOMMAND._serialized_end=2787 + _IMAGEUPLOADRESULT._serialized_start=2789 + _IMAGEUPLOADRESULT._serialized_end=2884 + _LOGUPLOADRESULT._serialized_start=2886 + _LOGUPLOADRESULT._serialized_end=3004 + _OTARESULT._serialized_start=3006 + _OTARESULT._serialized_end=3079 + _FILESTRINGMSG._serialized_start=3081 + _FILESTRINGMSG._serialized_end=3140 + _IPREPORT._serialized_start=3142 + _IPREPORT._serialized_end=3180 + _GETBATTERYREQUEST._serialized_start=3182 + _GETBATTERYREQUEST._serialized_end=3201 + _SETCENTERPOINTREQUEST._serialized_start=3203 + _SETCENTERPOINTREQUEST._serialized_end=3248 + _SHUTDOWNCOMMAND._serialized_start=3250 + _SHUTDOWNCOMMAND._serialized_end=3267 + _GETSHOOTPICREQUEST._serialized_start=3269 + _GETSHOOTPICREQUEST._serialized_end=3307 + _PUSHLOGREQUEST._serialized_start=3309 + _PUSHLOGREQUEST._serialized_end=3325 + _WIFISTATUSREQUEST._serialized_start=3327 + _WIFISTATUSREQUEST._serialized_end=3346 # @@protoc_insertion_point(module_scope) diff --git a/version.py b/version.py index 3ccc2bc..877bded 100644 --- a/version.py +++ b/version.py @@ -4,6 +4,6 @@ 应用版本号 每次 OTA 更新时,只需要更新这个文件中的版本号 """ -VERSION = '3.0.5' +VERSION = '3.1.15'