6 Commits
Author SHA1 Message Date
linyimin 110ef12320 feat: 1.json换为pb 2.wifi、ota 优化 2026-09-22 10:33:52 +08:00
linyimin c3f1cfdea0 feat: TCP协议支持protobuf序列化,保留JSON兼容
- 新增 tcp_messages_pb2.py protobuf Python绑定
- msg_handler.cpp: 新增 make_packet_pb/raw bytes打包, parse_packet_raw/raw解析
- archery_netcore.cpp: 注册 make_packet_pb/parse_packet_raw
- network.py: 新增 _use_proto 标志,_make_send_packet/_parse_recv 自动选择proto/JSON
- 登录version加+proto后缀标识proto模式
- protobuf序列化/反序列化失败时自动回退JSON
2026-09-16 09:30:26 +08:00
linyimin 8457bd29c6 fix: 2026-09-04 11:14:17 +08:00
linyimin 5bde549d91 update: version 2026-09-04 11:11:41 +08:00
linyimin d96ca4d031 fix: 调整气压值 2026-09-02 14:19:04 +08:00
linyimin ced66682ed fix: 去除3秒内只能射箭一次的限制 2026-09-02 14:10:46 +08:00
11 changed files with 1156 additions and 1497 deletions
+2 -1
View File
@@ -1,6 +1,6 @@
id: t11 id: t11
name: t11 name: t11
version: 3.0.0 version: 3.1.15
author: t11 author: t11
icon: '' icon: ''
desc: t11 desc: t11
@@ -29,6 +29,7 @@ files:
- shoot_manager.py - shoot_manager.py
- shot_id_generator.py - shot_id_generator.py
- target_roi_yolo.py - target_roi_yolo.py
- tcp_messages_pb2.py
- time_sync.py - time_sync.py
- triangle_positions.json - triangle_positions.json
- triangle_target.py - triangle_target.py
Binary file not shown.
+2 -2
View File
@@ -50,7 +50,7 @@ WIFI_CONFIG_AP_IP = "192.168.66.1" # 与 MaixPy Wifi.start_ap 默认一
# ===== TCP over SSL(TLS) 配置 ===== # ===== TCP over SSL(TLS) 配置 =====
USE_TCP_SSL = True # True=按手册走 MSSLCFG/MIPCFG 绑定 SSL USE_TCP_SSL = True # True=按手册走 MSSLCFG/MIPCFG 绑定 SSL
TCP_LINK_ID = 2 # TCP_LINK_ID = 2 #
TCP_SSL_PORT = 50006 # TLS 端口(不一定必须 443,以服务器为准) TCP_SSL_PORT = 50007 # TLS 端口(不一定必须 443,以服务器为准)
# SSL profile # SSL profile
SSL_ID = 1 # ssl_id=1 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。 # 充电时自动关机暂时禁用;需要恢复时改为 True。
CHARGING_AUTO_POWER_OFF_ENABLED = False CHARGING_AUTO_POWER_OFF_ENABLED = False
+7
View File
@@ -57,9 +57,16 @@ PYBIND11_MODULE(archery_netcore, m) {
"Pack TCP packet: header (len+type+checksum) + JSON body", "Pack TCP packet: header (len+type+checksum) + JSON body",
py::arg("msg_type"), py::arg("body_dict")); py::arg("msg_type"), py::arg("body_dict"));
m.def("make_packet_pb", &netcore::make_packet_pb,
"Pack TCP packet: header (len+type+checksum) + raw bytes body (for protobuf)",
py::arg("msg_type"), py::arg("body_bytes"));
m.def("parse_packet", &netcore::parse_packet, m.def("parse_packet", &netcore::parse_packet,
"Parse TCP packet, return (msg_type, body_dict)"); "Parse TCP packet, return (msg_type, body_dict)");
m.def("parse_packet_raw", &netcore::parse_packet_raw,
"Parse TCP packet, return (msg_type, body_bytes) without JSON parsing");
m.def("get_config", &get_config, "Get system configuration"); m.def("get_config", &get_config, "Get system configuration");
m.def( m.def(
+59
View File
@@ -51,6 +51,43 @@ namespace netcore {
return py::bytes(reinterpret_cast<const char*>(packet.data()), packet.size()); return py::bytes(reinterpret_cast<const char*>(packet.data()), packet.size());
} }
// 打包 TCP 数据包 (raw bytes body, 用于 protobuf)
py::bytes make_packet_pb(int msg_type, py::bytes body_bytes) {
netcore::log_debug(std::string("make_packet_pb msg_type=") + std::to_string(msg_type));
py::buffer_info buf = py::buffer(body_bytes).request();
uint32_t body_len = buf.size;
uint32_t checksum = body_len + msg_type;
std::vector<uint8_t> packet;
packet.reserve(12 + body_len);
// body_len (big-endian, 4 bytes)
packet.push_back((body_len >> 24) & 0xFF);
packet.push_back((body_len >> 16) & 0xFF);
packet.push_back((body_len >> 8) & 0xFF);
packet.push_back(body_len & 0xFF);
// msg_type (big-endian, 4 bytes)
packet.push_back((msg_type >> 24) & 0xFF);
packet.push_back((msg_type >> 16) & 0xFF);
packet.push_back((msg_type >> 8) & 0xFF);
packet.push_back(msg_type & 0xFF);
// checksum (big-endian, 4 bytes)
packet.push_back((checksum >> 24) & 0xFF);
packet.push_back((checksum >> 16) & 0xFF);
packet.push_back((checksum >> 8) & 0xFF);
packet.push_back(checksum & 0xFF);
// 追加 body bytes
const uint8_t* body_ptr = static_cast<const uint8_t*>(buf.ptr);
packet.insert(packet.end(), body_ptr, body_ptr + body_len);
netcore::log_debug(std::string("make_packet_pb done bytes=") + std::to_string(packet.size()));
return py::bytes(reinterpret_cast<const char*>(packet.data()), packet.size());
}
// 解析 TCP 数据包 // 解析 TCP 数据包
py::tuple parse_packet(py::bytes data) { py::tuple parse_packet(py::bytes data) {
// 1) 转换为 bytes view // 1) 转换为 bytes view
@@ -110,4 +147,26 @@ namespace netcore {
return py::make_tuple(py::int_(msg_type), raw_dict); return py::make_tuple(py::int_(msg_type), raw_dict);
} }
} }
// 解析 TCP 数据包 -> (msg_type, body_bytes) 不做 JSON 解析
py::tuple parse_packet_raw(py::bytes data) {
py::buffer_info buf = py::buffer(data).request();
if (buf.size < 12) {
return py::make_tuple(py::none(), py::none());
}
const uint8_t* ptr = static_cast<const uint8_t*>(buf.ptr);
uint32_t body_len = (ptr[0] << 24) | (ptr[1] << 16) | (ptr[2] << 8) | ptr[3];
uint32_t msg_type = (ptr[4] << 24) | (ptr[5] << 16) | (ptr[6] << 8) | ptr[7];
uint32_t expected_len = 12 + body_len;
if (buf.size < expected_len) {
return py::make_tuple(py::none(), py::none());
}
// 返回原始 body bytes(不做 JSON 解析)
py::bytes body_bytes(reinterpret_cast<const char*>(ptr + 12), body_len);
return py::make_tuple(py::int_(msg_type), body_bytes);
}
} }
+9 -2
View File
@@ -7,8 +7,15 @@ namespace py = pybind11;
namespace netcore { namespace netcore {
// 打包 TCP 数据包 // 打包 TCP 数据包 (JSON body)
py::bytes make_packet(int msg_type, py::dict body_dict); py::bytes make_packet(int msg_type, py::dict body_dict);
// 解包 TCP 数据包
// 打包 TCP 数据包 (raw bytes body, 用于 protobuf)
py::bytes make_packet_pb(int msg_type, py::bytes body_bytes);
// 解包 TCP 数据包 -> (msg_type, body_dict)
py::tuple parse_packet(py::bytes data); py::tuple parse_packet(py::bytes data);
// 解包 TCP 数据包 -> (msg_type, body_bytes) 不做 JSON 解析
py::tuple parse_packet_raw(py::bytes data);
} }
+128 -20
View File
@@ -13,7 +13,9 @@ from maix import camera, display, image, app, time, uart, pinmap, i2c
from maix.peripheral import adc from maix.peripheral import adc
import _thread import _thread
import os import os
import sys
import json import json
import shutil
import time as wall_time import time as wall_time
# 导入新模块 # 导入新模块
@@ -126,7 +128,7 @@ def cmd_str():
# 1. 初始化日志系统(WARNING级别,不打印/写入INFO和DEBUG日志,提高执行流畅度) # 1. 初始化日志系统(WARNING级别,不打印/写入INFO和DEBUG日志,提高执行流畅度)
import logging import logging
logger_manager.init_logging(log_level=logging.WARNING) logger_manager.init_logging(log_level=logging.DEBUG)
logger = logger_manager.logger logger = logger_manager.logger
# 补充:因为初始化的时候,激光会亮,先关了它 # 补充:因为初始化的时候,激光会亮,先关了它
@@ -136,6 +138,7 @@ def cmd_str():
sync_system_time_from_4g() sync_system_time_from_4g()
# 2.1 WiFi 热点配网兜底:仅当 STA 与 4G 均不可用时起 AP + HTTP;提交后删 /boot/wifi.ap、建 wifi.sta 并 reboot # 2.1 WiFi 热点配网兜底:仅当 STA 与 4G 均不可用时起 AP + HTTP;提交后删 /boot/wifi.ap、建 wifi.sta 并 reboot
_ota_pending_path = f"{config.APP_DIR}/ota_pending.json"
try: try:
from wifi_config_httpd import maybe_start_wifi_ap_fallback from wifi_config_httpd import maybe_start_wifi_ap_fallback
@@ -253,6 +256,55 @@ def cmd_str():
# 4. 初始化设备IDnetwork_manager 内部会自动设置 device_id 和 password # 4. 初始化设备IDnetwork_manager 内部会自动设置 device_id 和 password
network_manager.read_device_id() 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. 创建照片存储目录(如果启用图像保存或检测失败时强制保存) # 5. 创建照片存储目录(如果启用图像保存或检测失败时强制保存)
if config.SAVE_IMAGE_ENABLED or getattr(config, "SAVE_IMAGE_ON_FAILURE", False): if config.SAVE_IMAGE_ENABLED or getattr(config, "SAVE_IMAGE_ON_FAILURE", False):
photo_dir = config.PHOTO_DIR photo_dir = config.PHOTO_DIR
@@ -286,12 +338,14 @@ def cmd_str():
logger.info("系统准备完成...") logger.info("系统准备完成...")
last_adc_trigger = 0 last_adc_trigger = 0
trigger_adc_val = 0 # 触发时的气压值,气压需降回此值以下才能再次触发
# 读取一次ADC初始值,防止开机时传感器已有压力导致误触发 # 读取一次ADC初始值,防止开机时传感器已有压力导致误触发
enable_check = True
_should_reboot = False
try: try:
last_adc_val = hardware_manager.adc_obj.read() last_adc_val = hardware_manager.adc_obj.read()
except Exception: except Exception:
last_adc_val = 0 last_adc_val = 0
peak_adc_val = 0 # 当前周期内的压力峰值
# 气压采样:减少日志频率(每 N 个点输出一条),避免 logger.debug 拖慢采样 # 气压采样:减少日志频率(每 N 个点输出一条),避免 logger.debug 拖慢采样
PRESSURE_BATCH_SIZE = 100 PRESSURE_BATCH_SIZE = 100
@@ -343,6 +397,14 @@ def cmd_str():
time.sleep_ms(250) time.sleep_ms(250)
continue 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 状态下,检测是否空闲足够长,自动关机 # 不在 OTA 状态下,检测是否空闲足够长,自动关机
# print(f"[MAIN] 空闲时间: {hardware_manager.get_idle_time_in_sec() }秒") # print(f"[MAIN] 空闲时间: {hardware_manager.get_idle_time_in_sec() }秒")
# print(f"配置关机时间:{config.AUTO_POWER_OFF_IN_SECONDS} 秒") # print(f"配置关机时间:{config.AUTO_POWER_OFF_IN_SECONDS} 秒")
@@ -381,22 +443,16 @@ def cmd_str():
pressure_max = adc_val pressure_max = adc_val
if len(pressure_buf) >= PRESSURE_BATCH_SIZE: if len(pressure_buf) >= PRESSURE_BATCH_SIZE:
_flush_pressure_buf("batch") _flush_pressure_buf("batch")
# 峰值检测:压力从峰值下降时触发,确保捕获到最大冲击时刻 # 突变增量检测:压力增量大于300时触发
if adc_val > peak_adc_val: # 触发后需等气压降到触发值以下才重新检测增量
peak_adc_val = adc_val # 更新峰值 if adc_val < trigger_adc_val :
if (peak_adc_val >= config.ADC_TRIGGER_THRESHOLD enable_check = True
and adc_val < peak_adc_val if (adc_val - last_adc_val) > 200 and enable_check:
and last_adc_val >= peak_adc_val):
# 封顶后下降沿触发:peak是最大值,当前值开始下降,且上次值还在peak位置
hardware_manager.start_idle_timer() # 重新计时 hardware_manager.start_idle_timer() # 重新计时
diff_ms = current_time - last_adc_trigger
if diff_ms < 3000:
peak_adc_val = 0 # 去抖期间重置峰值
time.sleep_ms(5)
continue
last_adc_trigger = current_time last_adc_trigger = current_time
peak_adc_val = 0 # 触发后重置峰 trigger_adc_val = adc_val # 记录触发时的气压
# 触发前先把缓存刷出来,避免波形被长耗时处理截断 last_adc_val = adc_val # 更新基准值,防止连续增量误触发
enable_check = False
_flush_pressure_buf("before_trigger") _flush_pressure_buf("before_trigger")
try: try:
@@ -415,7 +471,7 @@ def cmd_str():
camera_manager.show(camera_manager.read_frame()) camera_manager.show(camera_manager.read_frame())
except Exception as e: except Exception as e:
pass pass
time.sleep_ms(5) time.sleep_ms(1)
last_adc_val = adc_val last_adc_val = adc_val
except Exception as e: except Exception as e:
@@ -434,10 +490,62 @@ def cmd_str():
_flush_pressure_buf("exception") _flush_pressure_buf("exception")
except: except:
pass 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")])
# 主程序入口 # 主程序入口
+378 -230
View File
@@ -13,6 +13,7 @@ import hmac
import hashlib import hashlib
import ujson import ujson
import os import os
import sys
import threading import threading
import socket import socket
import config import config
@@ -21,7 +22,13 @@ from hardware import hardware_manager
from power import get_bus_voltage, voltage_to_percent, is_charging from power import get_bus_voltage, voltage_to_percent, is_charging
from logger_manager import logger_manager from logger_manager import logger_manager
from wifi import wifi_manager from wifi import wifi_manager
import subprocess
# protobuf 支持(纯 proto 协议,必须可用)
try:
import tcp_messages_pb2 as pb
except ImportError:
print("[NET] tcp_messages_pb2 not found, protobuf disabled")
raise
def _wifi_tls_would_block(exc): def _wifi_tls_would_block(exc):
@@ -69,9 +76,18 @@ class NetworkManager:
self._uart4g_lock = threading.Lock() self._uart4g_lock = threading.Lock()
self._device_id = None self._device_id = None
self._password = None self._password = None
self._raw_line_data = []
self._manual_trigger_flag = False self._manual_trigger_flag = False
# 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() self._cmd_thread_lock = threading.Lock()
self._cmd_thread_count = 0 self._cmd_thread_count = 0
@@ -190,13 +206,7 @@ class NetworkManager:
return self._normal_send_queue.pop(0) return self._normal_send_queue.pop(0)
return None 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): def get_uart_lock(self):
"""获取UART锁(用于with语句)""" """获取UART锁(用于with语句)"""
@@ -615,53 +625,113 @@ class NetworkManager:
except Exception as e: except Exception as e:
self.logger.error(f"[LASER] cmd200 检测异常: {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): def _cmd300_ota(self, data_obj):
"""后台线程执行 cmd300 OTA,避免阻塞主循环""" """后台线程执行 cmd300 OTA,避免阻塞主循环
流程:检查WiFi → 下载ZIP → 解压覆盖项目 → 重启程序
"""
hardware_manager.start_idle_timer() hardware_manager.start_idle_timer()
inner_data = data_obj.get("data", {}) if isinstance(data_obj, dict) else {} inner_data = data_obj.get("data", {}) if isinstance(data_obj, dict) else {}
self.logger.info(f"[New Ota] cmd300 , data: {inner_data}") 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") ota_res_url = inner_data.get("url")
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: try:
for _f in ("/etc/wpa_supplicant.conf", "/boot/wpa_supplicant.conf", "/boot/wifi.ssid", "/boot/wifi.pass"): pkt = self._make_send_packet(2, {"cmd": 300, "result": "ota skip", "reason": f"cooldown {remaining}s"})
try: self.tcp_send_raw(pkt)
os.remove(_f) except Exception:
except OSError:
pass pass
w = network.wifi.Wifi() return
e = w.connect(ssid, password, wait=True, timeout=15)
err.check_raise(e, "connect wifi failed") if not wifi_manager.is_wifi_connected():
if self.logger: self.logger.warning("[ota] cmd300 当前未连接WiFi,拒绝OTA")
self.logger.info(f"[ota] Connect success, got ip{w.get_ip()}") self.safe_enqueue({"cmd": 300, "result": "ota fail", "reason": "wifi not connected"}, 2)
self.safe_enqueue( 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, "cmd": 300,
"result": "ota start...", "result": f"ota {phase}",
"wifi": w.get_ip(), "progress": progress,
}, "phase": phase,
2, })
) ok = self.tcp_send_raw(pkt)
subprocess.run( if not ok:
["sh", "/maixapp/apps/t11/ota_curl.sh", ota_res_url]) self.logger.warning(f"[ota] 进度发送失败: phase={phase} progress={progress}")
self.safe_enqueue(
{
"cmd": 300,
"result": "success",
"wifi": w.get_ip(),
},
2,
)
except Exception as e: except Exception as e:
self.logger.error(f"[ota] cmd300 失败: {e}") self.logger.error(f"[ota] 发送进度异常: {e}")
self.safe_enqueue(
{ try:
"cmd": 300, from ota_manager import ota_manager
"result": "ota fail", ok, msg = ota_manager.perform_ota(ota_res_url, progress_callback=_ota_progress)
"reason": str(e), self._last_ota_time = time.time()
}, if ok:
2, 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._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): def _cmd600_conn_wifi(self, data_obj):
hardware_manager.start_idle_timer() hardware_manager.start_idle_timer()
@@ -669,6 +739,7 @@ class NetworkManager:
self.logger.info(f"[conn wifi] cmd600 , data: {inner_data}") self.logger.info(f"[conn wifi] cmd600 , data: {inner_data}")
ssid = inner_data.get("ssid") ssid = inner_data.get("ssid")
password = inner_data.get("password") password = inner_data.get("password")
prev_network_type = self._network_type
# 停止旧的WiFi质量监测(无论当前是WiFi还是4G连接) # 停止旧的WiFi质量监测(无论当前是WiFi还是4G连接)
self._stop_wifi_quality_monitor() self._stop_wifi_quality_monitor()
try: try:
@@ -678,18 +749,10 @@ class NetworkManager:
except OSError: except OSError:
pass pass
w = network.wifi.Wifi() 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") err.check_raise(e, "connect wifi failed")
if self.logger: if self.logger:
self.logger.info(f"[ota] Connect success, got ip{w.get_ip()}") 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._session_force_4g = False
self.disconnect_server() self.disconnect_server()
self._tcp_connected = False self._tcp_connected = False
@@ -697,6 +760,11 @@ class NetworkManager:
self.logger.info("[conn wifi] WiFi已连接,等待主循环重新登录") self.logger.info("[conn wifi] WiFi已连接,等待主循环重新登录")
except Exception as e: except Exception as e:
self.logger.error(f"cmd600 失败: {e}") self.logger.error(f"cmd600 失败: {e}")
# 同步发送失败结果(旧连接仍存活时直接发送)
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( self.safe_enqueue(
{ {
"cmd": 600, "cmd": 600,
@@ -705,12 +773,173 @@ class NetworkManager:
}, },
2, 2,
) )
# 当前是4G在线,旧连接未断,无需切换;当前是WiFi,旧WiFi已被w.connect()断开,需回退4G
if prev_network_type == "wifi":
self._switch_to_4g_due_to_poor_wifi() self._switch_to_4g_due_to_poor_wifi()
def safe_enqueue(self, data_dict, msg_type=2, high=False): def safe_enqueue(self, data_dict, msg_type=2, high=False):
"""线程安全地将消息加入队列(公共方法)""" """线程安全地将消息加入队列(公共方法)"""
self._enqueue((msg_type, data_dict), high) self._enqueue((msg_type, data_dict), high)
def _make_send_packet(self, 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 序列化构造数据包"""
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)
def _parse_recv(self, payload):
"""解析接收的数据包,返回 (msg_type, body_dict)"""
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.code, "data": msg.msg}
elif msg_type == 4:
return {}
elif msg_type == 2:
msg = pb.LogicBody()
msg.ParseFromString(body_bytes)
result = {"cmd": msg.cmd}
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 == 100:
msg = pb.ImageUploadCommand()
msg.ParseFromString(body_bytes)
return {"uploadUrl": msg.upload_url, "token": msg.token, "shootId": msg.shoot_id, "outlink": msg.outlink}
elif msg_type == 101:
msg = pb.LogUploadCommand()
msg.ParseFromString(body_bytes)
return {"uploadUrl": msg.upload_url, "token": msg.token, "key": msg.key, "outlink": msg.outlink, "archive": msg.archive}
else:
return {"raw": body_bytes.hex()}
def connect_server(self): def connect_server(self):
""" """
连接到服务器(自动选择WiFi或4G) 连接到服务器(自动选择WiFi或4G)
@@ -1833,6 +2062,11 @@ class NetworkManager:
time.sleep_ms(200) time.sleep_ms(200)
continue continue
# OTA 完成后需要重启,从主循环退出(由 main.py 执行重启)
if self.ota_restart_pending:
self.logger.info("[ota] 主循环退出,准备重启...")
break
if not self.connect_server(): if not self.connect_server():
time.sleep_ms(1000) time.sleep_ms(1000)
continue continue
@@ -1848,8 +2082,7 @@ class NetworkManager:
} }
iccid_pending_marker = self._maybe_add_iccid_to_login(login_data) iccid_pending_marker = self._maybe_add_iccid_to_login(login_data)
print(f"login_data: {login_data}") print(f"login_data: {login_data}")
# if not self.tcp_send_raw(self.make_packet(1, login_data)): if not self.tcp_send_raw(self._make_send_packet(1, login_data)):
if not self.tcp_send_raw(self._netcore.make_packet(1, login_data)):
self._tcp_connected = False self._tcp_connected = False
try: try:
self.disconnect_server() self.disconnect_server()
@@ -1880,6 +2113,10 @@ class NetworkManager:
time.sleep_ms(200) time.sleep_ms(200)
continue continue
# OTA 完成后需要重启,跳出内层循环
if self.ota_restart_pending:
break
# 接收数据(根据网络类型选择接收方式) # 接收数据(根据网络类型选择接收方式)
# WiFi 粘包:一次 recv 可能含多条完整包;也可能缓冲里已有完整包但本轮 recv 超时为空 # WiFi 粘包:一次 recv 可能含多条完整包;也可能缓冲里已有完整包但本轮 recv 超时为空
rx_items = [] rx_items = []
@@ -1926,11 +2163,11 @@ class NetworkManager:
pass pass
# msg_type, body = self.parse_packet(payload) # msg_type, body = self.parse_packet(payload)
msg_type, body = self._netcore.parse_packet(payload) msg_type, body = self._parse_recv(payload)
# 处理登录响应 # 处理登录响应
if not logged_in and msg_type == 1: 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 logged_in = True
last_heartbeat_ack_time = time.ticks_ms() last_heartbeat_ack_time = time.ticks_ms()
self.logger.info("登录成功") self.logger.info("登录成功")
@@ -1961,32 +2198,7 @@ class NetworkManager:
last_heartbeat_ack_time = time.ticks_ms() last_heartbeat_ack_time = time.ticks_ms()
self.logger.debug("✅ 收到心跳确认") 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: elif logged_in and msg_type == 100:
self.logger.info(f"[IMAGE_UPLOAD] 收到图片上传命令 {body}") self.logger.info(f"[IMAGE_UPLOAD] 收到图片上传命令 {body}")
@@ -2079,89 +2291,44 @@ class NetworkManager:
) )
# 立即返回已入队确认 # 立即返回已入队确认
self.safe_enqueue({"result": "log_upload_queued"}, 2) self.safe_enqueue({"result": "log_upload_queued"}, 2)
elif logged_in and msg_type == 201: # 处理业务指令(纯 proto: cmd 在 body 顶层)
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()
# 处理业务指令
elif logged_in and isinstance(body, dict): elif logged_in and isinstance(body, dict):
inner_cmd = None cmd = body.get("cmd")
data_obj = body.get("data") data_obj = body.get("data") or {}
if isinstance(data_obj, dict): if cmd == 2: # AimRequest 开启激光并校准
inner_cmd = data_obj.get("cmd")
if inner_cmd == 2: # 开启激光并校准
from laser_manager import laser_manager from laser_manager import laser_manager
if not laser_manager.calibration_active: if not laser_manager.calibration_active:
laser_manager.turn_on_laser() laser_manager.turn_on_laser()
time.sleep_ms(100) time.sleep_ms(100)
hardware_manager.stop_idle_timer() # 停表 hardware_manager.stop_idle_timer()
if not config.HARDCODE_LASER_POINT: if not config.HARDCODE_LASER_POINT:
laser_manager.start_calibration() laser_manager.start_calibration()
self.safe_enqueue({"result": "calibrating"}, 2) self.safe_enqueue({"result": "calibrating"}, 2)
else: else:
# 写死的逻辑,不需要校准激光点
self.safe_enqueue({"result": "laser pos set by hard code"}, 2) self.safe_enqueue({"result": "laser pos set by hard code"}, 2)
elif inner_cmd == 3: # 关闭激光 elif cmd == 3: # CloseAimRequest 关闭激光
from laser_manager import laser_manager from laser_manager import laser_manager
laser_manager.turn_off_laser() laser_manager.turn_off_laser()
laser_manager.stop_calibration() laser_manager.stop_calibration()
hardware_manager.start_idle_timer() # 开表 hardware_manager.start_idle_timer()
self.safe_enqueue({"result": "laser_off"}, 2) self.safe_enqueue({"result": "laser_off"}, 2)
elif inner_cmd == 4: # 上报电量 elif cmd == 4: # GetBatteryRequest 上报电量
voltage = get_bus_voltage() voltage = get_bus_voltage()
battery_percent = voltage_to_percent(voltage) battery_percent = voltage_to_percent(voltage)
battery_data = { charging = is_charging()
self.safe_enqueue({
"cmd": 4,
"battery": battery_percent, "battery": battery_percent,
"voltage": round(float(voltage), 3), "voltage": round(float(voltage), 3),
"netType": self.network_type, "netType": self.network_type,
} "charging": charging,
self.safe_enqueue(battery_data, 2) }, 2)
self.logger.info(f"电量上报: {battery_percent}% 充电: {is_charging()}") self.logger.info(f"电量上报: {battery_percent}% 充电: {charging}")
if getattr(config, "CHARGING_AUTO_POWER_OFF_ENABLED", False) and is_charging(): elif cmd == 700:
self.safe_enqueue(
{
"cmd": 700,
},
2,
)
elif inner_cmd == 700:
self.logger.warning("服务器下发关机!!!") self.logger.warning("服务器下发关机!!!")
exit(-1) exit(-1)
elif inner_cmd == 5: # OTA 升级 elif cmd == 5: # OtaRequest OTA 升级
inner_data = data_obj.get("data", {}) if isinstance(data_obj, dict) else {} ota_url = data_obj.get("url", "")
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: if not ota_url:
self.logger.error("ota missing_url") self.logger.error("ota missing_url")
@@ -2175,119 +2342,100 @@ class NetworkManager:
_rx_skip_tcp_iteration = True _rx_skip_tcp_iteration = True
break break
# 自动判断模式:如果没有明确指定,根据WiFi连接状态和凭证决定 if not wifi_manager.is_wifi_connected():
if mode not in ("4g", "wifi"): self.logger.warning("[ota] cmd5 当前未连接WiFi,拒绝OTA")
self.logger.info("ota missing mode, auto-detecting...") self.safe_enqueue({"result": "ota fail", "reason": "wifi not connected"}, 2)
# 若本次会话已锁定 4G,则 OTA 自动也走 4G,避免后续回切导致体验不一致 _rx_skip_tcp_iteration = True
if self._session_force_4g: break
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后面会重启,会重新开表 hardware_manager.stop_idle_timer()
self._spawn_cmd_thread(self._cmd5_ota, (ota_url,))
if mode == "4g": elif cmd == 41: # Ota4gSubCodeRequest 射箭触发
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: # 读 MCCIDAT+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.logger.info(f"[TEST] 收到TCP射箭触发命令, {time.time()}")
self._manual_trigger_flag = True self._manual_trigger_flag = True
self.safe_enqueue({"result": "trigger_ack"}, 2) self.safe_enqueue({"result": "trigger_ack"}, 2)
hardware_manager.start_idle_timer() # 重新计时 hardware_manager.start_idle_timer()
elif inner_cmd == 42: # 关机命令 elif cmd == 42: # ShutdownCommand 关机命令
self.logger.info("[SHUTDOWN] 收到TCP关机命令,准备关机...") self.logger.info("[SHUTDOWN] 收到TCP关机命令,准备关机...")
self.safe_enqueue({"result": "shutdown_ack"}, 2) self.safe_enqueue({"result": "shutdown_ack"}, 2)
time.sleep_ms(1000) time.sleep_ms(1000)
self.disconnect_server() self.disconnect_server()
# 尝试关闭4G模块
try: try:
with self.get_uart_lock(): with self.get_uart_lock():
hardware_manager.at_client.send("AT+CFUN=0", "OK", 5000) hardware_manager.at_client.send("AT+CFUN=0", "OK", 5000)
except: except:
pass pass
time.sleep_ms(2000) time.sleep_ms(2000)
os.system("sync") # 刷新文件系统缓存到磁盘,防止数据丢失 os.system("sync")
time.sleep_ms(500) time.sleep_ms(500)
# os.system("poweroff")
hardware_manager.power_off() hardware_manager.power_off()
return return
elif inner_cmd == 43: # 上传日志命令 elif cmd == 44: # 读 4G 本机号码
# 格式: {"cmd":43, "data":{"ssid":"xxx","password":"xxx","url":"xxx", ...}} cnum = self.get_4g_phone_number()
inner_data = data_obj.get("data", {}) self.logger.info(f"4G 本机号码: {cnum}")
upload_url = inner_data.get("url") self.safe_enqueue(
wifi_ssid = inner_data.get("ssid") {"result": "cnum", "number": cnum if cnum is not None else ""}, 2)
wifi_password = inner_data.get("password") elif cmd == 45: # 读 MCCID
include_rotated = inner_data.get("include_rotated", True) mccid = self.get_4g_mccid()
max_files = inner_data.get("max_files") self.logger.info(f"4G MCCID: {mccid}")
archive_format = inner_data.get("archive", "tgz") # tgz 或 zip 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() # 重新计时 hardware_manager.start_idle_timer()
if not upload_url: if not upload_url:
self.logger.error("[LOG_UPLOAD] 缺少 url 参数") self.logger.error("[LOG_UPLOAD] 缺少 url 参数")
self.safe_enqueue({"result": "log_upload_failed", "reason": "missing_url"}, self.safe_enqueue({"result": "log_upload_failed", "reason": "missing_url"}, 2)
2)
else: else:
self.logger.info(f"[LOG_UPLOAD] 收到日志上传命令,目标URL: {upload_url}") self.logger.info(f"[LOG_UPLOAD] 收到日志上传命令,目标URL: {upload_url}")
# 在新线程中执行上传,避免阻塞主循环
self._spawn_cmd_thread( self._spawn_cmd_thread(
self._upload_log_file, self._upload_log_file,
(upload_url, wifi_ssid, wifi_password, include_rotated, max_files, (upload_url, wifi_ssid, wifi_password, include_rotated, max_files,
archive_format) archive_format)
) )
elif inner_cmd == 200: elif cmd == 200: # GenericResult "init_center_point" 触发激光检测
self.logger.info("[LASER] cmd200 在后台线程执行检测") self.logger.info("[LASER] cmd200 在后台线程执行检测")
self._spawn_cmd_thread(self._cmd200_detect_laser, ()) self._spawn_cmd_thread(self._cmd200_detect_laser, ())
elif inner_cmd == 300: 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.logger.info("[New Ota] cmd300 在后台线程执行OTA")
self._spawn_cmd_thread(self._cmd300_ota, (data_obj,)) self._spawn_cmd_thread(self._cmd300_ota, ({"data": data_obj},))
elif inner_cmd == 600: elif cmd == 600: # WifiConnectRequest 连接wifi
self.logger.info("[conn wifi] cmd600 在后台线程执行连接wifi: {data_obj}") self.logger.info(f"[conn wifi] cmd600 在后台线程执行连接wifi: {data_obj}")
self._spawn_cmd_thread(self._cmd600_conn_wifi, (data_obj,)) self._spawn_cmd_thread(self._cmd600_conn_wifi, ({"data": data_obj},))
elif inner_cmd == 601: elif cmd == 601:
pass pass
else: # data的结构不是 dict
self.logger.info(f"[NET] body={body}, {time.time()}")
else: else:
self.logger.info(f"[NET] 未知数据 {body}, {time.time()}") self.logger.info(f"[NET] body={body}, {time.time()}")
if _rx_login_fail: if _rx_login_fail:
break break
if _rx_skip_tcp_iteration: if _rx_skip_tcp_iteration:
@@ -2310,7 +2458,7 @@ class NetworkManager:
if item: if item:
msg_type, data_dict = item msg_type, data_dict = item
pkt = self._netcore.make_packet(msg_type, data_dict) pkt = self._make_send_packet(msg_type, data_dict)
if not self.tcp_send_raw(pkt): if not self.tcp_send_raw(pkt):
# 发送失败:将消息放回队首(队列满则丢弃) # 发送失败:将消息放回队首(队列满则丢弃)
with self.get_queue_lock(): with self.get_queue_lock():
@@ -2339,8 +2487,8 @@ class NetworkManager:
current_time = time.ticks_ms() current_time = time.ticks_ms()
if logged_in and current_time - last_heartbeat_send_time > config.HEARTBEAT_INTERVAL * 1000: if logged_in and current_time - last_heartbeat_send_time > config.HEARTBEAT_INTERVAL * 1000:
vol_val = get_bus_voltage() vol_val = get_bus_voltage()
if not self.tcp_send_raw( heartbeat_pkt = self._make_send_packet(4, {"vol": vol_val, "vol_per": voltage_to_percent(vol_val)})
self._netcore.make_packet(4, {"vol": vol_val, "vol_per": voltage_to_percent(vol_val)})): if not self.tcp_send_raw(heartbeat_pkt):
# if not self.tcp_send_raw(self.make_packet(4, {"vol": vol_val, "vol_per": voltage_to_percent(vol_val)})): # if not self.tcp_send_raw(self.make_packet(4, {"vol": vol_val, "vol_per": voltage_to_percent(vol_val)})):
send_hartbeat_fail_count += 1 send_hartbeat_fail_count += 1
# 短暂波动可能导致一次发送失败:连续失败达到阈值才重连,避免重连风暴 # 短暂波动可能导致一次发送失败:连续失败达到阈值才重连,避免重连风暴
+157 -1183
View File
File diff suppressed because it is too large Load Diff
+355
View File
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -4,6 +4,6 @@
应用版本号 应用版本号
每次 OTA 更新时,只需要更新这个文件中的版本号 每次 OTA 更新时,只需要更新这个文件中的版本号
""" """
VERSION = '3.0.0' VERSION = '3.1.15'