Compare commits
11
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9be7cffbb2 | ||
|
|
e1f12ae609 | ||
|
|
56fa8fc2a1 | ||
|
|
d3c8a26854 | ||
|
|
eae7da7291 | ||
|
|
dc5da0294f | ||
|
|
165eeff64e | ||
|
|
a184ff7d55 | ||
|
|
054e9e6d90 | ||
|
|
ae339889c2 | ||
|
|
b94b0f2e55 |
@@ -1,3 +1,4 @@
|
|||||||
/cpp_ext/build/
|
/cpp_ext/build/
|
||||||
/.cursor/
|
/.cursor/
|
||||||
/dist/
|
/dist/
|
||||||
|
.idea
|
||||||
Generated
-8
@@ -1,8 +0,0 @@
|
|||||||
# 默认忽略的文件
|
|
||||||
/shelf/
|
|
||||||
/workspace.xml
|
|
||||||
# 基于编辑器的 HTTP 客户端请求
|
|
||||||
/httpRequests/
|
|
||||||
# Datasource local storage ignored files
|
|
||||||
/dataSources/
|
|
||||||
/dataSources.local.xml
|
|
||||||
Generated
-12
@@ -1,12 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<module type="PYTHON_MODULE" version="4">
|
|
||||||
<component name="NewModuleRootManager">
|
|
||||||
<content url="file://$MODULE_DIR$" />
|
|
||||||
<orderEntry type="jdk" jdkName="yolov8" jdkType="Python SDK" />
|
|
||||||
<orderEntry type="sourceFolder" forTests="false" />
|
|
||||||
</component>
|
|
||||||
<component name="PyDocumentationSettings">
|
|
||||||
<option name="format" value="PLAIN" />
|
|
||||||
<option name="myDocStringFormat" value="Plain" />
|
|
||||||
</component>
|
|
||||||
</module>
|
|
||||||
-6
@@ -1,6 +0,0 @@
|
|||||||
<component name="InspectionProjectProfileManager">
|
|
||||||
<settings>
|
|
||||||
<option name="USE_PROJECT_PROFILE" value="false" />
|
|
||||||
<version value="1.0" />
|
|
||||||
</settings>
|
|
||||||
</component>
|
|
||||||
Generated
-7
@@ -1,7 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<project version="4">
|
|
||||||
<component name="Black">
|
|
||||||
<option name="sdkName" value="yolov8" />
|
|
||||||
</component>
|
|
||||||
<component name="ProjectRootManager" version="2" project-jdk-name="yolov8" project-jdk-type="Python SDK" />
|
|
||||||
</project>
|
|
||||||
Generated
-8
@@ -1,8 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<project version="4">
|
|
||||||
<component name="ProjectModuleManager">
|
|
||||||
<modules>
|
|
||||||
<module fileurl="file://$PROJECT_DIR$/.idea/archery.iml" filepath="$PROJECT_DIR$/.idea/archery.iml" />
|
|
||||||
</modules>
|
|
||||||
</component>
|
|
||||||
</project>
|
|
||||||
Generated
-6
@@ -1,6 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<project version="4">
|
|
||||||
<component name="VcsDirectoryMappings">
|
|
||||||
<mapping directory="" vcs="Git" />
|
|
||||||
</component>
|
|
||||||
</project>
|
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -1,6 +1,6 @@
|
|||||||
id: t11
|
id: t11
|
||||||
name: t11
|
name: t11
|
||||||
version: 2.15.18
|
version: 2.15.35
|
||||||
author: t11
|
author: t11
|
||||||
icon: ''
|
icon: ''
|
||||||
desc: t11
|
desc: t11
|
||||||
@@ -19,6 +19,8 @@ files:
|
|||||||
- laser_manager.py
|
- laser_manager.py
|
||||||
- logger_manager.py
|
- logger_manager.py
|
||||||
- main.py
|
- main.py
|
||||||
|
- model_285484.cvimodel
|
||||||
|
- model_285484.mud
|
||||||
- network.py
|
- network.py
|
||||||
- ota_curl.sh
|
- ota_curl.sh
|
||||||
- ota_manager.py
|
- ota_manager.py
|
||||||
|
|||||||
+37
-1
@@ -69,7 +69,7 @@ class ATClient:
|
|||||||
# 同上:避免在 _reader_loop 持锁期间二次 acquire
|
# 同上:避免在 _reader_loop 持锁期间二次 acquire
|
||||||
self._http_events.append(ev)
|
self._http_events.append(ev)
|
||||||
|
|
||||||
def send(self, cmd: str, expect: str = "OK", timeout_ms: int = 2000):
|
def send(self, cmd: str, expect: str = "OK", timeout_ms: int = 2000, abort_event=None):
|
||||||
"""
|
"""
|
||||||
发送 AT 命令并等待 expect(子串匹配)。
|
发送 AT 命令并等待 expect(子串匹配)。
|
||||||
注意:expect=">" 用于等待 prompt。
|
注意:expect=">" 用于等待 prompt。
|
||||||
@@ -90,6 +90,9 @@ class ATClient:
|
|||||||
|
|
||||||
t0 = time.ticks_ms()
|
t0 = time.ticks_ms()
|
||||||
while abs(time.ticks_diff(time.ticks_ms(), t0)) < timeout_ms:
|
while abs(time.ticks_diff(time.ticks_ms(), t0)) < timeout_ms:
|
||||||
|
if abort_event is not None and abort_event.is_set():
|
||||||
|
self._waiting = False
|
||||||
|
break
|
||||||
if (not self._waiting) or (self._expect in self._resp):
|
if (not self._waiting) or (self._expect in self._resp):
|
||||||
self._waiting = False
|
self._waiting = False
|
||||||
break
|
break
|
||||||
@@ -102,6 +105,39 @@ class ATClient:
|
|||||||
except:
|
except:
|
||||||
return str(self._resp)
|
return str(self._resp)
|
||||||
|
|
||||||
|
def send_raw_and_wait(self, data: bytes, expect: str = "OK", timeout_ms: int = 1000,
|
||||||
|
suffix: bytes = b""):
|
||||||
|
"""Register the response waiter before writing raw UART data."""
|
||||||
|
expect_b = expect.encode() if isinstance(expect, str) else expect
|
||||||
|
with self._cmd_lock:
|
||||||
|
with self._q_lock:
|
||||||
|
self._waiting = True
|
||||||
|
self._expect = expect_b
|
||||||
|
self._resp = b""
|
||||||
|
|
||||||
|
total = 0
|
||||||
|
while total < len(data):
|
||||||
|
n = self.uart.write(data[total:])
|
||||||
|
if not n or n < 0:
|
||||||
|
time.sleep_ms(1)
|
||||||
|
continue
|
||||||
|
total += n
|
||||||
|
if suffix:
|
||||||
|
self.uart.write(suffix)
|
||||||
|
|
||||||
|
t0 = time.ticks_ms()
|
||||||
|
while abs(time.ticks_diff(time.ticks_ms(), t0)) < timeout_ms:
|
||||||
|
if (not self._waiting) or (self._expect in self._resp):
|
||||||
|
self._waiting = False
|
||||||
|
break
|
||||||
|
time.sleep_ms(5)
|
||||||
|
|
||||||
|
self._waiting = False
|
||||||
|
try:
|
||||||
|
return self._resp.decode(errors="ignore")
|
||||||
|
except:
|
||||||
|
return str(self._resp)
|
||||||
|
|
||||||
def _find_urc_tag(self, tag: bytes):
|
def _find_urc_tag(self, tag: bytes):
|
||||||
"""
|
"""
|
||||||
只在"真正的 URC 边界"查找 tag,避免误命中 HTTP payload 内容。
|
只在"真正的 URC 边界"查找 tag,避免误命中 HTTP payload 内容。
|
||||||
|
|||||||
@@ -262,6 +262,16 @@ TRIANGLE_SAMPLE_PATCH_HALF_PX = 2
|
|||||||
# 开机阶段预加载 YOLO detector;detect 使用 dual_buff=False,避免返回上一帧结果。
|
# 开机阶段预加载 YOLO detector;detect 使用 dual_buff=False,避免返回上一帧结果。
|
||||||
TRIANGLE_YOLO_PRELOAD_ON_BOOT = False
|
TRIANGLE_YOLO_PRELOAD_ON_BOOT = False
|
||||||
|
|
||||||
|
# YOLO 靶规格识别:class 0=20cm,class 1=40cm。
|
||||||
|
TARGET_CLASS_YOLO_ENABLE = True
|
||||||
|
TARGET_CLASS_YOLO_MODEL_PATH = APP_DIR + "/model_285484.mud"
|
||||||
|
TARGET_CLASS_YOLO_LABELS = (20, 40)
|
||||||
|
TARGET_CLASS_YOLO_CONF_TH = 0.50
|
||||||
|
TARGET_CLASS_YOLO_IOU_TH = 0.45
|
||||||
|
TARGET_CLASS_YOLO_RETRY_ON_EMPTY = False
|
||||||
|
TARGET_CLASS_YOLO_RETRY_CONF_TH = 0.25
|
||||||
|
TARGET_CLASS_YOLO_PRELOAD_ON_BOOT = True
|
||||||
|
|
||||||
# ── 第二段 YOLO:仅在 Stage1 裁切出的靶环图上推理(与合成 stage2 训练数据一致)→ 子框内传统算法取直角点 ──
|
# ── 第二段 YOLO:仅在 Stage1 裁切出的靶环图上推理(与合成 stage2 训练数据一致)→ 子框内传统算法取直角点 ──
|
||||||
# Stage1 靶环裁切内如何找黑三角标记(对比耗时时可切换):
|
# Stage1 靶环裁切内如何找黑三角标记(对比耗时时可切换):
|
||||||
# "yolo" — 调 Stage2 黑三角模型得子框,再子框内传统提取(需 TRIANGLE_BLACK_YOLO_ENABLE=True)。
|
# "yolo" — 调 Stage2 黑三角模型得子框,再子框内传统提取(需 TRIANGLE_BLACK_YOLO_ENABLE=True)。
|
||||||
@@ -346,10 +356,13 @@ AUTO_POWER_OFF_IN_SECONDS = 10 * 60 # 自动关机时间(秒),0表示不
|
|||||||
# 实机数据:正常放电约为正电流,插入充电线后约为负电流。
|
# 实机数据:正常放电约为正电流,插入充电线后约为负电流。
|
||||||
CHARGING_SHUTDOWN_ENABLED = True # True=充电时退出应用,False=关闭充电关机功能
|
CHARGING_SHUTDOWN_ENABLED = True # True=充电时退出应用,False=关闭充电关机功能
|
||||||
CHARGING_DIAGNOSTIC_LOG_ENABLED = False
|
CHARGING_DIAGNOSTIC_LOG_ENABLED = False
|
||||||
CHARGING_CHECK_INTERVAL_MS = 5000
|
CHARGING_CHECK_INTERVAL_MS = 3000
|
||||||
CHARGING_CURRENT_THRESHOLD_MA = 100.0
|
CHARGING_CURRENT_THRESHOLD_MA = 100.0
|
||||||
CHARGING_CONFIRM_COUNT = 2
|
CHARGING_CONFIRM_COUNT = 2
|
||||||
CHARGING_NOTIFY_TIMEOUT_MS = 30000
|
CHARGING_NOTIFY_TIMEOUT_MS = 30000
|
||||||
|
CHARGING_4G_UART_LOCK_TIMEOUT_SEC = 2.5
|
||||||
|
CHARGING_4G_PROMPT_TIMEOUT_MS = 1500
|
||||||
|
CHARGING_4G_CONFIRM_TIMEOUT_MS = 1000
|
||||||
CHARGING_EXIT_SCRIPT = APP_DIR + "/charging_exit.sh"
|
CHARGING_EXIT_SCRIPT = APP_DIR + "/charging_exit.sh"
|
||||||
|
|
||||||
BATTERY_SOC_LPF_ALPHA = 0.5
|
BATTERY_SOC_LPF_ALPHA = 0.5
|
||||||
|
|||||||
@@ -0,0 +1,20 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<meta name="description" content="Weusing · 思考、记录与实践">
|
||||||
|
<title>Weusing · 思考与记录</title>
|
||||||
|
<link rel="stylesheet" href="/style.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<header class="top"><a class="brand" href="/">WEUSING<span>.</span></a><nav><a href="#posts">文章</a><a href="#about">关于</a></nav></header>
|
||||||
|
<main>
|
||||||
|
<section class="intro"><p class="kicker">A SMALL NOTEBOOK ON THE WEB</p><h1>把想法写下来,<br><em>让时间看得见。</em></h1><p class="lede">这里记录技术、产品和生活里的小发现。保持好奇,持续构建。</p></section>
|
||||||
|
<section id="posts" class="posts"><div class="section-head"><h2>最新文章</h2><span>2026 / 08</span></div>
|
||||||
|
<article><div class="date">08.13<br><small>2026</small></div><div><p class="tag">TECHNOLOGY</p><h3>让静态网站快起来:从请求到首屏的几件小事</h3><p class="excerpt">更少的依赖、更短的路径,以及一些值得长期坚持的工程习惯。</p></div><a class="arrow" href="#">↗</a></article>
|
||||||
|
<article><div class="date">08.06<br><small>2026</small></div><div><p class="tag">NOTES</p><h3>在复杂系统里,保留一条清晰的路</h3><p class="excerpt">记录一次调试过程,也记录那些最终留下来的判断。</p></div><a class="arrow" href="#">↗</a></article>
|
||||||
|
<article><div class="date">07.28<br><small>2026</small></div><div><p class="tag">LIFE</p><h3>慢一点,观察风从哪里来</h3><p class="excerpt">日常、远方和一些不急着得到答案的问题。</p></div><a class="arrow" href="#">↗</a></article>
|
||||||
|
</section>
|
||||||
|
<section id="about" class="about"><p class="kicker">ABOUT THIS SPACE</p><p>Weusing 是一个个人写作空间。愿每一次发布,都比上一次更接近真实。</p></section>
|
||||||
|
</main><footer><span>© 2026 WEUSING</span><span>BUILT WITH HTML & CSS</span></footer>
|
||||||
|
</body></html>
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
:root{--ink:#202326;--muted:#747a80;--line:#d9d7d1;--accent:#d95d39;--paper:#f5f3ee}*{box-sizing:border-box}html{scroll-behavior:smooth}body{margin:0;background:var(--paper);color:var(--ink);font-family:Arial,"Noto Sans SC",sans-serif}.top,main,footer{max-width:1080px;margin:auto}.top{height:88px;display:flex;align-items:center;justify-content:space-between;border-bottom:1px solid var(--line)}.brand{font-size:18px;letter-spacing:2px;font-weight:700;color:var(--ink);text-decoration:none}.brand span{color:var(--accent)}nav{display:flex;gap:30px}nav a{font-size:13px;color:var(--muted);text-decoration:none}nav a:hover{color:var(--accent)}.intro{padding:105px 0 115px;border-bottom:1px solid var(--line)}.kicker,.tag{font-size:11px;letter-spacing:2px;color:var(--accent);font-weight:700}.intro h1{font-size:clamp(42px,7vw,78px);line-height:1.1;letter-spacing:-2px;margin:25px 0}.intro em{font-family:Georgia,serif;font-weight:400;color:#555;font-style:italic}.lede{max-width:390px;color:var(--muted);font-size:16px;line-height:1.8}.posts{padding:72px 0}.section-head{display:flex;align-items:baseline;justify-content:space-between;border-bottom:2px solid var(--ink);padding-bottom:16px}.section-head h2{font-size:25px;margin:0}.section-head span{font-size:12px;color:var(--muted)}article{display:grid;grid-template-columns:90px 1fr 32px;gap:28px;padding:32px 0;border-bottom:1px solid var(--line);align-items:start}.date{font-size:14px;color:var(--accent);line-height:1.45;font-weight:700}.date small{font-size:11px;color:var(--muted);font-weight:400}article h3{font-size:23px;line-height:1.35;margin:9px 0 8px;font-weight:500}article .tag{margin:0;color:var(--muted);font-size:10px}.excerpt{color:var(--muted);font-size:14px;line-height:1.7;margin:0}.arrow{color:var(--accent);font-size:22px;text-decoration:none}.about{padding:30px 0 110px;display:grid;grid-template-columns:1fr 2fr;gap:30px;border-top:1px solid var(--line)}.about p:last-child{font:italic 27px/1.5 Georgia,serif;max-width:620px;margin:0}footer{border-top:1px solid var(--line);padding:25px 0 35px;display:flex;justify-content:space-between;color:var(--muted);font-size:10px;letter-spacing:1px}@media(max-width:700px){.top,main,footer{margin:0 22px}.top{height:70px}.intro{padding:70px 0 75px}.intro h1{letter-spacing:-1px}article{grid-template-columns:58px 1fr 20px;gap:14px}article h3{font-size:18px}.about{grid-template-columns:1fr;padding-bottom:75px}.about p:last-child{font-size:23px}footer{margin:0;padding:22px}.section-head{margin-top:0}}
|
||||||
@@ -22,7 +22,7 @@ from version import VERSION
|
|||||||
# from logger import init_logging, get_logger, stop_logging
|
# from logger import init_logging, get_logger, stop_logging
|
||||||
from logger_manager import logger_manager
|
from logger_manager import logger_manager
|
||||||
from time_sync import sync_system_time_from_4g
|
from time_sync import sync_system_time_from_4g
|
||||||
from power import charging_shutdown_monitor, init_ina226
|
from power import init_ina226
|
||||||
from laser_manager import laser_manager
|
from laser_manager import laser_manager
|
||||||
from vision import start_save_shot_worker
|
from vision import start_save_shot_worker
|
||||||
from network import network_manager
|
from network import network_manager
|
||||||
@@ -122,16 +122,9 @@ def cmd_str():
|
|||||||
|
|
||||||
# 1. 初始化日志系统
|
# 1. 初始化日志系统
|
||||||
import logging
|
import logging
|
||||||
logger_manager.init_logging(log_level=logging.DEBUG)
|
logger_manager.init_logging(log_level=logging.WARNING)
|
||||||
logger = logger_manager.logger
|
logger = logger_manager.logger
|
||||||
|
|
||||||
# 充电关机独立读取 INA226,不依赖 TCP 连接或心跳流程。
|
|
||||||
try:
|
|
||||||
_thread.start_new_thread(charging_shutdown_monitor, ())
|
|
||||||
except Exception as e:
|
|
||||||
if logger:
|
|
||||||
logger.error(f"[CHARGE] 启动独立监测线程失败: {e}")
|
|
||||||
|
|
||||||
# 补充:因为初始化的时候,激光会亮,先关了它
|
# 补充:因为初始化的时候,激光会亮,先关了它
|
||||||
# laser_manager.turn_off_laser()
|
# laser_manager.turn_off_laser()
|
||||||
|
|
||||||
@@ -169,7 +162,11 @@ def cmd_str():
|
|||||||
and _loc_black == "yolo"
|
and _loc_black == "yolo"
|
||||||
and bool(getattr(config, "TRIANGLE_BLACK_YOLO_PRELOAD_ON_BOOT", True))
|
and bool(getattr(config, "TRIANGLE_BLACK_YOLO_PRELOAD_ON_BOOT", True))
|
||||||
)
|
)
|
||||||
_preload_yolo = _preload_yolo or _need_black_preload
|
_need_target_preload = (
|
||||||
|
bool(getattr(config, "TARGET_CLASS_YOLO_ENABLE", False))
|
||||||
|
and bool(getattr(config, "TARGET_CLASS_YOLO_PRELOAD_ON_BOOT", True))
|
||||||
|
)
|
||||||
|
_preload_yolo = _preload_yolo or _need_black_preload or _need_target_preload
|
||||||
if _preload_yolo:
|
if _preload_yolo:
|
||||||
preload_yolo_detector(logger)
|
preload_yolo_detector(logger)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@@ -336,6 +333,7 @@ def cmd_str():
|
|||||||
time.sleep_ms(250)
|
time.sleep_ms(250)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
# todo 去除或者不在这里检测
|
||||||
# 不在 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} 秒")
|
||||||
|
|||||||
Binary file not shown.
@@ -0,0 +1,13 @@
|
|||||||
|
|
||||||
|
[basic]
|
||||||
|
type = cvimodel
|
||||||
|
model = model_285484.cvimodel
|
||||||
|
|
||||||
|
[extra]
|
||||||
|
model_type = yolov5
|
||||||
|
input_type = rgb
|
||||||
|
mean = 0, 0, 0
|
||||||
|
scale = 0.00392156862745098, 0.00392156862745098, 0.00392156862745098
|
||||||
|
anchors = 10, 13, 16, 30, 33, 23, 30, 61, 62, 45, 59, 119, 116, 90, 156, 198, 373, 326
|
||||||
|
labels = 20, 40
|
||||||
|
|
||||||
+145
-5
@@ -18,7 +18,7 @@ import socket
|
|||||||
import config
|
import config
|
||||||
|
|
||||||
from hardware import hardware_manager
|
from hardware import hardware_manager
|
||||||
from power import get_bus_voltage, voltage_to_percent
|
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
|
import subprocess
|
||||||
@@ -67,6 +67,7 @@ class NetworkManager:
|
|||||||
self._queue_lock = threading.Lock()
|
self._queue_lock = threading.Lock()
|
||||||
self._send_event = threading.Event()
|
self._send_event = threading.Event()
|
||||||
self._uart4g_lock = threading.Lock()
|
self._uart4g_lock = threading.Lock()
|
||||||
|
self._terminal_send_event = threading.Event()
|
||||||
self._device_id = None
|
self._device_id = None
|
||||||
self._password = None
|
self._password = None
|
||||||
self._raw_line_data = []
|
self._raw_line_data = []
|
||||||
@@ -669,6 +670,8 @@ 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")
|
||||||
|
# 停止旧的WiFi质量监测(无论当前是WiFi还是4G连接)
|
||||||
|
self._stop_wifi_quality_monitor()
|
||||||
try:
|
try:
|
||||||
for _f in ("/etc/wpa_supplicant.conf", "/boot/wpa_supplicant.conf", "/boot/wifi.ssid", "/boot/wifi.pass"):
|
for _f in ("/etc/wpa_supplicant.conf", "/boot/wpa_supplicant.conf", "/boot/wifi.ssid", "/boot/wifi.pass"):
|
||||||
try:
|
try:
|
||||||
@@ -676,7 +679,7 @@ 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=10)
|
||||||
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()}")
|
||||||
@@ -715,6 +718,29 @@ class NetworkManager:
|
|||||||
self._enqueue((msg_type, data_dict, sent_event), high)
|
self._enqueue((msg_type, data_dict, sent_event), high)
|
||||||
return bool(sent_event.wait(max(0, int(timeout_ms)) / 1000.0))
|
return bool(sent_event.wait(max(0, int(timeout_ms)) / 1000.0))
|
||||||
|
|
||||||
|
def safe_replace_queue_and_wait(self, data_dict, msg_type=2, timeout_ms=30000):
|
||||||
|
"""Drop queued messages, enqueue one terminal message, and wait for its TCP write."""
|
||||||
|
sent_event = threading.Event()
|
||||||
|
with self._queue_lock:
|
||||||
|
self._high_send_queue.clear()
|
||||||
|
self._normal_send_queue.clear()
|
||||||
|
self._high_send_queue.append((msg_type, data_dict, sent_event))
|
||||||
|
self._send_event.set()
|
||||||
|
return bool(sent_event.wait(max(0, int(timeout_ms)) / 1000.0))
|
||||||
|
|
||||||
|
def safe_terminal_send_and_wait(self, data_dict, msg_type=2, timeout_ms=30000):
|
||||||
|
"""Cancel ordinary 4G waits and replace queued work with one terminal message."""
|
||||||
|
sent_event = threading.Event()
|
||||||
|
result = {"sent": False}
|
||||||
|
self._terminal_send_event.set()
|
||||||
|
with self._queue_lock:
|
||||||
|
self._high_send_queue.clear()
|
||||||
|
self._normal_send_queue.clear()
|
||||||
|
self._high_send_queue.append((msg_type, data_dict, sent_event, "terminal", result))
|
||||||
|
self._send_event.set()
|
||||||
|
completed = sent_event.wait(max(0, int(timeout_ms)) / 1000.0)
|
||||||
|
return bool(completed and result["sent"])
|
||||||
|
|
||||||
def connect_server(self):
|
def connect_server(self):
|
||||||
"""
|
"""
|
||||||
连接到服务器(自动选择WiFi或4G)
|
连接到服务器(自动选择WiFi或4G)
|
||||||
@@ -1114,8 +1140,12 @@ class NetworkManager:
|
|||||||
return False
|
return False
|
||||||
try:
|
try:
|
||||||
for _ in range(max_retries):
|
for _ in range(max_retries):
|
||||||
|
if self._terminal_send_event.is_set():
|
||||||
|
return False
|
||||||
cmd = f'AT+MIPSEND={link_id},{len(data)}'
|
cmd = f'AT+MIPSEND={link_id},{len(data)}'
|
||||||
if ">" not in hardware_manager.at_client.send(cmd, ">", 2000):
|
if ">" not in hardware_manager.at_client.send(cmd, ">", 2000):
|
||||||
|
if self._terminal_send_event.is_set():
|
||||||
|
return False
|
||||||
time.sleep_ms(50)
|
time.sleep_ms(50)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
@@ -1130,14 +1160,73 @@ class NetworkManager:
|
|||||||
hardware_manager.uart4g.write(b"\x1A")
|
hardware_manager.uart4g.write(b"\x1A")
|
||||||
with hardware_manager.at_client._q_lock:
|
with hardware_manager.at_client._q_lock:
|
||||||
hardware_manager.at_client._rx = b""
|
hardware_manager.at_client._rx = b""
|
||||||
r = hardware_manager.at_client.send("", "OK", 8000)
|
r = hardware_manager.at_client.send(
|
||||||
|
"", "OK", 8000, abort_event=self._terminal_send_event
|
||||||
|
)
|
||||||
if ("SEND OK" in r) or ("OK" in r) or ("+MIPSEND" in r):
|
if ("SEND OK" in r) or ("OK" in r) or ("+MIPSEND" in r):
|
||||||
return True
|
return True
|
||||||
|
if self._terminal_send_event.is_set():
|
||||||
|
return False
|
||||||
time.sleep_ms(50)
|
time.sleep_ms(50)
|
||||||
return False
|
return False
|
||||||
finally:
|
finally:
|
||||||
self._uart4g_lock.release()
|
self._uart4g_lock.release()
|
||||||
|
|
||||||
|
def _tcp_send_terminal_raw(self, data: bytes) -> bool:
|
||||||
|
if not self._tcp_connected:
|
||||||
|
return False
|
||||||
|
if self._network_type == "wifi":
|
||||||
|
return self._tcp_send_raw_via_wifi(data, max_retries=1)
|
||||||
|
if self._network_type != "4g":
|
||||||
|
return False
|
||||||
|
|
||||||
|
link_id = getattr(config, "TCP_LINK_ID", 0)
|
||||||
|
lock_timeout_sec = float(
|
||||||
|
getattr(config, "CHARGING_4G_UART_LOCK_TIMEOUT_SEC", 2.5)
|
||||||
|
)
|
||||||
|
prompt_timeout_ms = int(
|
||||||
|
getattr(config, "CHARGING_4G_PROMPT_TIMEOUT_MS", 1500)
|
||||||
|
)
|
||||||
|
confirm_timeout_ms = int(
|
||||||
|
getattr(config, "CHARGING_4G_CONFIRM_TIMEOUT_MS", 1000)
|
||||||
|
)
|
||||||
|
lock_start_ms = time.ticks_ms()
|
||||||
|
if not self._uart4g_lock.acquire(timeout=max(0.0, lock_timeout_sec)):
|
||||||
|
self.logger.warning(
|
||||||
|
f"[CHARGE-4G] uart_lock timeout timeout_sec={lock_timeout_sec}"
|
||||||
|
)
|
||||||
|
return False
|
||||||
|
try:
|
||||||
|
lock_elapsed_ms = abs(time.ticks_diff(time.ticks_ms(), lock_start_ms))
|
||||||
|
cmd = f'AT+MIPSEND={link_id},{len(data)}'
|
||||||
|
prompt_start_ms = time.ticks_ms()
|
||||||
|
if ">" not in hardware_manager.at_client.send(
|
||||||
|
cmd, ">", max(0, prompt_timeout_ms)):
|
||||||
|
prompt_elapsed_ms = abs(time.ticks_diff(time.ticks_ms(), prompt_start_ms))
|
||||||
|
self.logger.warning(
|
||||||
|
f"[CHARGE-4G] prompt failed lock_ms={lock_elapsed_ms} "
|
||||||
|
f"prompt_ms={prompt_elapsed_ms}"
|
||||||
|
)
|
||||||
|
return False
|
||||||
|
prompt_elapsed_ms = abs(time.ticks_diff(time.ticks_ms(), prompt_start_ms))
|
||||||
|
confirm_start_ms = time.ticks_ms()
|
||||||
|
r = hardware_manager.at_client.send_raw_and_wait(
|
||||||
|
data,
|
||||||
|
expect="OK",
|
||||||
|
timeout_ms=max(0, confirm_timeout_ms),
|
||||||
|
suffix=b"\x1A",
|
||||||
|
)
|
||||||
|
confirm_elapsed_ms = abs(time.ticks_diff(time.ticks_ms(), confirm_start_ms))
|
||||||
|
sent = ("SEND OK" in r) or ("OK" in r) or ("+MIPSEND" in r)
|
||||||
|
self.logger.warning(
|
||||||
|
f"[CHARGE-4G] send_done lock_ms={lock_elapsed_ms} "
|
||||||
|
f"prompt_ms={prompt_elapsed_ms} confirm_ms={confirm_elapsed_ms} "
|
||||||
|
f"sent={sent}"
|
||||||
|
)
|
||||||
|
return sent
|
||||||
|
finally:
|
||||||
|
self._uart4g_lock.release()
|
||||||
|
|
||||||
def _configure_ssl_before_connect(self, link_id: int) -> bool:
|
def _configure_ssl_before_connect(self, link_id: int) -> bool:
|
||||||
"""按手册:MSSLCFG(auth) -> (可选) MSSLCERTWR -> MSSLCFG(cert) -> MIPCFG(ssl)"""
|
"""按手册:MSSLCFG(auth) -> (可选) MSSLCERTWR -> MSSLCFG(cert) -> MIPCFG(ssl)"""
|
||||||
ssl_id = getattr(config, "SSL_ID", 1)
|
ssl_id = getattr(config, "SSL_ID", 1)
|
||||||
@@ -1874,12 +1963,25 @@ class NetworkManager:
|
|||||||
pending_cleared = False
|
pending_cleared = False
|
||||||
last_heartbeat_ack_time = time.ticks_ms()
|
last_heartbeat_ack_time = time.ticks_ms()
|
||||||
last_heartbeat_send_time = time.ticks_ms()
|
last_heartbeat_send_time = time.ticks_ms()
|
||||||
|
last_wifi_sta_check_time = time.ticks_ms()
|
||||||
|
|
||||||
while True:
|
while True:
|
||||||
# 如果底层连接已断开,尽快跳出内层循环触发重连/重选网络
|
# 如果底层连接已断开,尽快跳出内层循环触发重连/重选网络
|
||||||
if not self._tcp_connected:
|
if not self._tcp_connected:
|
||||||
break
|
break
|
||||||
|
|
||||||
|
if self._network_type == "wifi":
|
||||||
|
now_ms = time.ticks_ms()
|
||||||
|
if abs(time.ticks_diff(now_ms, last_wifi_sta_check_time)) >= 1000:
|
||||||
|
last_wifi_sta_check_time = now_ms
|
||||||
|
if not wifi_manager.is_sta_associated():
|
||||||
|
self.logger.warning(
|
||||||
|
"[WIFI-TCP] STA disconnected; leave WiFi session and reselect network"
|
||||||
|
)
|
||||||
|
wifi_manager.disconnect_wifi()
|
||||||
|
self._tcp_connected = False
|
||||||
|
break
|
||||||
|
|
||||||
# OTA 期间暂停 TCP 活动
|
# OTA 期间暂停 TCP 活动
|
||||||
try:
|
try:
|
||||||
from ota_manager import ota_manager
|
from ota_manager import ota_manager
|
||||||
@@ -2156,7 +2258,17 @@ class NetworkManager:
|
|||||||
"netType": self.network_type,
|
"netType": self.network_type,
|
||||||
}
|
}
|
||||||
self.safe_enqueue(battery_data, 2)
|
self.safe_enqueue(battery_data, 2)
|
||||||
self.logger.info(f"电量上报: {battery_percent}%")
|
self.logger.info(f"电量上报: {battery_percent}% 充电: {is_charging()}")
|
||||||
|
if is_charging():
|
||||||
|
self.safe_enqueue(
|
||||||
|
{
|
||||||
|
"cmd": 700,
|
||||||
|
},
|
||||||
|
2,
|
||||||
|
)
|
||||||
|
elif inner_cmd == 700:
|
||||||
|
self.logger.warning("服务器下发关机!!!")
|
||||||
|
exit(-1)
|
||||||
elif inner_cmd == 5: # OTA 升级
|
elif inner_cmd == 5: # OTA 升级
|
||||||
inner_data = data_obj.get("data", {}) if isinstance(data_obj, dict) else {}
|
inner_data = data_obj.get("data", {}) if isinstance(data_obj, dict) else {}
|
||||||
ssid = inner_data.get("ssid")
|
ssid = inner_data.get("ssid")
|
||||||
@@ -2312,8 +2424,34 @@ class NetworkManager:
|
|||||||
if item:
|
if item:
|
||||||
msg_type, data_dict = item[:2]
|
msg_type, data_dict = item[:2]
|
||||||
sent_event = item[2] if len(item) > 2 else None
|
sent_event = item[2] if len(item) > 2 else None
|
||||||
|
item_is_terminal = len(item) > 3 and item[3] == "terminal"
|
||||||
|
terminal_result = item[4] if item_is_terminal and len(item) > 4 else None
|
||||||
|
if (
|
||||||
|
isinstance(data_dict, dict)
|
||||||
|
and data_dict.get("cmd") == 1
|
||||||
|
and isinstance(data_dict.get("data"), dict)
|
||||||
|
):
|
||||||
|
shot_data = data_dict["data"]
|
||||||
|
self.logger.info(
|
||||||
|
f"[REPORT-TX] shot_id={shot_data.get('shot_id')}, "
|
||||||
|
f"target_class={shot_data.get('target_class')}, "
|
||||||
|
f"confidence={shot_data.get('target_class_confidence')}"
|
||||||
|
)
|
||||||
pkt = self._netcore.make_packet(msg_type, data_dict)
|
pkt = self._netcore.make_packet(msg_type, data_dict)
|
||||||
if not self.tcp_send_raw(pkt):
|
send_ok = (
|
||||||
|
self._tcp_send_terminal_raw(pkt)
|
||||||
|
if item_is_terminal
|
||||||
|
else self.tcp_send_raw(pkt)
|
||||||
|
)
|
||||||
|
if not send_ok:
|
||||||
|
if item_is_terminal:
|
||||||
|
if terminal_result is not None:
|
||||||
|
terminal_result["sent"] = False
|
||||||
|
if sent_event is not None:
|
||||||
|
sent_event.set()
|
||||||
|
break
|
||||||
|
if self._terminal_send_event.is_set():
|
||||||
|
continue
|
||||||
# 发送失败:将消息放回队首(队列满则丢弃)
|
# 发送失败:将消息放回队首(队列满则丢弃)
|
||||||
with self.get_queue_lock():
|
with self.get_queue_lock():
|
||||||
if item_is_high:
|
if item_is_high:
|
||||||
@@ -2329,6 +2467,8 @@ class NetworkManager:
|
|||||||
pass
|
pass
|
||||||
break
|
break
|
||||||
if sent_event is not None:
|
if sent_event is not None:
|
||||||
|
if terminal_result is not None:
|
||||||
|
terminal_result["sent"] = True
|
||||||
sent_event.set()
|
sent_event.set()
|
||||||
|
|
||||||
# 发送激光校准结果
|
# 发送激光校准结果
|
||||||
|
|||||||
@@ -142,115 +142,6 @@ def is_charging(threshold_ma=10.0):
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
def charging_shutdown_monitor():
|
|
||||||
"""独立监测 INA226;连续确认充电后通知服务器并退出应用。"""
|
|
||||||
logger = logger_manager.logger
|
|
||||||
shutdown_enabled = bool(getattr(config, "CHARGING_SHUTDOWN_ENABLED", False))
|
|
||||||
diagnostic_enabled = bool(getattr(config, "CHARGING_DIAGNOSTIC_LOG_ENABLED", False))
|
|
||||||
if not shutdown_enabled and not diagnostic_enabled:
|
|
||||||
if logger:
|
|
||||||
logger.info("[CHARGE] 充电退出监测已禁用")
|
|
||||||
return
|
|
||||||
|
|
||||||
interval_ms = max(100, int(getattr(config, "CHARGING_CHECK_INTERVAL_MS", 5000)))
|
|
||||||
threshold_ma = float(getattr(config, "CHARGING_CURRENT_THRESHOLD_MA", 10.0))
|
|
||||||
confirm_required = max(1, int(getattr(config, "CHARGING_CONFIRM_COUNT", 2)))
|
|
||||||
confirm_count = 0
|
|
||||||
|
|
||||||
if logger:
|
|
||||||
logger.info(
|
|
||||||
f"[CHARGE] 独立监测线程启动: interval={interval_ms}ms, "
|
|
||||||
f"threshold={threshold_ma:.1f}mA, confirm={confirm_required}, "
|
|
||||||
f"shutdown={'on' if shutdown_enabled else 'off'}"
|
|
||||||
)
|
|
||||||
|
|
||||||
while True:
|
|
||||||
current_ma = get_current()
|
|
||||||
if diagnostic_enabled and logger:
|
|
||||||
voltage = get_bus_voltage()
|
|
||||||
logger.info(
|
|
||||||
f"[CHARGE-DIAG] INA226 voltage={voltage:.3f}V, "
|
|
||||||
f"current={current_ma:.1f}mA"
|
|
||||||
)
|
|
||||||
|
|
||||||
if not shutdown_enabled:
|
|
||||||
maix_time.sleep_ms(interval_ms)
|
|
||||||
continue
|
|
||||||
|
|
||||||
if current_ma < -abs(threshold_ma):
|
|
||||||
confirm_count += 1
|
|
||||||
if logger:
|
|
||||||
logger.info(
|
|
||||||
f"[CHARGE] INA226 充电电流 {current_ma:.1f}mA "
|
|
||||||
f"({confirm_count}/{confirm_required})"
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
confirm_count = 0
|
|
||||||
|
|
||||||
if confirm_count >= confirm_required:
|
|
||||||
script_path = getattr(
|
|
||||||
config,
|
|
||||||
"CHARGING_EXIT_SCRIPT",
|
|
||||||
config.APP_DIR + "/charging_exit.sh",
|
|
||||||
)
|
|
||||||
if not os.path.isfile(script_path):
|
|
||||||
if logger:
|
|
||||||
logger.error(f"[CHARGE] 退出脚本不存在: {script_path}")
|
|
||||||
confirm_count = 0
|
|
||||||
else:
|
|
||||||
if logger:
|
|
||||||
logger.warning(
|
|
||||||
f"[CHARGE] 已连续确认充电,通知服务器后退出应用: current={current_ma:.1f}mA"
|
|
||||||
)
|
|
||||||
try:
|
|
||||||
from network import network_manager
|
|
||||||
|
|
||||||
notify_timeout_ms = max(
|
|
||||||
0,
|
|
||||||
int(getattr(config, "CHARGING_NOTIFY_TIMEOUT_MS", 30000)),
|
|
||||||
)
|
|
||||||
notification_sent = network_manager.safe_enqueue_and_wait(
|
|
||||||
{"poweroff": "充电中"},
|
|
||||||
2,
|
|
||||||
high=True,
|
|
||||||
timeout_ms=notify_timeout_ms,
|
|
||||||
)
|
|
||||||
if notification_sent:
|
|
||||||
if logger:
|
|
||||||
logger.info("[CHARGE] 充电状态已发送到服务器")
|
|
||||||
elif logger:
|
|
||||||
logger.warning(
|
|
||||||
f"[CHARGE] 等待服务器发送超时({notify_timeout_ms}ms),继续执行退出"
|
|
||||||
)
|
|
||||||
except Exception as e:
|
|
||||||
if logger:
|
|
||||||
logger.error(f"[CHARGE] 充电状态上报失败,继续执行退出: {e}")
|
|
||||||
try:
|
|
||||||
from laser_manager import laser_manager
|
|
||||||
|
|
||||||
laser_manager.turn_off_laser()
|
|
||||||
if logger:
|
|
||||||
logger.info("[CHARGE] 激光关闭命令已发送")
|
|
||||||
except Exception as e:
|
|
||||||
if logger:
|
|
||||||
logger.error(f"[CHARGE] Python 关闭激光失败,交由退出脚本兜底: {e}")
|
|
||||||
try:
|
|
||||||
subprocess.Popen([
|
|
||||||
"/bin/sh",
|
|
||||||
script_path,
|
|
||||||
str(os.getpid()),
|
|
||||||
str(getattr(config, "DISTANCE_SERIAL_DEVICE", "/dev/ttyS1")),
|
|
||||||
str(getattr(config, "DISTANCE_SERIAL_BAUDRATE", 9600)),
|
|
||||||
])
|
|
||||||
return
|
|
||||||
except Exception as e:
|
|
||||||
if logger:
|
|
||||||
logger.error(f"[CHARGE] 调用退出脚本失败: {e}")
|
|
||||||
confirm_count = 0
|
|
||||||
|
|
||||||
maix_time.sleep_ms(interval_ms)
|
|
||||||
|
|
||||||
|
|
||||||
def voltage_to_percent(voltage):
|
def voltage_to_percent(voltage):
|
||||||
"""
|
"""
|
||||||
根据电压估算电池百分比(高密度查表插值 + 滤波)。
|
根据电压估算电池百分比(高密度查表插值 + 滤波)。
|
||||||
|
|||||||
+32
-1
@@ -320,8 +320,20 @@ def process_shot(adc_val):
|
|||||||
logger = logger_manager.logger
|
logger = logger_manager.logger
|
||||||
|
|
||||||
try:
|
try:
|
||||||
network_manager.safe_enqueue({"shoot_event": "start"}, msg_type=2, high=True)
|
|
||||||
frame = camera_manager.read_frame()
|
frame = camera_manager.read_frame()
|
||||||
|
network_manager.safe_enqueue({"shoot_event": "start"}, msg_type=2, high=True)
|
||||||
|
|
||||||
|
# 每箭只识别当前帧,不复用上一箭的靶规格结果。
|
||||||
|
target_class_result = None
|
||||||
|
try:
|
||||||
|
from target_roi_yolo import try_get_target_class_from_yolo
|
||||||
|
|
||||||
|
target_class_result = try_get_target_class_from_yolo(frame, logger=logger)
|
||||||
|
if logger:
|
||||||
|
logger.info(f"[YOLO-TARGET] 当前箭业务结果: {target_class_result}")
|
||||||
|
except Exception as exc:
|
||||||
|
if logger:
|
||||||
|
logger.warning(f"[YOLO-TARGET] 当前箭分类失败,按未知处理: {exc}")
|
||||||
|
|
||||||
# 调用算法分析
|
# 调用算法分析
|
||||||
analysis_result = analyze_shot(frame)
|
analysis_result = analyze_shot(frame)
|
||||||
@@ -382,11 +394,25 @@ def process_shot(adc_val):
|
|||||||
srv_y = round(float(dy), 4) if dy is not None else 200.0
|
srv_y = round(float(dy), 4) if dy is not None else 200.0
|
||||||
|
|
||||||
# 构造上报数据
|
# 构造上报数据
|
||||||
|
target_label = (
|
||||||
|
target_class_result.get("label")
|
||||||
|
if isinstance(target_class_result, dict)
|
||||||
|
else None
|
||||||
|
)
|
||||||
|
target_confidence = (
|
||||||
|
target_class_result.get("confidence")
|
||||||
|
if isinstance(target_class_result, dict)
|
||||||
|
else None
|
||||||
|
)
|
||||||
inner_data = {
|
inner_data = {
|
||||||
"shot_id": shot_id,
|
"shot_id": shot_id,
|
||||||
"x": srv_x,
|
"x": srv_x,
|
||||||
"y": srv_y,
|
"y": srv_y,
|
||||||
"r": 20.0, # 保留字段(服务端当前忽略,物理外环半径 cm)
|
"r": 20.0, # 保留字段(服务端当前忽略,物理外环半径 cm)
|
||||||
|
"target_class": target_label,
|
||||||
|
"target_class_confidence": (
|
||||||
|
float(target_confidence) if target_confidence is not None else None
|
||||||
|
),
|
||||||
"d": round((distance_m or 0.0) * 100),
|
"d": round((distance_m or 0.0) * 100),
|
||||||
"d_laser": round((laser_distance_m or 0.0) * 100),
|
"d_laser": round((laser_distance_m or 0.0) * 100),
|
||||||
"d_laser_quality": laser_signal_quality,
|
"d_laser_quality": laser_signal_quality,
|
||||||
@@ -414,6 +440,11 @@ def process_shot(adc_val):
|
|||||||
inner_data["ellipse_center_y"] = None
|
inner_data["ellipse_center_y"] = None
|
||||||
|
|
||||||
report_data = {"cmd": 1, "data": inner_data}
|
report_data = {"cmd": 1, "data": inner_data}
|
||||||
|
if logger:
|
||||||
|
logger.info(
|
||||||
|
f"[REPORT-TARGET] enqueue shot_id={shot_id}, "
|
||||||
|
f"target_class={target_label}, confidence={target_confidence}"
|
||||||
|
)
|
||||||
network_manager.safe_enqueue(report_data, msg_type=2, high=True)
|
network_manager.safe_enqueue(report_data, msg_type=2, high=True)
|
||||||
|
|
||||||
# 数据上报后再画标注,不干扰检测阶段的原始画面
|
# 数据上报后再画标注,不干扰检测阶段的原始画面
|
||||||
|
|||||||
Binary file not shown.
+143
-1
@@ -89,6 +89,29 @@ def _stage2_roi_crop_save_worker(
|
|||||||
_detector_by_path = {}
|
_detector_by_path = {}
|
||||||
|
|
||||||
|
|
||||||
|
def _resolve_model_path(model_path: str):
|
||||||
|
"""Resolve a model in either the installed app or MaixVision run directory."""
|
||||||
|
model_path = (model_path or "").strip()
|
||||||
|
if model_path and os.path.isfile(model_path):
|
||||||
|
return model_path
|
||||||
|
if not model_path:
|
||||||
|
return ""
|
||||||
|
name = os.path.basename(model_path)
|
||||||
|
module_dir = os.path.dirname(os.path.abspath(__file__))
|
||||||
|
candidates = (
|
||||||
|
os.path.join(module_dir, name),
|
||||||
|
os.path.join(module_dir, "test", name),
|
||||||
|
os.path.join("/tmp/maixpy_run", name),
|
||||||
|
os.path.join("/tmp/maixpy_run", "test", name),
|
||||||
|
os.path.join(os.getcwd(), name),
|
||||||
|
os.path.join(os.getcwd(), "test", name),
|
||||||
|
)
|
||||||
|
for candidate in candidates:
|
||||||
|
if os.path.isfile(candidate):
|
||||||
|
return candidate
|
||||||
|
return model_path
|
||||||
|
|
||||||
|
|
||||||
def reset_yolo_detector_cache():
|
def reset_yolo_detector_cache():
|
||||||
"""切换模型路径时可调用(通常不必)。"""
|
"""切换模型路径时可调用(通常不必)。"""
|
||||||
global _detector_by_path
|
global _detector_by_path
|
||||||
@@ -175,6 +198,23 @@ def preload_yolo_detector(logger=None):
|
|||||||
% (_loc_black,)
|
% (_loc_black,)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if bool(getattr(cfg, "TARGET_CLASS_YOLO_ENABLE", False)) and bool(
|
||||||
|
getattr(cfg, "TARGET_CLASS_YOLO_PRELOAD_ON_BOOT", True)
|
||||||
|
):
|
||||||
|
class_model_path = _resolve_model_path(
|
||||||
|
getattr(cfg, "TARGET_CLASS_YOLO_MODEL_PATH", "") or ""
|
||||||
|
)
|
||||||
|
class_detector = _get_detector(class_model_path)
|
||||||
|
if class_detector is None:
|
||||||
|
if logger:
|
||||||
|
logger.warning(
|
||||||
|
f"[YOLO-TARGET] 预加载失败:无法加载模型 {class_model_path}"
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
ok = True
|
||||||
|
if logger:
|
||||||
|
logger.info(f"[YOLO-TARGET] 靶规格模型已预加载: {class_model_path}")
|
||||||
|
|
||||||
return ok
|
return ok
|
||||||
|
|
||||||
|
|
||||||
@@ -206,8 +246,10 @@ def _det_obj_class_id(o):
|
|||||||
if v is None:
|
if v is None:
|
||||||
continue
|
continue
|
||||||
try:
|
try:
|
||||||
|
if callable(v):
|
||||||
|
v = v()
|
||||||
return int(float(v))
|
return int(float(v))
|
||||||
except (TypeError, ValueError):
|
except (TypeError, ValueError, AttributeError):
|
||||||
continue
|
continue
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@@ -242,6 +284,106 @@ def _normalize_objs(objs):
|
|||||||
return out
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
def _det_obj_score(o):
|
||||||
|
"""兼容 Maix YOLO 不同版本的置信度字段。"""
|
||||||
|
for key in ("score", "confidence", "conf", "prob"):
|
||||||
|
if hasattr(o, key):
|
||||||
|
try:
|
||||||
|
value = getattr(o, key)
|
||||||
|
if callable(value):
|
||||||
|
value = value()
|
||||||
|
value = float(value)
|
||||||
|
if value == value:
|
||||||
|
return value
|
||||||
|
except (TypeError, ValueError, AttributeError):
|
||||||
|
pass
|
||||||
|
return 0.0
|
||||||
|
|
||||||
|
|
||||||
|
def try_get_target_class_from_yolo(maix_frame, logger=None):
|
||||||
|
"""识别当前帧的 20/40 靶规格,失败返回 None。"""
|
||||||
|
try:
|
||||||
|
import config as cfg
|
||||||
|
except Exception:
|
||||||
|
return None
|
||||||
|
|
||||||
|
if not bool(getattr(cfg, "TARGET_CLASS_YOLO_ENABLE", False)):
|
||||||
|
return None
|
||||||
|
model_path = _resolve_model_path(
|
||||||
|
getattr(cfg, "TARGET_CLASS_YOLO_MODEL_PATH", "") or ""
|
||||||
|
)
|
||||||
|
if not os.path.isfile(model_path):
|
||||||
|
if logger:
|
||||||
|
logger.warning(f"[YOLO-TARGET] 模型文件不存在: {model_path}")
|
||||||
|
return None
|
||||||
|
detector = _get_detector(model_path)
|
||||||
|
if detector is None:
|
||||||
|
if logger:
|
||||||
|
logger.warning("[YOLO-TARGET] 无法加载 nn.YOLOv5")
|
||||||
|
return None
|
||||||
|
|
||||||
|
conf_th = float(getattr(cfg, "TARGET_CLASS_YOLO_CONF_TH", 0.5))
|
||||||
|
iou_th = float(getattr(cfg, "TARGET_CLASS_YOLO_IOU_TH", 0.45))
|
||||||
|
labels = getattr(cfg, "TARGET_CLASS_YOLO_LABELS", (20, 40))
|
||||||
|
if isinstance(labels, str):
|
||||||
|
labels = tuple(x.strip() for x in labels.split(",") if x.strip())
|
||||||
|
labels = tuple(labels)
|
||||||
|
|
||||||
|
def _detect(threshold):
|
||||||
|
try:
|
||||||
|
raw = detector.detect(maix_frame, conf_th=threshold, iou_th=iou_th)
|
||||||
|
except Exception as exc:
|
||||||
|
if logger:
|
||||||
|
logger.warning(f"[YOLO-TARGET] detect 异常: {exc}")
|
||||||
|
return []
|
||||||
|
return _normalize_objs(raw if raw is not None else [])
|
||||||
|
|
||||||
|
def _candidates(objs):
|
||||||
|
found = []
|
||||||
|
for obj in objs:
|
||||||
|
class_id = _det_obj_class_id(obj)
|
||||||
|
if class_id is None or class_id < 0 or class_id >= len(labels):
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
label = int(float(labels[class_id]))
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
continue
|
||||||
|
if label in (20, 40):
|
||||||
|
found.append((label, class_id, _det_obj_score(obj)))
|
||||||
|
return found
|
||||||
|
|
||||||
|
objects = _detect(conf_th)
|
||||||
|
candidates = _candidates(objects)
|
||||||
|
if logger and objects:
|
||||||
|
logger.info(
|
||||||
|
"[YOLO-TARGET] 原始框=%d, 解析类别=%s"
|
||||||
|
% (
|
||||||
|
len(objects),
|
||||||
|
[(_det_obj_class_id(o), _det_obj_score(o)) for o in objects[:8]],
|
||||||
|
)
|
||||||
|
)
|
||||||
|
if not candidates and bool(
|
||||||
|
getattr(cfg, "TARGET_CLASS_YOLO_RETRY_ON_EMPTY", False)
|
||||||
|
):
|
||||||
|
retry_th = float(getattr(cfg, "TARGET_CLASS_YOLO_RETRY_CONF_TH", conf_th))
|
||||||
|
if 0 < retry_th < conf_th:
|
||||||
|
candidates = _candidates(_detect(retry_th))
|
||||||
|
|
||||||
|
if not candidates:
|
||||||
|
if logger:
|
||||||
|
logger.warning("[YOLO-TARGET] 当前帧未识别到 20/40,按未知处理")
|
||||||
|
return None
|
||||||
|
|
||||||
|
label, class_id, confidence = max(candidates, key=lambda item: item[2])
|
||||||
|
result = {"label": label, "class_id": class_id, "confidence": confidence}
|
||||||
|
if logger:
|
||||||
|
logger.info(
|
||||||
|
f"[YOLO-TARGET] 当前帧分类={label}, class_id={class_id}, "
|
||||||
|
f"conf={confidence:.3f}"
|
||||||
|
)
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
def _det_to_src_xyxy(o, coord_mode: str, src_w: int, src_h: int, net_w: int, net_h: int):
|
def _det_to_src_xyxy(o, coord_mode: str, src_w: int, src_h: int, net_w: int, net_h: int):
|
||||||
"""把单个检测框转为全图坐标系下的 xyxy(半开区间语义与后续 clip 一致)。"""
|
"""把单个检测框转为全图坐标系下的 xyxy(半开区间语义与后续 clip 一致)。"""
|
||||||
x, y, w, h = float(o.x), float(o.y), float(o.w), float(o.h)
|
x, y, w, h = float(o.x), float(o.y), float(o.w), float(o.h)
|
||||||
|
|||||||
@@ -36,3 +36,4 @@
|
|||||||
# 2.15.24 空改测试
|
# 2.15.24 空改测试
|
||||||
# 2.15.25 修复整合后关机失败和ota格式更新问题
|
# 2.15.25 修复整合后关机失败和ota格式更新问题
|
||||||
# 2.15.26
|
# 2.15.26
|
||||||
|
# 2.15.33 26-8-12 14:03 修改充4g电关机时间 修复切换网络卡住bug
|
||||||
|
|||||||
+1
-1
@@ -4,6 +4,6 @@
|
|||||||
应用版本号
|
应用版本号
|
||||||
每次 OTA 更新时,只需要更新这个文件中的版本号
|
每次 OTA 更新时,只需要更新这个文件中的版本号
|
||||||
"""
|
"""
|
||||||
VERSION = '2.15.25'
|
VERSION = '2.15.36'
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -631,7 +631,7 @@ def detect_circle_v3(frame, laser_point=None, img_cv=None):
|
|||||||
min_r = min(rc["radius"], yellow_radius)
|
min_r = min(rc["radius"], yellow_radius)
|
||||||
max_r = max(rc["radius"], yellow_radius)
|
max_r = max(rc["radius"], yellow_radius)
|
||||||
size_ratio = min_r / max_r if max_r > 0 else 0
|
size_ratio = min_r / max_r if max_r > 0 else 0
|
||||||
if dist_centers < max_dist and size_ratio > 0.5:
|
if dist_centers < max_dist and size_ratio >= 0.3:
|
||||||
if logger:
|
if logger:
|
||||||
logger.info(f"[target] -> 找到匹配的红圈: 黄心({yellow_center}), "
|
logger.info(f"[target] -> 找到匹配的红圈: 黄心({yellow_center}), "
|
||||||
f"红心({rc['center']}), 距离:{dist_centers:.1f}, "
|
f"红心({rc['center']}), 距离:{dist_centers:.1f}, "
|
||||||
|
|||||||
@@ -549,15 +549,20 @@ class WiFiManager:
|
|||||||
on_poor_quality_callback: WiFi质量差时的回调函数
|
on_poor_quality_callback: WiFi质量差时的回调函数
|
||||||
"""
|
"""
|
||||||
with self._wifi_quality_lock:
|
with self._wifi_quality_lock:
|
||||||
if self._wifi_quality_monitor_thread is not None and self._wifi_quality_monitor_thread.is_alive():
|
current_thread = self._wifi_quality_monitor_thread
|
||||||
|
current_stop_event = self._wifi_quality_stop_event
|
||||||
|
if (current_thread is not None and current_thread.is_alive()
|
||||||
|
and not current_stop_event.is_set()):
|
||||||
self.logger.warning("[WiFi Monitor] 监测线程已在运行")
|
self.logger.warning("[WiFi Monitor] 监测线程已在运行")
|
||||||
return
|
return
|
||||||
|
|
||||||
self._network_type_callback = network_type_callback
|
self._network_type_callback = network_type_callback
|
||||||
self._on_poor_quality_callback = on_poor_quality_callback
|
self._on_poor_quality_callback = on_poor_quality_callback
|
||||||
self._wifi_quality_stop_event.clear()
|
stop_event = threading.Event()
|
||||||
|
self._wifi_quality_stop_event = stop_event
|
||||||
self._wifi_quality_monitor_thread = threading.Thread(
|
self._wifi_quality_monitor_thread = threading.Thread(
|
||||||
target=self._quality_monitor_loop,
|
target=self._quality_monitor_loop,
|
||||||
|
args=(stop_event,),
|
||||||
daemon=True,
|
daemon=True,
|
||||||
name="wifi_quality_monitor"
|
name="wifi_quality_monitor"
|
||||||
)
|
)
|
||||||
@@ -568,13 +573,14 @@ class WiFiManager:
|
|||||||
"""停止 WiFi 质量监测线程"""
|
"""停止 WiFi 质量监测线程"""
|
||||||
with self._wifi_quality_lock:
|
with self._wifi_quality_lock:
|
||||||
t = self._wifi_quality_monitor_thread
|
t = self._wifi_quality_monitor_thread
|
||||||
|
stop_event = self._wifi_quality_stop_event
|
||||||
if t is None:
|
if t is None:
|
||||||
return
|
return
|
||||||
if not t.is_alive():
|
if not t.is_alive():
|
||||||
self._wifi_quality_monitor_thread = None
|
self._wifi_quality_monitor_thread = None
|
||||||
return
|
return
|
||||||
|
|
||||||
self._wifi_quality_stop_event.set()
|
stop_event.set()
|
||||||
try:
|
try:
|
||||||
t.join(timeout=2.0)
|
t.join(timeout=2.0)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@@ -588,12 +594,12 @@ class WiFiManager:
|
|||||||
self._wifi_quality_monitor_thread = None
|
self._wifi_quality_monitor_thread = None
|
||||||
self.logger.info("[WiFi Monitor] 已停止后台监测线程")
|
self.logger.info("[WiFi Monitor] 已停止后台监测线程")
|
||||||
|
|
||||||
def _quality_monitor_loop(self):
|
def _quality_monitor_loop(self, stop_event):
|
||||||
"""
|
"""
|
||||||
WiFi 质量监测循环(后台线程)
|
WiFi 质量监测循环(后台线程)
|
||||||
每 5 秒检查 STA 关联状态和 RSSI,发现断链或质量差则触发切换
|
每 5 秒检查 STA 关联状态和 RSSI,发现断链或质量差则触发切换
|
||||||
"""
|
"""
|
||||||
while not self._wifi_quality_stop_event.is_set():
|
while not stop_event.is_set():
|
||||||
try:
|
try:
|
||||||
# 只在 WiFi 连接时才测量
|
# 只在 WiFi 连接时才测量
|
||||||
network_type = self._network_type_callback()
|
network_type = self._network_type_callback()
|
||||||
@@ -631,7 +637,8 @@ class WiFiManager:
|
|||||||
self.logger.warning("[WiFi Monitor] 质量差,切换前快速重试 2 次(每次间隔1秒)")
|
self.logger.warning("[WiFi Monitor] 质量差,切换前快速重试 2 次(每次间隔1秒)")
|
||||||
|
|
||||||
for retry_idx in range(2):
|
for retry_idx in range(2):
|
||||||
time.sleep_ms(1000)
|
if stop_event.wait(1.0):
|
||||||
|
return
|
||||||
reachable2 = self.is_sta_associated()
|
reachable2 = self.is_sta_associated()
|
||||||
rtt2 = None
|
rtt2 = None
|
||||||
rssi2 = self._get_wifi_rssi_dbm()
|
rssi2 = self._get_wifi_rssi_dbm()
|
||||||
@@ -660,7 +667,7 @@ class WiFiManager:
|
|||||||
self._on_poor_quality_callback()
|
self._on_poor_quality_callback()
|
||||||
|
|
||||||
# 休眠 5 秒
|
# 休眠 5 秒
|
||||||
time.sleep(5)
|
stop_event.wait(5.0)
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.logger.error(f"[WiFi Monitor] 监测异常:{e}")
|
self.logger.error(f"[WiFi Monitor] 监测异常:{e}")
|
||||||
|
|||||||
Reference in New Issue
Block a user