12 Commits
Author SHA1 Message Date
linyimin 2a5c7ed2cb fix: 3.1.16 、10分钟无操作自动关机、日志改为warning级别 2026-09-23 17:34:20 +08:00
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
linyimin a09b45738a fix: camera flip 2026-09-02 13:43:16 +08:00
linyimin e4d8454947 fix: update version 2026-09-02 11:26:44 +08:00
linyimin c09189332d fix: 摄像头翻转 2026-09-02 11:25:33 +08:00
yrx a00baa1770 two ! 2026-09-02 11:22:01 +08:00
yrx 179b30a944 two 2026-09-01 11:07:46 +08:00
46 changed files with 1311 additions and 1153 deletions
+1 -1
View File
@@ -1,3 +1,3 @@
{
"cmake.sourceDirectory": "E:/code/code/code/new/new/new/new/new/nw/2.17.0/archery/cpp_ext"
"cmake.sourceDirectory": "E:/code/code/code/new/new/new/new/new/nw/archery - 副本/cpp_ext"
}
+88 -24
View File
@@ -109,6 +109,7 @@
from maix import app, uart, pinmap, time
import hashlib
import hmac
import re
import ujson
# ========== 配置 ==========
@@ -130,53 +131,109 @@ def generate_token(device_id):
return "Arrow_" + hmac.new((SALT + device_id).encode(), SALT2.encode(), hashlib.sha256).hexdigest()
def send_cmd(cmd_str, timeout_ms=3000):
"""发送 AT 指令并等待 OK / ERROR"""
"""发送 AT 指令并返回完整响应;超时返回已收到的内容。"""
print("[AT] =>", cmd_str)
http_serial.write((cmd_str + "\r\n").encode())
buffer = b""
start = time.ticks_ms()
while time.ticks_ms() - start < timeout_ms:
while time.ticks_diff(time.ticks_ms(), start) < timeout_ms:
data = http_serial.read(128)
if data:
buffer += data
try:
decoded = buffer.decode()
print("<= ", decoded.strip())
if "OK" in decoded:
return True
if "+CME ERROR" in decoded or "ERROR" in decoded:
return False
decoded = buffer.decode("utf-8", "ignore")
if "OK" in decoded or "+CME ERROR" in decoded or "ERROR" in decoded:
print("[AT] <=", decoded.strip())
return decoded
except:
pass
time.sleep_ms(10)
decoded = buffer.decode("utf-8", "ignore")
print("[AT] !! timeout", timeout_ms, "ms, response:", decoded.strip() or "<empty>")
return decoded
def response_ok(response):
return "OK" in response and "ERROR" not in response
def wait_modem_ready():
"""等待模组响应,并确认 PDP 上下文已经获得 IP。"""
for attempt in range(15):
if response_ok(send_cmd("AT", 1000)):
break
print("[4G] 等待模组启动", attempt + 1, "/15")
time.sleep_ms(1000)
else:
print("[4G] UART2 无 AT 响应,请检查模组供电、A28/A29 接线和串口占用")
return False
send_cmd("ATE0", 1000)
cpin = send_cmd("AT+CPIN?", 3000)
if "READY" not in cpin:
print("[4G] SIM 卡未就绪:", cpin.strip())
return False
addr = send_cmd("AT+CGPADDR=1", 3000)
match = re.search(r'\+CGPADDR:\s*1,"([^\"]+)"', addr)
if match and match.group(1) != "0.0.0.0":
print("[4G] PDP ready, IP:", match.group(1))
return True
send_cmd("AT+MIPCALL=1,1", 15000)
for _ in range(20):
addr = send_cmd("AT+CGPADDR=1", 3000)
match = re.search(r'\+CGPADDR:\s*1,"([^\"]+)"', addr)
if match and match.group(1) != "0.0.0.0":
print("[4G] PDP ready, IP:", match.group(1))
return True
time.sleep_ms(1000)
print("[4G] PDP 未获得 IP,请检查 SIM 流量、信号和 APN")
return False
def clear_http_instances():
for instance_id in range(6):
send_cmd(f"AT+MHTTPDEL={instance_id}", 1200)
def create_http_instance(url):
cmd = f'AT+MHTTPCREATE="{url}"'
if send_cmd(cmd):
# 尝试提取 instance ID(如果模块返回)
# 注意:部分模块不会返回 ID,可忽略,直接用 0 或 1
return True
return False
response = send_cmd(cmd, 8000)
match = re.search(r"\+MHTTPCREATE:\s*(\d+)", response)
if not response_ok(response) or not match:
print("❌ 创建 HTTP 实例失败,模组响应:", response.strip() or "<empty>")
return None
return int(match.group(1))
def send_http_request(url, api_path, token, device_id, json_data):
# 1. 创建 HTTP 实例
if not create_http_instance(url):
print("❌ 创建 HTTP 实例失败")
instance_id = create_http_instance(url)
if instance_id is None:
return False
# 2. 设置 Headers(假设实例 ID 为 0,或根据模块默认)
instance_id = 0 # 大多数模块默认实例为 0;若支持多实例,需解析返回值
send_cmd(f'AT+MHTTPCFG="header",{instance_id},"Content-Type: application/json"')
send_cmd(f'AT+MHTTPCFG="header",{instance_id},"Authorization: {token}"')
send_cmd(f'AT+MHTTPCFG="header",{instance_id},"DeviceId: {device_id}"')
# 2. 设置 Headers
commands = (
f'AT+MHTTPCFG="header",{instance_id},"Content-Type: application/json"',
f'AT+MHTTPCFG="header",{instance_id},"Authorization: {token}"',
f'AT+MHTTPCFG="header",{instance_id},"DeviceId: {device_id}"',
)
for command in commands:
if not response_ok(send_cmd(command)):
print("❌ HTTP Header 配置失败")
send_cmd(f"AT+MHTTPDEL={instance_id}", 2000)
return False
# 3. 发送 Body
json_str = ujson.dumps(json_data)
send_cmd(f'AT+MHTTPCONTENT={instance_id},0,0,"{json_str}"')
at_json = json_str.replace("\\", "\\\\").replace('"', '\\"')
if not response_ok(send_cmd(f'AT+MHTTPCONTENT={instance_id},0,0,"{at_json}"', 8000)):
print("❌ HTTP Body 配置失败")
send_cmd(f"AT+MHTTPDEL={instance_id}", 2000)
return False
# 4. 发起 POST 请求
if send_cmd(f'AT+MHTTPREQUEST={instance_id},2,0,"{api_path}"'):
if response_ok(send_cmd(f'AT+MHTTPREQUEST={instance_id},2,0,"{api_path}"', 15000)):
print("✅ HTTP 请求已发送")
return True
else:
@@ -199,7 +256,7 @@ def read_response(timeout_ms=5000):
print("🚀 启动直接上传流程...")
token = generate_token(device_id)
print("🔑 Token:", token)
print("🔑 Token 已生成:", token[:12] + "...")
# 构造模拟数据
timestamp = int(time.time() * 1000)
@@ -216,7 +273,14 @@ json_data = {
}
# 执行上传
if send_http_request(url, api_path, token, device_id, json_data):
upload_ok = False
if not wait_modem_ready():
print("💥 4G 模组未就绪")
else:
clear_http_instances()
upload_ok = send_http_request(url, api_path, token, device_id, json_data)
if upload_ok:
read_response()
else:
print("💥 上传流程失败")
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.
Binary file not shown.
Binary file not shown.
+4 -3
View File
@@ -1,6 +1,6 @@
id: t11
name: t11
version: 2.18.10
version: 3.1.16
author: t11
icon: ''
desc: t11
@@ -18,13 +18,14 @@ files:
- laser_manager.py
- logger_manager.py
- main.py
- model_326162.cvimodel
- model_326162.mud
- model_285484.cvimodel
- model_285484.mud
- network.py
- ota_curl.sh
- ota_manager.py
- power.py
- server.pem
- set_autostart.py
- shoot_manager.py
- shot_id_generator.py
- target_roi_yolo.py
+17 -1
View File
@@ -8,6 +8,15 @@ import threading
import config
from logger_manager import logger_manager
_USE_CV = False
try:
import cv2
import numpy as np
from maix import image as _maix_image
_USE_CV = True
except ImportError:
pass
class CameraManager:
"""相机管理器(单例)"""
@@ -57,6 +66,12 @@ class CameraManager:
with self._camera_lock:
if self._camera is None:
self._camera = camera.Camera(width, height)
v_flip = getattr(config, 'CAMERA_V_FLIP', False)
h_mirror = getattr(config, 'CAMERA_H_MIRROR', False)
if v_flip:
self._camera.vflip(1)
if h_mirror:
self._camera.hmirror(1)
return self._camera
@@ -101,7 +116,8 @@ class CameraManager:
with self._camera_lock:
if self._camera is None:
self.init_camera()
return self._camera.read()
frame = self._camera.read()
return frame
def show(self, image):
"""
+20 -24
View File
@@ -15,6 +15,8 @@ LOCAL_FILENAME = APP_DIR + "/main_tmp.py"
# 相机初始化分辨率(CameraManager / main.py 使用)
CAMERA_WIDTH = 640
CAMERA_HEIGHT = 480
CAMERA_V_FLIP = True # 摄像头垂直翻转(上下颠倒时设为 True)
CAMERA_H_MIRROR = True # 摄像头水平镜像(左右反了时设为 True)
# 三角形检测缩图比例:默认按相机最长边缩到 1/2(性能更稳;可按需调整)
# 取值范围建议 (0.25 ~ 1.0];1.0 表示不缩图
@@ -96,6 +98,11 @@ ADC_LASER_THRESHOLD = 3000
# ==================== 激光配置 ====================
MODULE_ADDR = 0x00
# 激光开关改由 A14 GPIO 控制:低电平开启,高电平关闭。
LASER_CONTROL_PIN = "A14"
LASER_CONTROL_GPIO = "GPIOA14"
LASER_CONTROL_ON_LEVEL = 0
LASER_CONTROL_OFF_LEVEL = 1
LASER_ON_CMD = bytes([0xAA, MODULE_ADDR, 0x01, 0xBE, 0x00, 0x01, 0x00, 0x01, 0xC1])
LASER_OFF_CMD = bytes([0xAA, MODULE_ADDR, 0x01, 0xBE, 0x00, 0x01, 0x00, 0x00, 0xC0])
DISTANCE_QUERY_CMD = bytes([0xAA, MODULE_ADDR, 0x00, 0x20, 0x00, 0x01, 0x00, 0x00, 0x21]) # 激光测距查询命令
@@ -234,10 +241,10 @@ TRIANGLE_BLACKHAT_KERNEL_FRAC = 0.018 # 核大小 ≈ min(h,w)*frac,取奇数
# ── YOLO(NPU) 靶环 ROI → 裁剪后再跑三角形(减小 CPU 处理面积)──────────────────
# 日志里 net_in=W×H 来自 .mud 模型(det.input_width/height),不是这里配置的。
TRIANGLE_YOLO_ROI_ENABLE = True
TRIANGLE_YOLO_MODEL_PATH = APP_DIR + "/model_317211.mud"
TRIANGLE_YOLO_MODEL_PATH = APP_DIR + "/model_270139.mud"
# 参与 ROI 的类别:多类时只填「整靶/靶环」的 id;不要填角标类,否则 union 仍可对,但 largest 会偏小。
TRIANGLE_YOLO_RING_CLASS_IDS = (0,)
TRIANGLE_YOLO_CONF_TH = 0.9
TRIANGLE_YOLO_CONF_TH = 0.7
TRIANGLE_YOLO_IOU_TH = 0.45
# YOLO 首次/临界帧可能在高阈值下 0 框;启用后仅在 0 候选时用较低阈值重试一次。
# 后续仍会经过 min_box_side、ROI aspect、三角形几何校验,避免直接放大假阳性。
@@ -262,25 +269,14 @@ TRIANGLE_SAMPLE_PATCH_HALF_PX = 2
# 开机阶段预加载 YOLO detector;detect 使用 dual_buff=False,避免返回上一帧结果。
TRIANGLE_YOLO_PRELOAD_ON_BOOT = False
# YOLO target size classification: class_id → label,下标即 class_id:0=40cm, 1=0(靶心), 2=20cm.
# YOLO target size classification: class 0=20cm, class 1=40cm.
TARGET_CLASS_YOLO_ENABLE = True
TARGET_CLASS_YOLO_MODEL_PATH = APP_DIR + "/model_326162.mud"
TARGET_CLASS_YOLO_LABELS = (40, 0, 20)
# 只有映射结果落在该集合内才用于上报(如 class 1→label 0 直接忽略)
TARGET_CLASS_YOLO_VALID_LABELS = (20, 40)
# 解析阶段直接丢弃的 class_id(1=靶心),不参与任何选择/上报
TARGET_CLASS_YOLO_IGNORE_CLASS_IDS = (1,)
TARGET_CLASS_YOLO_CONF_TH = 0.5
# 首次检测 0 结果时用该阈值重新检测一次(≥CONF_TH 时按 CONF_TH 同参重跑)
TARGET_CLASS_YOLO_RETRY_CONF_TH = 0.25
# 检出靶框后裁剪放大再 detect 一次,与全图结果互相印证(不一致以复检为准)
TARGET_CLASS_YOLO_RECHECK_CROP = True
TARGET_CLASS_YOLO_RECHECK_MARGIN_FRAC = 0.1
# 复检的 detect 阈值与接受门槛:置信度大于该值即采纳复检结果
TARGET_CLASS_YOLO_RECHECK_CONF_TH = 0.25
# 全图结果(无复检时)的上报置信度门槛,None/0 表示不启用
TARGET_CLASS_YOLO_MIN_REPORT_CONF = 0.5
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 训练数据一致)→ 子框内传统算法取直角点 ──
@@ -337,13 +333,10 @@ LOG_QUEUE_MAXSIZE = 10000 # 日志队列上限
MAX_CMD_THREADS = 10 # 并发命令线程上限(防止服务器下发命令时无限创建线程)
# ==================== 图像保存配置 ====================
SAVE_IMAGE_ENABLED = True # 是否保存图像(True=保存,False=不保存)
SAVE_IMAGE_ENABLED = False # 是否保存图像(True=保存,False=不保存)
SAVE_IMAGE_ON_FAILURE = True # 检测失败时是否强制保存图像(供调试测试用)
PHOTO_DIR = "/root/phot" # 照片存储目录
MAX_IMAGES = 1000
SAVE_RAW_IMAGE_ENABLED = False # 额外保存完整原始帧(不画框、不画点、不裁剪)
RAW_IMAGE_DIR = PHOTO_DIR + "/raw"
RAW_IMAGE_MAX_IMAGES = MAX_IMAGES
# Stage2 调试目录(默认 PHOTO_DIR/stage2_roi)内 JPEG 最多保留张数;None 表示与 MAX_IMAGES 相同
TRIANGLE_BLACK_YOLO_STAGE2_ROI_MAX_IMAGES = None
@@ -362,11 +355,14 @@ PIN_MAPPINGS = {
"A28": "UART2_TX",
"A15": "I2C5_SCL",
"A27": "I2C5_SDA",
"A14": "GPIOA14", # 激光开关:低开、高关
"A24": "GPIOA24", # 电源板关机控制
"A25": "GPIOA25", # 电源状态绿灯
"A23": "GPIOA23", # 电源状态红灯
}
# ==================== 电源配置 ====================
AUTO_POWER_OFF_IN_SECONDS = 100 * 60 # 自动关机时间(秒),0表示不自动关机
AUTO_POWER_OFF_IN_SECONDS = 10 * 60 # 自动关机时间(秒),0表示不自动关机
# 充电时自动关机暂时禁用;需要恢复时改为 True。
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",
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,
"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(
+59
View File
@@ -51,6 +51,43 @@ namespace netcore {
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 数据包
py::tuple parse_packet(py::bytes data) {
// 1) 转换为 bytes view
@@ -110,4 +147,26 @@ namespace netcore {
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 {
// 打包 TCP 数据包
// 打包 TCP 数据包 (JSON body)
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);
// 解包 TCP 数据包 -> (msg_type, body_bytes) 不做 JSON 解析
py::tuple parse_packet_raw(py::bytes data);
}
+79 -1
View File
@@ -5,6 +5,7 @@
提供硬件对象的统一管理和访问
"""
from maix import time
import _thread
import config
from at_client import ATClient
@@ -28,6 +29,7 @@ class HardwareManager:
self._bus = None # I2C总线
self._adc_obj = None # ADC对象
self._at_client = None # AT客户端
self._status_led_monitor_started = False
self._last_active_time = 0 # 用于记录用户的最后一次活跃的时间
self._stop_timer = False # 用于停止定时器的标志
@@ -104,11 +106,87 @@ class HardwareManager:
# 物理引脚是 A24,对应 GPIO 功能是 GPIOA24
# 注意:这里需要先在 config.PIN_MAPPINGS 中配置好 "A24": "GPIOA24"
from maix import gpio
# 输出高电平关闭
# 一代电源板关机信号为高电平
gpio.GPIO("GPIOA24", gpio.Mode.OUT).value(1)
except Exception as e:
print(f"关机失败: {e}")
def start_status_led_monitor(self):
"""后台更新状态灯:正常/充满绿常亮、充电绿闪烁、低电量红常亮。"""
if self._status_led_monitor_started:
return
self._status_led_monitor_started = True
_thread.start_new_thread(self._status_led_loop, ())
def _status_led_loop(self):
from maix import gpio
from power import get_bus_voltage, is_charging, voltage_to_percent
try:
green = None
if getattr(config, "STATUS_LED_GREEN_ENABLED", True):
green = gpio.GPIO(config.STATUS_LED_GREEN_GPIO, gpio.Mode.OUT)
red = None
if getattr(config, "STATUS_LED_RED_ENABLED", True):
red = gpio.GPIO(config.STATUS_LED_RED_GPIO, gpio.Mode.OUT)
active = int(config.STATUS_LED_ACTIVE_LEVEL)
inactive = 0 if active else 1
if green is not None:
green.value(inactive)
if red is not None:
red.value(inactive)
last_state = None
blink_on = False
blink_period = max(100, int(config.STATUS_LED_CHARGING_BLINK_MS))
poll_ms = max(100, int(config.STATUS_LED_POLL_MS))
tick_ms = min(blink_period, poll_ms)
sensor_elapsed = poll_ms
blink_elapsed = blink_period
state = "normal"
percent = None
charging = False
while self._status_led_monitor_started:
if sensor_elapsed >= poll_ms:
voltage = get_bus_voltage()
percent = voltage_to_percent(voltage) if voltage > 0 else None
charging = is_charging()
low = percent is not None and percent <= int(config.STATUS_LED_LOW_BATTERY_PERCENT)
full = percent is not None and percent >= int(config.STATUS_LED_FULL_BATTERY_PERCENT)
if charging:
state = "full" if full else "charging"
else:
state = "low" if low else "normal"
sensor_elapsed = 0
if state == "low":
if green is not None:
green.value(inactive)
if red is not None:
red.value(active)
elif state == "charging":
if blink_elapsed >= blink_period:
blink_on = not blink_on
blink_elapsed = 0
if green is not None:
green.value(active if blink_on else inactive)
if red is not None:
red.value(inactive)
else: # normal or full
if green is not None:
green.value(active)
if red is not None:
red.value(inactive)
if state != last_state:
print(f"[STATUS_LED] state={state} percent={percent} charging={charging}")
last_state = state
time.sleep_ms(tick_ms)
sensor_elapsed += tick_ms
blink_elapsed += tick_ms
except Exception as e:
self._status_led_monitor_started = False
print(f"[STATUS_LED] monitor failed: {e}")
def start_idle_timer(self):
self._stop_timer = False
self._last_active_time = time.time()
+42 -70
View File
@@ -31,6 +31,7 @@ class LaserManager:
# 私有状态
self._serial = None # 激光串口,由 laser_manager 自己持有
self._laser_gpio = None # A14 激光开关,低电平开启、高电平关闭
self._calibration_active = False
self._calibration_result = None
self._calibration_lock = threading.Lock()
@@ -69,10 +70,21 @@ class LaserManager:
# ==================== 初始化方法 ====================
def init_control_gpio(self):
"""尽早初始化 A14,并拉高确保激光关闭。"""
from maix import gpio, pinmap
pinmap.set_pin_function(config.LASER_CONTROL_PIN, config.LASER_CONTROL_GPIO)
if self._laser_gpio is None:
self._laser_gpio = gpio.GPIO(config.LASER_CONTROL_GPIO, gpio.Mode.OUT)
self._laser_gpio.value(config.LASER_CONTROL_OFF_LEVEL)
self._laser_turned_on = False
print(f"[LASER] {config.LASER_CONTROL_PIN}=HIGH,激光已关闭")
def init(self, serial_device=None, baudrate=None):
"""
初始化激光模块(包括串口)
初始化完成后主动发送关闭命令,防止 UART 初始化噪声误触发激光
初始化激光模块(A14 开关 + 测距串口)
初始化时先将 A14 拉高关闭激光,防止开机误触发
Args:
serial_device: 串口设备路径,默认使用 config.DISTANCE_SERIAL_DEVICE
@@ -82,23 +94,11 @@ class LaserManager:
device = serial_device or config.DISTANCE_SERIAL_DEVICE
baud = baudrate or config.DISTANCE_SERIAL_BAUDRATE
self.init_control_gpio()
self._serial = uart.UART(device, baud)
print(f"[LASER] 激光串口初始化完成: device={device}, baudrate={baud}")
# 等待串口稳定后主动关闭激光,防止初始化噪声误触发
time.sleep_ms(100)
try:
self._serial.read(-1) # 清空接收缓冲区
except Exception:
pass
self._serial.write(config.LASER_OFF_CMD)
time.sleep_ms(60)
try:
self._serial.read(-1) # 清空回包
except Exception:
pass
print("[LASER] 已发送关闭命令(防止开机误触发)")
# ==================== 业务方法 ====================
def load_laser_point(self):
@@ -147,66 +147,38 @@ class LaserManager:
return False
def turn_on_laser(self):
"""发送指令开启激光,并读取回包(部分模块支持)"""
if self._serial is None:
self.logger.error("[LASER] 激光串口未初始化,请先调用 init()")
return None
# 打印调试信息
self.logger.info(f"[LASER] 发送开启命令: {config.LASER_ON_CMD.hex()}")
# 清空接收缓冲区
"""A14 输出低电平,开启激光。"""
if self._laser_gpio is None:
if self.logger:
self.logger.error("[LASER] A14 GPIO 未初始化,请先调用 init()")
return False
try:
self._serial.read(-1) # 清空缓冲区
except:
pass
# 发送命令
written = self._serial.write(config.LASER_ON_CMD)
self.logger.info(f"[LASER] 写入字节数: {written}")
time.sleep_ms(60)
# 读取回包
resp = self._serial.read(len=20, timeout=10)
if resp:
self.logger.info(f"[LASER] 收到回包 ({len(resp)}字节): {resp.hex()}")
if resp == config.LASER_ON_CMD:
self.logger.info("✅ 激光开启指令已确认")
else:
self.logger.warning("🔇 无回包(可能正常或模块不支持回包)")
self._laser_gpio.value(config.LASER_CONTROL_ON_LEVEL)
self._laser_turned_on = True
return resp
if self.logger:
self.logger.info("[LASER] A14=LOW,激光开启")
return True
except Exception as e:
if self.logger:
self.logger.error(f"[LASER] A14 开启激光失败: {e}")
return False
def turn_off_laser(self):
"""发送指令关闭激光"""
if self._serial is None:
self.logger.error("[LASER] 激光串口未初始化,请先调用 init()")
return None
# 打印调试信息
self.logger.info(f"[LASER] 发送关闭命令: {config.LASER_OFF_CMD.hex()}")
# 清空接收缓冲区
"""A14 输出高电平,关闭激光。"""
if self._laser_gpio is None:
if self.logger:
self.logger.error("[LASER] A14 GPIO 未初始化,请先调用 init()")
return False
try:
self._serial.read(-1)
except:
pass
# 发送命令
written = self._serial.write(config.LASER_OFF_CMD)
self.logger.info(f"[LASER] 写入字节数: {written}")
time.sleep_ms(60)
# 读取回包
resp = self._serial.read(20)
if resp:
self.logger.info(f"[LASER] 收到回包 ({len(resp)}字节): {resp.hex()}")
else:
self.logger.warning("🔇 无回包")
self._laser_gpio.value(config.LASER_CONTROL_OFF_LEVEL)
self._laser_turned_on = False
return resp
if self.logger:
self.logger.info("[LASER] A14=HIGH,激光关闭")
return True
except Exception as e:
if self.logger:
self.logger.error(f"[LASER] A14 关闭激光失败: {e}")
return False
def flash_laser(self, duration_ms=1000):
"""闪一下激光(非阻塞版本)"""
+9 -11
View File
@@ -78,12 +78,14 @@ def laser_calibration_worker():
import traceback
traceback.print_exc()
time.sleep_ms(1000) # 等待1秒后继续
def cmd_str():
"""主程序入口"""
# ==================== 第一阶段:硬件初始化 ====================
# 按照 main104.py 的顺序,先完成所有硬件初始化
# 开机第一步先拉高 A14 关闭激光,避免其他硬件初始化期间误亮。
laser_manager.init_control_gpio()
# 1. 引脚功能映射
for pin, func in config.PIN_MAPPINGS.items():
try:
@@ -105,6 +107,8 @@ def cmd_str():
print(f"[BOOT] init_ina226 开始 wall_s={_w_boot:.3f}")
init_ina226()
print(f"[BOOT] init_ina226 结束 wall +{int(round((wall_time.time() - _w_boot) * 1000))} ms")
# 启动 A25 绿灯和 A23 红灯状态指示。
hardware_manager.start_status_led_monitor()
# 4. 初始化显示和相机
_w_boot = wall_time.time()
@@ -170,16 +174,14 @@ def cmd_str():
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 and not os.path.exists(_ota_pending_path):
if _preload_yolo:
preload_yolo_detector(logger)
elif _preload_yolo and logger:
logger.warning("[YOLO] ota_pending.json found; skip model preload until rollback check")
except Exception as e:
if logger:
logger.warning(f"[YOLO-ROI] 启动预加载异常(不影响后续射箭): {e}")
# 3. 启动时检查:是否需要恢复备份
pending_path = _ota_pending_path
pending_path = f"{config.APP_DIR}/ota_pending.json"
if os.path.exists(pending_path):
try:
with open(pending_path, 'r', encoding='utf-8') as f:
@@ -304,11 +306,7 @@ def cmd_str():
logger.error(f"[OTA] 处理临时目录失败: {e}")
# 5. 创建照片存储目录(如果启用图像保存或检测失败时强制保存)
if (
config.SAVE_IMAGE_ENABLED
or getattr(config, "SAVE_IMAGE_ON_FAILURE", False)
or getattr(config, "SAVE_RAW_IMAGE_ENABLED", False)
):
if config.SAVE_IMAGE_ENABLED or getattr(config, "SAVE_IMAGE_ON_FAILURE", False):
photo_dir = config.PHOTO_DIR
if photo_dir not in os.listdir("/root"):
try:
@@ -445,7 +443,7 @@ def cmd_str():
pressure_max = adc_val
if len(pressure_buf) >= PRESSURE_BATCH_SIZE:
_flush_pressure_buf("batch")
# 突变增量检测:压力增量大于400时触发
# 突变增量检测:压力增量大于300时触发
# 触发后需等气压降到触发值以下才重新检测增量
if adc_val < trigger_adc_val :
enable_check = True
Binary file not shown.
+2 -2
View File
@@ -1,7 +1,7 @@
[basic]
type = cvimodel
model = model_326162.cvimodel
model = model_285484.cvimodel
[extra]
model_type = yolov5
@@ -9,5 +9,5 @@ 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 = 40, 00, 20
labels = 20, 40
+603
View File
@@ -0,0 +1,603 @@
#!/usr/bin/env python3
# PYTHON_ARGCOMPLETE_OK
import sys
import logging
import os
import re
import os.path
import collections
import uuid
import argparse
import tarfile
import io
from struct import pack, unpack
PYTHON_MIN_VERSION = (3, 5, 2) # Ubuntu 16.04 LTS contains Python v3.5.2 by default
if sys.version_info < PYTHON_MIN_VERSION:
print("Python >= %r is required" % (PYTHON_MIN_VERSION,))
sys.exit(-1)
try:
import coloredlogs
except ImportError:
coloredlogs = None
try:
import argcomplete
except ImportError:
argcomplete = None
TOC_HEADER_NAME = 0xAA640001
FIP_MAX_SIZE = 0xA0000
FIP_ALIGN_SIZE = 2 * 1024
ENTRY_SIZE = 0x28
IV_ZERO = b"\0" * 16
class FIP_HEADER_FLAG:
BitRange = collections.namedtuple("BitRange", "shift, bits")
REE_SCS = BitRange(0, 2)
REE_ENCRYPTION = BitRange(2, 2)
@classmethod
def test(cls, value, flag):
v = value >> flag.shift
v &= (1 << flag.bits) - 1
return v
@classmethod
def value(cls, flag):
v = (1 << flag.bits) - 1
v <<= flag.shift
return v
class FIP_UUID:
# from arm-trusted-firmware/include/tools_share/firmware_image_package.h
uuid_c_define = """
/* ToC Entry UUIDs */
#define UUID_LICENSE_FILE \
{0x25360c62, 0x5151, 0x48ad, 0xb5, 0x91, {0x2d, 0x35, 0x67, 0x26, 0x85, 0xa5} }
#define UUID_TRUSTED_UPDATE_FIRMWARE_SCP_BL2U \
{0x03279265, 0x742f, 0x44e6, 0x8d, 0xff, {0x57, 0x9a, 0xc1, 0xff, 0x06, 0x10} }
#define UUID_TRUSTED_UPDATE_FIRMWARE_BL2U \
{0x37ebb360, 0xe5c1, 0x41ea, 0x9d, 0xf3, {0x19, 0xed, 0xa1, 0x1f, 0x68, 0x01} }
#define UUID_TRUSTED_UPDATE_FIRMWARE_NS_BL2U \
{0x111d514f, 0xe52b, 0x494e, 0xb4, 0xc5, {0x83, 0xc2, 0xf7, 0x15, 0x84, 0x0a} }
#define UUID_TRUSTED_FWU_CERT \
{0xb28a4071, 0xd618, 0x4c87, 0x8b, 0x2e, {0xc6, 0xdc, 0xcd, 0x50, 0xf0, 0x96} }
#define UUID_TRUSTED_BOOT_FIRMWARE_BL2 \
{0x0becf95f, 0x224d, 0x4d3e, 0xa5, 0x44, {0xc3, 0x9d, 0x81, 0xc7, 0x3f, 0x0a} }
#define UUID_BLD \
{0x3dfd6697, 0xbe89, 0x49e8, 0xae, 0x5d, {0x78, 0xa1, 0x40, 0x60, 0x82, 0x13} }
#define UUID_EL3_RUNTIME_FIRMWARE_BL31 \
{0x6d08d447, 0xfe4c, 0x4698, 0x9b, 0x95, {0x29, 0x50, 0xcb, 0xbd, 0x5a, 0x00} }
#define UUID_SECURE_PAYLOAD_BL32 \
{0x89e1d005, 0xdc53, 0x4713, 0x8d, 0x2b, {0x50, 0x0a, 0x4b, 0x7a, 0x3e, 0x38} }
#define UUID_NON_TRUSTED_FIRMWARE_BL33 \
{0xa7eed0d6, 0xeafc, 0x4bd5, 0x97, 0x82, {0x99, 0x34, 0xf2, 0x34, 0xb6, 0xe4} }
/* Key certificates */
#define UUID_ROT_KEY_CERT \
{0x721d2d86, 0x60f8, 0x11e4, 0x92, 0x0b, {0x8b, 0xe7, 0x62, 0x16, 0x0f, 0x24} }
#define UUID_BLD1_KEY_CERT \
{0x90e87e82, 0x60f8, 0x11e4, 0xa1, 0xb4, {0x77, 0x7a, 0x21, 0xb4, 0xf9, 0x4c} }
#define UUID_BLD2_KEY_CERT \
{0xa1214202, 0x60f8, 0x11e4, 0x8d, 0x9b, {0xf3, 0x3c, 0x0e, 0x15, 0xa0, 0x14} }
#define UUID_SOC_FW_KEY_CERT \
{0xccbeb88a, 0x60f9, 0x11e4, 0x9a, 0xd0, {0xeb, 0x48, 0x22, 0xd8, 0xdc, 0xf8} }
#define UUID_TRUSTED_OS_FW_KEY_CERT \
{0x03d67794, 0x60fb, 0x11e4, 0x85, 0xdd, {0xb7, 0x10, 0x5b, 0x8c, 0xee, 0x04} }
#define UUID_BL33_KEY_CERT \
{0x2a83d58a, 0x60fb, 0x11e4, 0x8a, 0xaf, {0xdf, 0x30, 0xbb, 0xc4, 0x98, 0x59} }
/* Content certificates */
#define UUID_TRUSTED_BOOT_FW_CERT \
{0xea69e2d6, 0x635d, 0x11e4, 0x8d, 0x8c, {0x9f, 0xba, 0xbe, 0x99, 0x56, 0xa5} }
#define UUID_BLD_CONTENT_CERT \
{0x046fbe44, 0x635e, 0x11e4, 0xb2, 0x8b, {0x73, 0xd8, 0xea, 0xae, 0x96, 0x56} }
#define UUID_SOC_FW_CONTENT_CERT \
{0x200cb2e2, 0x635e, 0x11e4, 0x9c, 0xe8, {0xab, 0xcc, 0xf9, 0x2b, 0xb6, 0x66} }
#define UUID_TRUSTED_OS_FW_CONTENT_CERT \
{0x11449fa4, 0x635e, 0x11e4, 0x87, 0x28, {0x3f, 0x05, 0x72, 0x2a, 0xf3, 0x3d} }
#define UUID_BL33_CONTENT_CERT \
{0xf3c1c48e, 0x635d, 0x11e4, 0xa7, 0xa9, {0x87, 0xee, 0x40, 0xb2, 0x3f, 0xa7} }
/* CV keys */
#define UUID_CV_TRUSTED_KEY_CERT \
{0x64fbfc49, 0x4b8c, 0x4ad3, 0xb9, 0x92, {0x93, 0x55, 0x89, 0xee, 0xf0, 0x12} }
#define UUID_CV_NON_TRUSTED_KEY_CERT \
{0xcb48bf0d, 0x7012, 0x4201, 0xbc, 0x35, {0x8a, 0x51, 0xc4, 0x90, 0x90, 0x94} }
/* DDR init*/
#define UUID_CV_DDRINIT_KEY_CERT \
{0xa61c53c9, 0x886c, 0x484f, 0x96, 0x5d, {0xd2, 0xda, 0xd7, 0xc3, 0xeb, 0x13} }
#define UUID_CV_DDRINIT_CONTENT_CERT \
{0x9dfaabd2, 0x7f1b, 0x47e6, 0xa8, 0xa6, {0x6a, 0xc3, 0x10, 0xcc, 0xac, 0x91} }
#define UUID_CV_DDRINIT \
{0x5888a5cd, 0x38fc, 0x4f66, 0xae, 0x3d, {0x2e, 0x18, 0x6d, 0x69, 0x41, 0xfb} }
/* Fast boot */
#define UUID_CV_FASTBOOT_KEY_CERT \
{0x285df54e, 0x7b50, 0x4309, 0x9b, 0x52, {0x4b, 0xc4, 0x92, 0x82, 0x60, 0xdd} }
#define UUID_CV_FASTBOOT_CONTENT_CERT \
{0x61f7595b, 0x8d77, 0x4e13, 0x91, 0x2a, {0x63, 0x6e, 0x58, 0xda, 0x5b, 0x69} }
#define UUID_CV_FASTBOOT \
{0x43766198, 0xc363, 0x48db, 0xa9, 0x97, {0xf1, 0x0e, 0x93, 0x80, 0x4f, 0xea} }
"""
@classmethod
def cls_init(cls):
txt = cls.uuid_c_define
txt = txt.replace("\r\n", "\n")
txt = txt.replace("\\\n", "\n")
rx = r"""
\#define\s+
(?P<name>\S+)\s+
{
\s*(?P<u0>0x\S+)\s*,\s*
\s*(?P<u1>0x\S+)\s*,\s*
\s*(?P<u2>0x\S+)\s*,\s*
\s*(?P<u3>0x\S+)\s*,\s*
\s*(?P<u4>0x\S+)\s*,\s*
{
\s*(?P<u5>0x\S+)\s*,\s*
\s*(?P<u6>0x\S+)\s*,\s*
\s*(?P<u7>0x\S+)\s*,\s*
\s*(?P<u8>0x\S+)\s*,\s*
\s*(?P<u9>0x\S+)\s*,\s*
\s*(?P<u10>0x\S+)\s*
}\s*,?\s*
}
"""
for m in re.finditer(rx, txt, flags=re.X):
name = m.group("name")
u = m.group(*["u%d" % i for i in range(11)])
u = [int(i, 0) for i in u]
u = pack("<IHHBBBBBBBB", *u)
u = uuid.UUID(bytes=u)
setattr(cls, name, u)
class Entry:
__slots__ = ["name", "loc", "uuid", "address", "flag", "content"]
def __init__(self):
self.loc = 0
self.uuid = uuid.UUID(int=0)
self.address = 0
self.flag = 0
self.content = b""
@classmethod
def make(cls, uuid, content):
entry = cls()
entry.uuid = uuid
entry.content = content
return entry
@classmethod
def from_fip(cls, name, loc, fip_bin):
data = fip_bin[loc : loc + ENTRY_SIZE]
uuid_bytes, address, size, flag = unpack("<16sQQQ", data)
content = fip_bin[address : address + size]
entry = cls()
entry.name = name
entry.loc = loc
entry.uuid = uuid.UUID(bytes=uuid_bytes)
entry.address = address
entry.flag = flag
entry.content = content
return entry
def to_bytes(self):
return pack("<16sQQQ", self.uuid.bytes, self.address, self.size, self.flag)
@property
def size(self):
return len(self.content)
@property
def end(self):
return self.address + self.size
def __str__(self):
return "<%-31s loc=0x%03x U=%s a=0x%05x,0x%05x,0x%05x f=0x%x>" % (
self.name,
self.loc,
self.uuid.hex[:8],
self.address,
self.end,
self.size,
self.flag,
)
class FIP:
ENTRY_NAMES = collections.OrderedDict(
[
("LICENSE_FILE", "UUID_LICENSE_FILE"),
("BL2", "UUID_TRUSTED_BOOT_FIRMWARE_BL2"),
("BLD", "UUID_BLD"),
("BL31", "UUID_EL3_RUNTIME_FIRMWARE_BL31"),
("BL32", "UUID_SECURE_PAYLOAD_BL32"),
("BL33", "UUID_NON_TRUSTED_FIRMWARE_BL33"),
("BLD1_KEY_CERT", "UUID_BLD1_KEY_CERT"),
("BLD2_KEY_CERT", "UUID_BLD2_KEY_CERT"),
("CV_TRUSTED_KEY_CERT", "UUID_CV_TRUSTED_KEY_CERT"),
("SOC_FW_KEY_CERT", "UUID_SOC_FW_KEY_CERT"),
("TRUSTED_OS_FW_KEY_CERT", "UUID_TRUSTED_OS_FW_KEY_CERT"),
("CV_NON_TRUSTED_KEY_CERT", "UUID_CV_NON_TRUSTED_KEY_CERT"),
("BL33_KEY_CERT", "UUID_BL33_KEY_CERT"),
("TRUSTED_BOOT_FW_CERT", "UUID_TRUSTED_BOOT_FW_CERT"),
("BLD_CONTENT_CERT", "UUID_BLD_CONTENT_CERT"),
("SOC_FW_CONTENT_CERT", "UUID_SOC_FW_CONTENT_CERT"),
("TRUSTED_OS_FW_CONTENT_CERT", "UUID_TRUSTED_OS_FW_CONTENT_CERT"),
("BL33_CONTENT_CERT", "UUID_BL33_CONTENT_CERT"),
("CV_DDRINIT", "UUID_CV_DDRINIT"),
("CV_FASTBOOT", "UUID_CV_FASTBOOT"),
]
)
TOC_Header = collections.namedtuple(
"TOC_Header", "name, serial, flag_res, flag_plat, flag_res2"
)
def __init__(self, path):
logging.info("FIP_BIN: %s", path)
self.path = path
def load(self):
with open(self.path, "rb") as fp:
self.binary = fp.read(FIP_MAX_SIZE)
logging.info("%s is %d bytes", self.path, len(self.binary))
self.header = self.TOC_Header(*unpack("<IIIHH", self.binary[0x00:0x10]))
if self.header.name != TOC_HEADER_NAME:
raise ValueError(
"FIP header is 0x%08x but should be 0x%08x"
% (self.header[0], TOC_HEADER_NAME)
)
logging.info("TOC header: flag_plat=0x%04x", self.header.flag_plat)
logging.info(
" REE_SCS: %r",
FIP_HEADER_FLAG.test(self.header.flag_plat, FIP_HEADER_FLAG.REE_SCS),
)
logging.info(
" REE_ENCRYPTION: %r",
FIP_HEADER_FLAG.test(self.header.flag_plat, FIP_HEADER_FLAG.REE_ENCRYPTION),
)
ents = []
for k, v in self.ENTRY_NAMES.items():
try:
ents.append((k, self.find_entry(v)))
except ValueError as err:
logging.warning("%s", err)
ents.sort(key=lambda x: x[1].address)
for n, (k, v) in enumerate(ents):
logging.debug("%s", v)
if n > 0:
pk, pv = ents[n - 1]
if v.loc != pv.loc + ENTRY_SIZE or v.address != pv.address + pv.size:
raise Exception("Invalid FIP")
rest = self.binary[ents[-1][1].end :]
loc = rest.find(b"APLB")
if loc < 0:
raise Exception("No BLD/DDRC")
self.blp_ddrc_binary = rest[loc:]
logging.debug("blp_ddrc: 0x%04x at 0x%08x", len(self.blp_ddrc_binary), loc)
self.ents = collections.OrderedDict(ents)
def make_fip(self, output_path=None):
logging.info("New TOC header: flag_plat=0x%04x", self.header.flag_plat)
header_bin = pack("<IIIHH", *self.header)
fip_bin = header_bin
# Sort self.ents by the order of FIP.ENTRY_NAMES
sorted_ents = collections.OrderedDict()
for name in self.ENTRY_NAMES:
try:
sorted_ents[name] = self.ents[name]
except KeyError:
pass
self.ents = sorted_ents
offset = (len(self.ents) + 1) * ENTRY_SIZE + 0x10
for k, v in self.ents.items():
v.address = offset
fip_bin += v.to_bytes()
offset += v.size
null_entry = Entry()
null_entry.address = offset
fip_bin += null_entry.to_bytes()
for k, v in self.ents.items():
fip_bin += v.content
if (len(fip_bin) % FIP_ALIGN_SIZE) > 0:
fip_bin += b"\x00" * (FIP_ALIGN_SIZE - len(fip_bin) % FIP_ALIGN_SIZE)
fip_bin += self.blp_ddrc_binary
if output_path:
path = output_path
else:
path = os.path.splitext(self.path)
path = path[0] + "_signed_encrypted" + path[1]
logging.info("Save new FIP image to %s", path)
with open(path, "wb") as fp:
fp.write(fip_bin)
def dump_uuids(self):
for k, v in vars(FIP_UUID).items():
if k.startswith("UUID_"):
print("%-38s" % k, v.hex)
def find_entry(self, name):
# UUID=0, offset=any, size=0, flags=0
nullm = re.search(rb"\0{16}.{8}\0{16}", self.binary, flags=re.DOTALL)
if nullm is None:
raise Exception("NULL TOC entry is not found")
max_toc_size = nullm.start(0)
uuid = getattr(FIP_UUID, name)
loc = self.binary.find(uuid.bytes, 0, max_toc_size)
if loc < 0:
raise ValueError("%s is not found" % name)
return Entry.from_fip(name, loc, self.binary)
def entry(args):
logging.debug("cmd_fip")
def init_logging(log_file=None, file_level="DEBUG", stdout_level="WARNING"):
root_logger = logging.getLogger()
root_logger.setLevel(logging.NOTSET)
fmt = "%(asctime)s %(levelname)8s:%(name)s:%(message)s"
if log_file is not None:
file_handler = logging.FileHandler(log_file, encoding="utf-8")
file_handler.setFormatter(logging.Formatter(fmt))
file_handler.setLevel(file_level)
root_logger.addHandler(file_handler)
if coloredlogs:
os.environ["COLOREDLOGS_DATE_FORMAT"] = "%H:%M:%S"
field_styles = {
"asctime": {"color": "green"},
"hostname": {"color": "magenta"},
"levelname": {"color": "black", "bold": True},
"name": {"color": "blue"},
"programname": {"color": "cyan"},
}
level_styles = coloredlogs.DEFAULT_LEVEL_STYLES
level_styles["debug"]["color"] = "cyan"
coloredlogs.install(
level=stdout_level,
fmt=fmt,
field_styles=field_styles,
level_styles=level_styles,
milliseconds=True,
)
def parse_fip(fip_path):
logging.debug("parse_fip: %s", fip_path)
fip = FIP(fip_path)
fip.load()
def unpack_fip(fip_path):
logging.debug("unpack_fip: %s", fip_path)
fip = FIP(fip_path)
fip.load()
def save(name, content):
fn = os.path.splitext(fip_path)
fn = "%s_%s%s" % (fn[0], name, fn[1])
logging.info("Save %s", fn)
with open(fn, "wb") as fp:
fp.write(content)
for k, v in fip.ents.items():
save(k, v.content)
save("BLP_DDRC", fip.blp_ddrc_binary)
def tar_bld(fip_path, output_path, multibin):
logging.debug("tar_bld: %s multibin=%r", fip_path, multibin)
fip = FIP(fip_path)
fip.load()
members = [
"BLD_CONTENT_CERT",
"BLD2_KEY_CERT",
"BLD1_KEY_CERT",
"CV_DDRINIT" if multibin else "BLD",
]
if not output_path:
output_path = os.path.join(os.path.dirname(fip_path), "bld.tar")
logging.info("bld_tar_path=%s", output_path)
with tarfile.open(output_path, "w") as tf:
for m in members:
logging.debug("Tar %s", m)
try:
fp = io.BytesIO(fip.ents[m].content)
except KeyError:
logging.warning("%s doesn't exist", m)
continue
info = tarfile.TarInfo(name=m + ".bin")
info.size = len(fp.getbuffer())
tf.addfile(tarinfo=info, fileobj=fp)
def merge_fip(fip_path, inputs, output_path):
logging.debug("merge_fip: %s", fip_path)
fip = FIP(fip_path)
fip.load()
for name in FIP.ENTRY_NAMES:
binary = inputs.get(name)
if not binary:
continue
logging.debug("merge %s", name)
ent = fip.ents.get(name)
if ent:
ent.content = binary
else:
ent = Entry.make(getattr(FIP_UUID, "UUID_" + name), binary)
fip.ents[name] = ent
binary = inputs.get("BLP_DDRC")
if binary:
fip.blp_ddrc_binary = binary
if not output_path:
fn = os.path.splitext(fip_path)
fn = "%s_%s%s" % (fn[0], "merged", fn[1])
output_path = fn
fip.make_fip(output_path)
def round_up(n, k):
return (n + k - 1) // k * k
def read_blp_and_ddrc(inputs, blp_path, ddrc_path):
logging.info("Open %s and %s", blp_path, ddrc_path)
with open(blp_path, "rb") as fp:
blp_bin = fp.read()
logging.info("Open %s", ddrc_path)
with open(ddrc_path, "rb") as fp:
ddrc_bin = fp.read()
blp_bin += b"\0" * (round_up(len(blp_bin), FIP_ALIGN_SIZE) - len(blp_bin))
ddrc_bin += b"\0" * (round_up(len(ddrc_bin), FIP_ALIGN_SIZE) - len(ddrc_bin))
inputs["BLP_DDRC"] = blp_bin + ddrc_bin
def read_bld_tar(inputs, bld_tar_path, multibin):
logging.info("Open %s multibin=%r", bld_tar_path, multibin)
members = [
"BLD_CONTENT_CERT.bin",
"BLD2_KEY_CERT.bin",
"BLD1_KEY_CERT.bin",
"CV_DDRINIT.bin" if multibin else "BLD.bin",
]
with tarfile.open(bld_tar_path, "r") as tf:
for member in members:
try:
fp = tf.extractfile(member)
inputs[os.path.splitext(member)[0]] = fp.read()
except KeyError:
logging.warning("%s does not exist", member)
def main():
parser = argparse.ArgumentParser(description="FIP packer")
for name in FIP.ENTRY_NAMES:
parser.add_argument(
"--add-%s" % name.lower(),
dest=name,
type=str,
help="Merge %s into FIP" % name,
)
parser.add_argument(
"--add-blp-ddrc", dest="BLP_DDRC", type=str, help="Merge BLP+DDRC into FIP"
)
parser.add_argument("--add-blp", dest="BLP", type=str, help="Merge BLP into FIP")
parser.add_argument("--add-ddrc", dest="DDRC", type=str, help="Merge DDRC into FIP")
parser.add_argument(
"--add-bld-tar", dest="BLD_TAR", type=str, help="Merge BLD.tar into FIP"
)
parser.add_argument("--multibin", action="store_true", help="Use multibin")
parser.add_argument("FIP_BIN", type=str, nargs=1, help="Input FIP binary")
parser.add_argument("--output", type=str, help="Output filename")
parser.add_argument(
"--version", action="store_true", help="Output version information and exit"
)
parser.add_argument(
"--verbose",
help="Increase output verbosity",
action="store_const",
const=logging.DEBUG,
default=logging.DEBUG,
)
parser.add_argument("--unpack", action="store_true", help="Unpack FIP.bin")
parser.add_argument("--parse", action="store_true", help="Parse FIP.bin")
parser.add_argument(
"--tar-bld", action="store_true", help="Extrace BLD.bin and tar"
)
if argcomplete:
argcomplete.autocomplete(parser)
args = parser.parse_args()
init_logging(stdout_level=args.verbose)
logging.debug("args=%r", args)
FIP_UUID.cls_init()
if args.parse:
parse_fip(args.FIP_BIN[0])
if args.unpack:
unpack_fip(args.FIP_BIN[0])
if args.tar_bld:
tar_bld(args.FIP_BIN[0], args.output, args.multibin)
inputs = collections.OrderedDict()
for name in list(FIP.ENTRY_NAMES) + ["BLP_DDRC"]:
fn = getattr(args, name)
if not fn:
continue
logging.info("Open %s", fn)
with open(fn, "rb") as fp:
inputs[name] = fp.read()
if args.BLP or args.DDRC:
read_blp_and_ddrc(inputs, args.BLP, args.DDRC)
if args.BLD_TAR:
read_bld_tar(inputs, args.BLD_TAR, args.multibin)
if len(inputs):
merge_fip(args.FIP_BIN[0], inputs, args.output)
if __name__ == "__main__":
main()
+10
View File
@@ -7,10 +7,12 @@
import config
import os
import subprocess
import _thread
from logger_manager import logger_manager
from maix import time as maix_time
_INA226_PRESENT = None
_INA226_LOCK = _thread.allocate_lock()
def _ina226_ready() -> bool:
@@ -32,7 +34,11 @@ def write_register(reg, value):
data = [(value >> 8) & 0xFF, value & 0xFF]
# 某些底层驱动在失败时只打印 “write failed” 并返回 -1,而不是抛异常;
# 为避免误判“初始化成功”导致后续 readfrom_mem SIGSEGV,这里把失败显式转成异常。
_INA226_LOCK.acquire()
try:
ret = hardware_manager.bus.writeto_mem(config.INA226_ADDR, reg, bytes(data))
finally:
_INA226_LOCK.release()
if isinstance(ret, int) and ret < 0:
if logger:
logger.error(f"[INA226] writeto_mem 失败: addr=0x{config.INA226_ADDR:02X} reg=0x{reg:02X} ret={ret}")
@@ -42,7 +48,11 @@ def write_register(reg, value):
def read_register(reg):
"""读取INA226寄存器"""
from hardware import hardware_manager
_INA226_LOCK.acquire()
try:
data = hardware_manager.bus.readfrom_mem(config.INA226_ADDR, reg, 2)
finally:
_INA226_LOCK.release()
return (data[0] << 8) | data[1]
+9 -46
View File
@@ -8,12 +8,7 @@ from laser_manager import laser_manager
from logger_manager import logger_manager
from network import network_manager
from triangle_target import load_camera_from_xml, load_triangle_positions, try_triangle_scoring
from vision import (
estimate_distance,
detect_circle_v3,
enqueue_save_shot,
enqueue_save_raw_shot,
)
from vision import estimate_distance, detect_circle_v3, enqueue_save_shot
from maix import image, time
# 缓存相机标定与三角形位置,避免每次射箭重复读磁盘
@@ -59,7 +54,6 @@ def analyze_shot(frame, laser_point=None):
"""
logger = logger_manager.logger
from datetime import datetime
yellow_algorithm_ms = 0.0
# ── Step 1: 确定激光点 ────────────────────────────────────────────────────
laser_point_method = None
@@ -75,11 +69,7 @@ def analyze_shot(frame, laser_point=None):
logger.info(f"[算法] 使用校准值: {laser_manager.laser_point}")
else:
# 动态模式:先做一次无激光点检测以估算距离,再推算激光点
_t_yellow = time_std.perf_counter()
try:
_, _, _, _, best_radius1_temp, _ = detect_circle_v3(frame, None)
finally:
yellow_algorithm_ms += (time_std.perf_counter() - _t_yellow) * 1000.0
distance_m_first = estimate_distance(best_radius1_temp) if best_radius1_temp else None
if distance_m_first and distance_m_first > 0:
laser_point = laser_manager.calculate_laser_point_from_distance(distance_m_first)
@@ -124,7 +114,6 @@ def analyze_shot(frame, laser_point=None):
"laser_point": laser_point, "laser_point_method": laser_point_method,
"offset_method": "yellow_ellipse" if ellipse_params else "yellow_circle",
"distance_method": "yellow_radius",
"yellow_algorithm_ms": float(yellow_algorithm_ms),
}
if yolo_roi_xyxy is not None:
out["yolo_roi_xyxy"] = yolo_roi_xyxy
@@ -132,12 +121,9 @@ def analyze_shot(frame, laser_point=None):
if not use_tri:
# 三角形未配置,直接跑圆形检测
_t_yellow = time_std.perf_counter()
try:
cdata = detect_circle_v3(frame, laser_point, img_cv=img_cv)
finally:
yellow_algorithm_ms += (time_std.perf_counter() - _t_yellow) * 1000.0
return _build_circle_result(cdata)
return _build_circle_result(
detect_circle_v3(frame, laser_point, img_cv=img_cv)
)
# ── Step 4: 先独占跑三角形,超时或失败后再跑圆形(不与圆心并行,避免抢 CPU)──
roi_xyxy = None
@@ -290,7 +276,6 @@ def analyze_shot(frame, laser_point=None):
"laser_point": laser_point, "laser_point_method": laser_point_method,
"offset_method": tri.get("offset_method") or "triangle_homography",
"distance_method": tri.get("distance_method") or "pnp_triangle",
"yellow_algorithm_ms": float(yellow_algorithm_ms),
"tri_markers": tri.get("markers", []),
"tri_markers_completed": tri.get("markers_completed", []),
"tri_homography": tri.get("homography"),
@@ -310,12 +295,8 @@ def analyze_shot(frame, laser_point=None):
logger.warning(f"[TRI] 超时 {tri_timeout_s:.2f}s 仍未结束,启动圆心算法(三角形仍在后台)")
# 三角形超时或失败 → 跑圆心;圆心跑完后再检查三角形是否已结束
try:
_t_yellow = time_std.perf_counter()
try:
cdata = detect_circle_v3(frame, laser_point, img_cv=img_cv)
finally:
yellow_algorithm_ms += (time_std.perf_counter() - _t_yellow) * 1000.0
except Exception as e:
logger.error(f"[CIRCLE] 圆形检测异常: {e}")
cdata = (frame, None, None, None, None, None)
@@ -341,25 +322,15 @@ def process_shot(adc_val):
try:
frame = camera_manager.read_frame()
# Copy the untouched frame before any detection or drawing.
from shot_id_generator import shot_id_generator
shot_id = shot_id_generator.generate_id()
enqueue_save_raw_shot(frame, shot_id)
# 网络事件移到拍照之后,避免阻塞拍照
network_manager.safe_enqueue({"shoot_event": "start"}, msg_type=2, high=True)
# Classify only the current shot frame; never reuse a previous result.
target_class_result = None
yolo_target_ms = 0.0
try:
from target_roi_yolo import try_get_target_class_from_yolo
_t_yolo_target = time_std.perf_counter()
try:
target_class_result = try_get_target_class_from_yolo(frame, logger=logger)
finally:
yolo_target_ms = (time_std.perf_counter() - _t_yolo_target) * 1000.0
if logger:
logger.info(f"[YOLO-TARGET] 当前箭业务结果: {target_class_result}")
except Exception as exc:
@@ -389,7 +360,6 @@ def process_shot(adc_val):
laser_point_method = analysis_result["laser_point_method"]
offset_method = analysis_result.get("offset_method", "yellow_circle")
distance_method = analysis_result.get("distance_method", "yellow_radius")
yellow_algorithm_ms = float(analysis_result.get("yellow_algorithm_ms", 0.0) or 0.0)
tri_markers = analysis_result.get("tri_markers", [])
tri_markers_completed = analysis_result.get("tri_markers_completed", [])
tri_homography = analysis_result.get("tri_homography")
@@ -410,6 +380,10 @@ def process_shot(adc_val):
if dx is None and dy is None and logger:
logger.warning("[MAIN] 未检测到偏移量(三角形与圆形均失败),但会保存图像")
# 生成射箭ID
from shot_id_generator import shot_id_generator
shot_id = shot_id_generator.generate_id()
if logger:
logger.info(f"[MAIN] 射箭ID: {shot_id}")
@@ -439,9 +413,7 @@ def process_shot(adc_val):
"r": 20.0, # 保留字段(服务端当前忽略,物理外环半径 cm)
"target_class": target_label,
"target_class_confidence": (
round(float(target_confidence), 2)
if target_confidence is not None
else None
float(target_confidence) if target_confidence is not None else None
),
"d": round((distance_m or 0.0) * 100),
"d_laser": round((laser_distance_m or 0.0) * 100),
@@ -453,8 +425,6 @@ def process_shot(adc_val):
"target_y": float(y),
"offset_method": offset_method,
"distance_method": distance_method,
"yellow_algorithm_ms": round(yellow_algorithm_ms, 2),
"yolo_target_ms": round(float(yolo_target_ms), 2),
}
if ellipse_params:
@@ -471,13 +441,6 @@ def process_shot(adc_val):
inner_data["ellipse_center_x"] = None
inner_data["ellipse_center_y"] = None
# 记录这组 inner_data 即将进入上报队列的本地时间,精确到毫秒。
upload_time_ms = int(time_std.time() * 1000)
upload_time_sec, upload_time_millis = divmod(upload_time_ms, 1000)
inner_data["upload_time"] = (
time_std.strftime("%Y-%m-%d %H:%M:%S", time_std.localtime(upload_time_sec))
+ f".{upload_time_millis:03d}"
)
report_data = {"cmd": 1, "data": inner_data}
if logger:
logger.info(
+39 -226
View File
@@ -126,19 +126,10 @@ def _get_detector(model_path: str):
return _detector_by_path[model_path]
try:
from maix import nn
except Exception:
except ImportError:
return None
# YOLO is an optional capability. A broken/incompatible model must not
# abort boot (especially before the OTA rollback check).
try:
detector = nn.YOLOv5(model=model_path, dual_buff=False)
except Exception:
# Cache the failure to avoid retrying a broken native load every frame.
# reset_yolo_detector_cache() clears this after a model replacement.
_detector_by_path[model_path] = None
return None
_detector_by_path[model_path] = detector
return detector
_detector_by_path[model_path] = nn.YOLOv5(model=model_path, dual_buff=False)
return _detector_by_path[model_path]
def preload_yolo_detector(logger=None):
@@ -310,10 +301,7 @@ def _det_obj_score(o):
def try_get_target_class_from_yolo(maix_frame, logger=None):
"""调用 YOLO 检测:class_id∈IGNORE_CLASS_IDS(默认 1) 的框在解析时直接过滤;
detections 输出其余全部框(不按置信度过滤);
上报以「检出框裁剪放大后的复检结果」为主,复检不可用时用全图结果;
有效靶规格(20/40)中取置信度最高的一条,且低于 MIN_REPORT_CONF 不上报类别。"""
"""Classify the current target as 20cm or 40cm; return None if unknown."""
try:
import config as cfg
except Exception:
@@ -336,239 +324,64 @@ def try_get_target_class_from_yolo(maix_frame, logger=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))
# class_id → 靶规格标签,下标即 class_id:0→40, 1→0, 2→20
labels = getattr(cfg, "TARGET_CLASS_YOLO_LABELS", (40, 0, 20))
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)
# 只有映射到这些标签的检测才算有效靶规格(class 1→label 0 直接忽略)
valid_labels = getattr(cfg, "TARGET_CLASS_YOLO_VALID_LABELS", (20, 40))
if isinstance(valid_labels, str):
valid_labels = tuple(
int(float(x.strip())) for x in valid_labels.split(",") if x.strip()
)
valid_labels = frozenset(int(float(v)) for v in valid_labels)
# 解析阶段就丢弃这些 class_id(1=靶心),不进入 detections,也不参与上报选择
ignore_class_ids = getattr(cfg, "TARGET_CLASS_YOLO_IGNORE_CLASS_IDS", (1,))
if isinstance(ignore_class_ids, int):
ignore_class_ids = (ignore_class_ids,)
if isinstance(ignore_class_ids, str):
ignore_class_ids = tuple(
int(float(x.strip())) for x in ignore_class_ids.split(",") if x.strip()
)
ignore_class_ids = frozenset(int(float(v)) for v in ignore_class_ids)
n_ignored = [0]
def _run(threshold):
def _detect(threshold):
try:
return detector.detect(maix_frame, conf_th=threshold, iou_th=iou_th)
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 None
return []
return _normalize_objs(raw if raw is not None else [])
def _obj_box(o):
try:
return (
round(float(o.x), 1),
round(float(o.y), 1),
round(float(o.w), 1),
round(float(o.h), 1),
)
except (TypeError, ValueError, AttributeError):
return None
def _parse(raw_result):
out = []
n_ignored[0] = 0
for obj in _normalize_objs(raw_result if raw_result 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 in ignore_class_ids:
n_ignored[0] += 1
if class_id is None or class_id < 0 or class_id >= len(labels):
continue
label = None
if 0 <= class_id < len(labels):
try:
label = int(float(labels[class_id]))
except (TypeError, ValueError):
label = None
out.append(
{
"class_id": class_id,
"label": label,
"confidence": round(_det_obj_score(obj), 4),
"box_xywh": _obj_box(obj),
}
)
return out
continue
if label in (20, 40):
found.append((label, class_id, _det_obj_score(obj)))
return found
# 第 1 次检测
raw = _run(conf_th)
detections = _parse(raw)
# 0 结果时重新检测一次(阈值取更小值,仍为 0 则同参数重跑)
if not detections:
retry_th = float(getattr(cfg, "TARGET_CLASS_YOLO_RETRY_CONF_TH", 0.25) or 0)
if not (0 < retry_th < conf_th):
retry_th = conf_th
if logger:
logger.info(f"[YOLO-TARGET] 0 结果,重新检测 conf_th={retry_th}")
raw_retry = _run(retry_th)
detections_retry = _parse(raw_retry)
if detections_retry:
detections = detections_retry
if logger:
objects = _detect(conf_th)
candidates = _candidates(objects)
if logger and objects:
logger.info(
f"[YOLO-TARGET] 检测结果={detections}"
f"(已忽略 class_id∈{tuple(sorted(ignore_class_ids))} 共{n_ignored[0]} 条)"
"[YOLO-TARGET] 原始框=%d, 解析类别=%s"
% (
len(objects),
[(_det_obj_class_id(o), _det_obj_score(o)) for o in objects[:8]],
)
if not detections:
if logger:
logger.warning("[YOLO-TARGET] 未检测到目标(已重检 1 次)")
return None
def _best_valid(dets):
valid = [d for d in dets if d["label"] in valid_labels]
if not valid:
return None
return max(valid, key=lambda d: d["confidence"])
def _crop_target(box, margin_frac):
"""按检出框(带 margin)裁出放大 ROI;返回 Maix image,失败返回 None。"""
if not box or len(box) != 4:
return None
x, y, w, h = [float(v) for v in box]
try:
fw, fh = int(maix_frame.width()), int(maix_frame.height())
except Exception:
fw = fh = 0
mx, my = w * margin_frac, h * margin_frac
x0, y0 = int(max(0, x - mx)), int(max(0, y - my))
x1 = int(min(fw, x + w + mx)) if fw else int(x + w + mx)
y1 = int(min(fh, y + h + my)) if fh else int(y + h + my)
if (x1 - x0) < 32 or (y1 - y0) < 32:
if logger:
logger.warning(
f"[YOLO-TARGET] 复检裁剪过小 [{x0},{y0},{x1},{y1}]"
)
return None
try:
cropped = maix_frame.crop(x0, y0, x1 - x0, y1 - y0)
if cropped is not None:
return cropped
except Exception:
pass
try:
from maix import image as maix_image
arr = maix_frame.to_numpy()
slab = np.ascontiguousarray(arr[y0:y1, x0:x1], dtype=np.uint8)
if slab.size == 0:
return None
return maix_image.cv2image(slab, False, True)
except Exception as exc:
if logger:
logger.warning(f"[YOLO-TARGET] 复检裁剪失败: {exc}")
return None
# 上报口径:只在有效靶规格(20/40)里取置信度最高的一条,class_id=1 已在解析阶段过滤
full_best = _best_valid(detections)
# 二次裁剪复检:把全图检出的靶框裁剪放大后再 detect 一次,与全图结果互相印证
# 复检用更低阈值 RECHECK_CONF_TH,置信度大于该值即接受
recheck_th = float(getattr(cfg, "TARGET_CLASS_YOLO_RECHECK_CONF_TH", 0.25))
crop_best = None
recheck_status = "off"
if full_best is not None and bool(
getattr(cfg, "TARGET_CLASS_YOLO_RECHECK_CROP", True)
if not candidates and bool(
getattr(cfg, "TARGET_CLASS_YOLO_RETRY_ON_EMPTY", False)
):
recheck_status = "fail"
margin_frac = float(
getattr(cfg, "TARGET_CLASS_YOLO_RECHECK_MARGIN_FRAC", 0.1)
)
crop_img = _crop_target(full_best["box_xywh"], margin_frac)
if crop_img is not None:
try:
raw_crop = detector.detect(
crop_img, conf_th=recheck_th, iou_th=iou_th
)
except Exception as exc:
raw_crop = None
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(f"[YOLO-TARGET] 复检 detect 异常: {exc}")
if raw_crop is not None:
crop_dets = _parse(raw_crop)
recheck_status = "ok"
crop_best = _best_valid(crop_dets)
if crop_best is not None and crop_best["confidence"] <= recheck_th:
crop_best = None
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] 复检(conf_th={recheck_th}, "
f"裁剪框={full_best['box_xywh']}) 结果={crop_dets}"
f"[YOLO-TARGET] 当前帧分类={label}, class_id={class_id}, "
f"conf={confidence:.3f}"
)
# 合并:以复检(裁剪放大)结果为主;复检无结果才用全图
picked = full_best
picked_from_crop = False
note = ""
if recheck_status == "ok":
if crop_best is None:
note = f"复检无 conf>{recheck_th} 的有效框,沿用全图"
else:
picked = crop_best
picked_from_crop = True
if crop_best["label"] == full_best["label"]:
note = f"复检一致({crop_best['label']}),以复检为准"
else:
note = (
f"复检与全图不一致(全图={full_best['label']}@{full_best['confidence']}, "
f"裁剪={crop_best['label']}@{crop_best['confidence']}),以复检为准"
)
# 置信度门槛:低于阈值不上报类别,避免硬猜(复检结果只需 > RECHECK_CONF_TH)
if picked is not None:
if picked_from_crop:
min_report_conf = recheck_th
below = picked["confidence"] <= min_report_conf
else:
min_report_conf = float(
getattr(cfg, "TARGET_CLASS_YOLO_MIN_REPORT_CONF", 0.5)
)
below = picked["confidence"] < min_report_conf
if below:
note = (note + "; " if note else "") + (
f"conf={picked['confidence']} 未达门槛{min_report_conf},置信度不足"
)
picked = None
if picked is None:
label, class_id, confidence = None, None, None
else:
label = picked["label"]
class_id = picked["class_id"]
confidence = picked["confidence"]
if logger:
logger.info(
f"[YOLO-TARGET] 上报={label}, class_id={class_id}, conf={confidence}, "
f"全图候选={[(d['label'], d['confidence']) for d in detections if d['label'] in valid_labels]}, "
f"复检={recheck_status} {note}"
)
if note:
logger.warning(f"[YOLO-TARGET] {note}")
return {
"label": label,
"class_id": class_id,
"confidence": confidence,
"detections": detections,
}
return result
def _det_to_src_xyxy(o, coord_mode: str, src_w: int, src_h: int, net_w: int, net_h: int):
Binary file not shown.
+27
View File
@@ -0,0 +1,27 @@
"""Run independently and keep A24 at a high logic level."""
from maix import app, gpio, pinmap, time
PIN = "A17"
GPIO_NAME = "GPIOA17"
def main():
pinmap.set_pin_function(PIN, GPIO_NAME)
output = gpio.GPIO(GPIO_NAME, gpio.Mode.OUT)
output.value(1)
print(f"{PIN} is HIGH. Stop the script to set it LOW.")
try:
while not app.need_exit():
# Refresh the output in case another component changes its state.
output.value(1)
time.sleep_ms(100)
finally:
output.value(0)
print(f"{PIN} is LOW.")
if __name__ == "__main__":
main()
Binary file not shown.
-135
View File
@@ -1,135 +0,0 @@
#!/usr/bin/env python3
# test_bluetooth.py
# 参考: https://wiki.sipeed.com/maixpy/doc/zh/modules/bluetooth.html
#
# 依赖:
# 1. 使能蓝牙: bluetoothctl power on
# (可选开机自启: echo "bluetoothctl power on" >> /etc/rc.local)
# 2. 安装依赖: pip install bleak
import asyncio
import sys
from collections import namedtuple
try:
from bleak import BleakScanner, BleakClient
except ImportError:
print("ModuleNotFoundError: No module named 'bleak'")
print("请先在设备上安装依赖:")
print(" pip install bleak")
print("并使能蓝牙:")
print(" bluetoothctl power on")
sys.exit(1)
MODEL_NBR_UUID = "1A2A"
BLEDev = namedtuple("BLEDev", ["address", "name"])
async def scan_devices(timeout=5.0):
"""扫描周围的蓝牙设备"""
print("=" * 60)
print("Scanning BLE devices...")
print("=" * 60)
found = {}
def _cb(device, adv):
found[device.address] = adv.local_name or device.name
try:
await BleakScanner.discover(timeout=timeout, detection_callback=_cb)
except Exception as e:
print(f"Scan failed: {e}")
print("Possible causes:")
print(" 1. Bluetooth not powered on: bluetoothctl power on")
print(" 2. bleak not installed: pip install bleak")
print(" 3. BLE hardware/driver not available")
return []
if not found:
print("No devices found.")
return []
devices = []
for address, name in found.items():
devices.append(BLEDev(address, name))
print(f" {address} {name}")
return devices
async def connect_device(address, timeout=20.0, retries=3):
"""连接蓝牙设备并尝试读取特征值"""
print("=" * 60)
print(f"Connecting to {address} ...")
print("=" * 60)
for attempt in range(1, retries + 1):
try:
async with BleakClient(address, timeout=timeout) as client:
print(f"Connected: {client.is_connected}")
# 遍历服务和特征值
for service in client.services:
print(f" Service: {service.uuid}")
for char in service.characteristics:
print(f" Char: {char.uuid} props={list(char.properties)}")
# 尝试读取 Model Number 特征
try:
value = await client.read_gatt_char(MODEL_NBR_UUID)
print(f"Model Number: {value.decode(errors='replace')}")
except Exception as e:
print(f"Read {MODEL_NBR_UUID} failed (device may not have it): {e}")
return True
except Exception as e:
print(f"[attempt {attempt}/{retries}] Connect failed: {e}")
if attempt < retries:
await asyncio.sleep(2)
print("\nAll attempts failed. 请检查:")
print(" 1. forrily 是否已连到手机/其它主机(BLE 外设通常只允许一连接),")
print(" 先在手机上断开它")
print(" 2. 清除 BlueZ 缓存后重试:")
print(f" bluetoothctl remove {address}")
print(" systemctl restart bluetooth")
print(" bluetoothctl power on")
print(" 3. 升级依赖:")
print(" pip install -U bleak dbus-fast")
return False
async def main():
target_name = "forrily"
devices = await scan_devices(timeout=5.0)
if not devices:
return
if len(sys.argv) > 1:
address = sys.argv[1]
else:
# 按名字查找目标设备,不连第一个
target = None
for d in devices:
if d.name and d.name.strip().lower() == target_name.lower():
target = d
break
if target is None:
for d in devices:
if d.name and target_name.lower() in d.name.strip().lower():
target = d
break
if target is None:
print(f"\nDevice named '{target_name}' not found!")
print("Scanned devices are listed above.")
return
address = target.address
print(f"\nFound '{target_name}': {address}")
await connect_device(address)
if __name__ == "__main__":
asyncio.run(main())
-239
View File
@@ -1,239 +0,0 @@
#!/usr/bin/env python3
# test_bluetooth_peripheral.py
# 设备(MaixCAM)作为 BLE 从机(Peripheral/GATT Server)广播给手机连接。
# 参考: https://wiki.sipeed.com/maixpy/doc/zh/modules/bluetooth.html
#
# 用法:
# python test_bluetooth_peripheral.py [广播名字] # 默认 shoot
#
# 依赖:
# 1. 使能蓝牙: bluetoothctl power on
# 2. 安装依赖: pip install bless
# 3. 部分 BlueZ 需要 experimental 模式才支持 GATT Server:
# 编辑 /etc/bluetooth/main.conf 保证有 Experimental = true
# 然后 systemctl restart bluetooth && bluetoothctl power on
#
# 手机端: 用 nRF Connect / 任意 BLE 调试 App 搜索广播名连接,
# 读写 characteristic 并订阅 notify,每 2 秒会收到计数通知。
#
# 实现:
# - GATT 服务由 bless 注册到 BlueZ
# - 广播由常驻 bluetoothctl 子进程控制 (menu advertise / name / advertise on),
# 这是已验证手机能搜到的路径; 若 bluetoothctl 不可用则回退到 btmgmt adv on
import asyncio
import shutil
import subprocess
import sys
import time
try:
from bless import (
BlessServer,
GATTCharacteristicProperties,
GATTAttributePermissions,
)
except ImportError:
print("ModuleNotFoundError: No module named 'bless'")
print("请先在设备上安装依赖:")
print(" pip install bless")
print("并使能蓝牙:")
print(" bluetoothctl power on")
raise SystemExit(1)
DEVICE_NAME = sys.argv[1] if len(sys.argv) > 1 else "shoot"
SERVICE_UUID = "A07498CA-AD5B-474E-940D-16F1FBE7E8CD"
CHAR_UUID = "51FF12BB-3ED8-46E5-B4F9-D64E2FEC021B"
BTCTL_LOG = "/tmp/btctl_adv.log"
# 不同 bless 版本枚举名不同: writeable / writable
try:
WRITE_PERMISSION = GATTAttributePermissions.writeable
except AttributeError:
WRITE_PERMISSION = GATTAttributePermissions.writable
_btctl = None
def on_read(characteristic, request):
print(f"[GATT] read {characteristic.uuid}")
return characteristic.value
def on_write(characteristic, value, request):
characteristic.value = value
print(f"[GATT] write {characteristic.uuid} = {bytes(value)}")
def on_subscribe(characteristic, session):
print(f"[GATT] client subscribed: {characteristic.uuid}")
def on_unsubscribe(characteristic, session):
print(f"[GATT] client unsubscribed: {characteristic.uuid}")
def _run(cmd):
try:
r = subprocess.run(cmd, capture_output=True, text=True, timeout=10)
out = ((r.stdout or "") + (r.stderr or "")).strip()
if out:
print(f"[adv] {' '.join(cmd)} -> {out}")
return r.returncode == 0
except FileNotFoundError:
print(f"[adv] {cmd[0]} not found")
return False
except Exception as e:
print(f"[adv] {' '.join(cmd)} failed: {e}")
return False
def adv_start(name):
"""启动广播: 优先 bluetoothctl (常驻子进程), 失败则回退 btmgmt"""
global _btctl
if shutil.which("bluetoothctl") is not None:
try:
log = open(BTCTL_LOG, "w")
_btctl = subprocess.Popen(
["bluetoothctl"],
stdin=subprocess.PIPE,
stdout=log,
stderr=subprocess.STDOUT,
text=True,
bufsize=1,
)
except Exception as e:
print(f"[adv] start bluetoothctl failed: {e}")
_btctl = None
if _btctl is not None:
for cmd in (
"menu advertise",
f"name {name}",
"discoverable on",
"menu main",
"advertise on",
):
try:
_btctl.stdin.write(cmd + "\n")
_btctl.stdin.flush()
except Exception as e:
print(f"[adv] write '{cmd}' failed: {e}")
break
time.sleep(0.3)
time.sleep(1.0)
if _btctl.poll() is None:
print(f"[adv] bluetoothctl advertising on (log: {BTCTL_LOG})")
return True
print("[adv] bluetoothctl exited early, fallback to btmgmt")
# 回退: mgmt 传统广播
if shutil.which("btmgmt") is None:
print("[adv] bluetoothctl/btmgmt 均不可用")
return False
_run(["btmgmt", "le", "on"])
_run(["btmgmt", "connectable", "on"])
return _run(["btmgmt", "adv", "on"])
def adv_stop():
global _btctl
if _btctl is not None:
try:
_btctl.stdin.write("advertise off\n")
_btctl.stdin.flush()
time.sleep(0.3)
_btctl.terminate()
except Exception:
pass
_btctl = None
elif shutil.which("btmgmt") is not None:
_run(["btmgmt", "adv", "off"])
async def run():
gatt = {
SERVICE_UUID: {
CHAR_UUID: {
"Properties": (
GATTCharacteristicProperties.read
| GATTCharacteristicProperties.write
| GATTCharacteristicProperties.notify
),
"Permissions": (
GATTAttributePermissions.readable | WRITE_PERMISSION
),
"Value": bytearray(b"hello from maixcam"),
}
}
}
server = BlessServer(
name=DEVICE_NAME,
on_read=on_read,
on_write=on_write,
on_subscribe=on_subscribe,
on_unsubscribe=on_unsubscribe,
)
print("Adding GATT ...")
await server.add_gatt(gatt)
print(f"Starting BLE peripheral, name = '{DEVICE_NAME}' ...")
try:
await server.start()
except Exception as e:
print(f"start() failed: {e}")
print("排查:")
print(" 1. 蓝牙是否上电: bluetoothctl power on (bluetoothctl show 看 Powered)")
print(" 2. /etc/bluetooth/main.conf 加 Experimental = true 后重启:")
print(" systemctl restart bluetooth && bluetoothctl power on")
print(" 3. 依赖: pip install -U bless dbus-next bleak")
return
adv = await server.is_advertising()
print("=" * 60)
print(f"'{DEVICE_NAME}' advertising status: {adv}")
print("Starting advertising ...")
if not adv_start(DEVICE_NAME):
print("广播启动失败,请手动执行: bluetoothctl -> advertise on")
print("手机用 nRF Connect 等 App 搜索并连接,")
print(f"服务 {SERVICE_UUID}")
print(f"特征 {CHAR_UUID} (read/write/notify)")
print("Ctrl+C 退出")
print("=" * 60)
try:
# 每 2 秒向订阅者推送一次计数
counter = 0
while True:
counter += 1
try:
char = server.get_characteristic(CHAR_UUID)
if char is not None:
char.value = bytearray(str(counter).encode())
server.update_value(SERVICE_UUID, CHAR_UUID)
except Exception as e:
# 没有客户端订阅时可能失败,忽略
print(f"notify failed: {e}")
if counter % 10 == 0:
try:
print(f"[status] advertising={await server.is_advertising()} "
f"connected={await server.is_connected()}")
except Exception:
pass
await asyncio.sleep(2)
finally:
adv_stop()
if __name__ == "__main__":
try:
asyncio.run(run())
except KeyboardInterrupt:
print("\nStopped.")
+62
View File
@@ -0,0 +1,62 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""Interactive GPIO test for physical pin A14."""
import sys
from maix import gpio, pinmap
PIN = "A14"
GPIO_NAME = "GPIOA14"
def set_level(output, command):
if command == "1":
output.value(1)
print("A14 = HIGH, laser OFF")
return True
if command == "0":
output.value(0)
print("A14 = LOW, laser ON")
return True
return False
def main():
pinmap.set_pin_function(PIN, GPIO_NAME)
output = gpio.GPIO(GPIO_NAME, gpio.Mode.OUT)
# One-shot mode for SSH/serial shells: python3 test_gpio_a14.py 1|0
if len(sys.argv) > 1:
command = sys.argv[1].strip()
if not set_level(output, command):
print("Invalid argument. Use 1 or 0.")
return
return
output.value(1)
print("A14 laser test: input 0 for ON, 1 for OFF, q to quit.")
try:
while True:
try:
command = input("A14> ").strip().lower()
except EOFError:
print("This runner has no stdin. Run from an SSH/serial shell with argument 1 or 0.")
return
if set_level(output, command):
continue
elif command in ("q", "quit", "exit"):
break
elif command:
print("Invalid input. Use 1, 0, or q.")
except KeyboardInterrupt:
print()
finally:
output.value(1)
print("A14 = HIGH, laser OFF, test stopped.")
if __name__ == "__main__":
main()
-108
View File
@@ -1,108 +0,0 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""Run from MaixVision on PC to inspect the box's live 20/40 YOLO output."""
import os
from maix import app, camera, display, image, nn, time
# This file is sent to /tmp/maixpy_run by MaixVision. Keep the model path
# absolute so the script uses the model already installed on the box.
MODEL_PATH = "/maixapp/apps/t11/model_317181.mud"
CAMERA_WIDTH = 640
CAMERA_HEIGHT = 480
CONF_TH = 0.65
IOU_TH = 0.45
def _flatten_objects(raw):
if raw is None:
return []
if isinstance(raw, (list, tuple)):
result = []
for item in raw:
if isinstance(item, (list, tuple)):
result.extend(_flatten_objects(item))
else:
result.append(item)
return result
return [raw]
def main():
if not os.path.isfile(MODEL_PATH):
raise FileNotFoundError("model not found on box: " + MODEL_PATH)
detector = nn.YOLOv5(model=MODEL_PATH, dual_buff=False)
cam = camera.Camera(CAMERA_WIDTH, CAMERA_HEIGHT)
disp = display.Display()
labels = tuple(str(label) for label in detector.labels)
print("[YOLO] model:", MODEL_PATH)
print("[YOLO] labels:", labels)
print("[YOLO] conf=%.2f iou=%.2f" % (CONF_TH, IOU_TH))
fps = 0.0
frame_count = 0
last_log_ms = time.ticks_ms()
while not app.need_exit():
loop_start_ms = time.ticks_ms()
img = cam.read()
detect_start_ms = time.ticks_ms()
raw = detector.detect(img, conf_th=CONF_TH, iou_th=IOU_TH)
detect_ms = max(0, time.ticks_diff(time.ticks_ms(), detect_start_ms))
objects = _flatten_objects(raw)
candidates = []
for obj in objects:
class_id = int(obj.class_id)
score = float(obj.score)
label = labels[class_id] if 0 <= class_id < len(labels) else "unknown"
color = image.COLOR_GREEN if label in ("20", "40") else image.COLOR_RED
img.draw_rect(obj.x, obj.y, obj.w, obj.h, color=color)
img.draw_string(
obj.x,
max(0, obj.y - 16),
"%scm %.2f" % (label, score),
color=color,
)
if label in ("20", "40"):
candidates.append((score, label))
loop_ms = max(1, time.ticks_diff(time.ticks_ms(), loop_start_ms))
instant_fps = 1000.0 / float(loop_ms)
fps = instant_fps if frame_count == 0 else fps * 0.9 + instant_fps * 0.1
if candidates:
best_score, best_label = max(candidates, key=lambda item: item[0])
status = "TARGET %scm %.2f" % (best_label, best_score)
status_color = image.COLOR_GREEN
else:
status = "TARGET UNKNOWN"
status_color = image.COLOR_RED
img.draw_string(5, 5, status, color=status_color)
img.draw_string(
5,
25,
"infer=%dms fps=%.1f boxes=%d" % (detect_ms, fps, len(objects)),
color=image.COLOR_YELLOW,
)
disp.show(img)
frame_count += 1
now_ms = time.ticks_ms()
if time.ticks_diff(now_ms, last_log_ms) >= 1000:
print(
"[YOLO] %s infer=%dms fps=%.1f boxes=%d"
% (status, detect_ms, fps, len(objects))
)
last_log_ms = now_ms
if __name__ == "__main__":
main()
+122
View File
@@ -0,0 +1,122 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""Standalone WiFi/GPIO/INA226 isolation test for the official MaixPy tool.
This file intentionally does not import project modules or start project
threads. Select TEST_MODE below, then run the file directly.
"""
import time
from maix import gpio, i2c, network, pinmap
# Change only this value before each run.
# wifi WiFi only
# a25 A25 only
# a23 A23 only
# gpio A23/A26 only
# ina INA226 only
# gpio_ina GPIOs, then INA226
# a25_wifi A25, then WiFi
# a23_wifi A23, then WiFi
# all GPIOs, INA226, then WiFi
TEST_MODE = "a25_wifi"
WIFI_SSID = "sheling4b02-5G"
WIFI_PASSWORD = "Aa12345678"
WIFI_TIMEOUT_S = 20
I2C_BUS_NUM = 5
INA226_ADDR = 0x40
def init_leds():
return init_selected_leds(True, True)
def init_selected_leds(use_a26, use_a23):
outputs = []
if use_a26:
print("Initializing A25 -> GPIOA25")
pinmap.set_pin_function("A25", "GPIOA25")
green = gpio.GPIO("GPIOA25", gpio.Mode.OUT)
green.value(0)
outputs.append(("GPIOA25", green))
print("GPIOA25 initialized LOW")
if use_a23:
print("Initializing A23 -> GPIOA23")
pinmap.set_pin_function("A23", "GPIOA23")
red = gpio.GPIO("GPIOA23", gpio.Mode.OUT)
red.value(0)
outputs.append(("GPIOA23", red))
print("GPIOA23 initialized LOW")
return outputs
def test_ina226():
# Match the board mapping used by the application before opening I2C5.
pinmap.set_pin_function("A15", "I2C5_SCL")
pinmap.set_pin_function("A27", "I2C5_SDA")
print("A15/A27 configured for I2C5")
print("Initializing I2C bus", I2C_BUS_NUM)
bus = i2c.I2C(I2C_BUS_NUM, i2c.Mode.MASTER)
print("Reading INA226 at 0x%02X" % INA226_ADDR)
config = bus.readfrom_mem(INA226_ADDR, 0x00, 2)
voltage_raw = bus.readfrom_mem(INA226_ADDR, 0x02, 2)
voltage = ((voltage_raw[0] << 8) | voltage_raw[1]) * 1.25 / 1000
print("INA226 config=0x%02X%02X voltage=%.3fV" % (config[0], config[1], voltage))
return bus
def test_wifi():
print("Starting MaixPy WiFi connection...")
wifi = network.wifi.Wifi()
result = wifi.connect(WIFI_SSID, WIFI_PASSWORD, wait=True, timeout=WIFI_TIMEOUT_S)
print("WiFi connect result:", result)
print("WiFi connected:", wifi.is_connected())
try:
print("WiFi IP:", wifi.get_ip())
except Exception as exc:
print("WiFi status query failed:", exc)
def main():
valid = ("wifi", "a25", "a23", "gpio", "ina", "gpio_ina", "a25_wifi", "a23_wifi", "all")
mode = TEST_MODE.lower()
if mode not in valid:
print("TEST_MODE must be one of:", ", ".join(valid))
return 1
leds = []
try:
print("=== Standalone WiFi/GPIO/INA226 isolation ===")
print("mode:", mode)
if mode in ("a25", "a25_wifi"):
leds = init_selected_leds(True, False)
time.sleep(1)
elif mode in ("a23", "a23_wifi"):
leds = init_selected_leds(False, True)
time.sleep(1)
elif mode in ("gpio", "gpio_ina", "all"):
leds = init_leds()
time.sleep(1)
if mode in ("ina", "gpio_ina", "all"):
test_ina226()
time.sleep(1)
if mode in ("wifi", "a25_wifi", "a23_wifi", "all"):
test_wifi()
print("TEST COMPLETE")
return 0
except Exception as exc:
print("TEST FAILED:", repr(exc))
return 1
finally:
for name, led in leds:
try:
led.value(0)
print(name, "LOW")
except Exception as exc:
print(name, "cleanup failed:", exc)
main()
+88
View File
@@ -0,0 +1,88 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""Standalone WiFi/GPIO isolation test.
This script intentionally does not import any project module. It only tests
MaixPy WiFi startup with optional A23/A26 GPIO initialization.
"""
import time
from maix import gpio, network, pinmap
GREEN_PIN = "A26"
GREEN_GPIO = "GPIOA26"
RED_PIN = "A23"
RED_GPIO = "GPIOA23"
# Run this file directly from the official MaixPy tool.
# Change only TEST_MODE between runs: none -> a26 -> a23 -> both.
TEST_MODE = "none"
WIFI_SSID = "sheling4b02-5G"
WIFI_PASSWORD = "Aa12345678"
WIFI_TIMEOUT_S = 20
def init_gpio(mode):
outputs = []
if mode in ("a26", "both"):
pinmap.set_pin_function(GREEN_PIN, GREEN_GPIO)
green = gpio.GPIO(GREEN_GPIO, gpio.Mode.OUT)
green.value(1)
outputs.append((GREEN_GPIO, green))
print("GPIOA26 initialized HIGH")
if mode in ("a23", "both"):
pinmap.set_pin_function(RED_PIN, RED_GPIO)
red = gpio.GPIO(RED_GPIO, gpio.Mode.OUT)
red.value(1)
outputs.append((RED_GPIO, red))
print("GPIOA23 initialized HIGH")
return outputs
def connect_wifi(ssid, password, timeout_s):
print("Starting MaixPy WiFi connection...")
wifi = network.wifi.Wifi()
result = wifi.connect(ssid, password, wait=True, timeout=timeout_s)
print("WiFi connect result:", result)
try:
print("WiFi connected:", wifi.is_connected())
print("WiFi IP:", wifi.get_ip())
except Exception as exc:
print("WiFi status query failed:", exc)
return result
def main():
mode = TEST_MODE.lower()
if mode not in ("none", "a26", "a23", "both"):
print("TEST_MODE must be none, a26, a23, or both")
return 1
ssid = WIFI_SSID
password = WIFI_PASSWORD
timeout_s = WIFI_TIMEOUT_S
print("=== Standalone WiFi/GPIO isolation ===")
print("mode:", mode)
print("ssid:", ssid)
outputs = []
try:
outputs = init_gpio(mode)
time.sleep(1)
connect_wifi(ssid, password, timeout_s)
return 0
except Exception as exc:
print("TEST FAILED:", repr(exc))
return 1
finally:
for gpio_name, output in outputs:
try:
output.value(0)
print(gpio_name, "LOW")
except Exception as exc:
print(gpio_name, "cleanup failed:", exc)
if __name__ == "__main__":
raise SystemExit(main())
-184
View File
@@ -1,184 +0,0 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""Offline baseline for traditional target-paper detection.
Dataset format: sibling .txt files use YOLO boxes and classes.txt maps ids
(the supplied dataset uses 0=40, 1=20, 2=10). This intentionally simple
baseline uses grayscale segmentation and contour geometry; it is useful as a
reference before adding more specialized black-triangle grouping.
"""
from __future__ import annotations
import argparse
import csv
import glob
import itertools
import os
import cv2
import numpy as np
def detect_white_papers(image: np.ndarray) -> list[tuple[int, int, int, int]]:
gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
h, w = gray.shape[:2]
mask = cv2.inRange(gray, 120, 255)
mask = cv2.morphologyEx(mask, cv2.MORPH_CLOSE, np.ones((9, 9), np.uint8))
mask = cv2.morphologyEx(mask, cv2.MORPH_OPEN, np.ones((5, 5), np.uint8))
contours, _ = cv2.findContours(mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
out = []
for contour in contours:
x, y, bw, bh = cv2.boundingRect(contour)
area = float(bw * bh)
if area < 0.05 * w * h or min(bw, bh) < 80:
continue
fill = cv2.contourArea(contour) / max(area, 1.0)
aspect = bw / max(float(bh), 1.0)
if fill >= 0.45 and 0.4 <= aspect <= 2.5:
out.append((x, y, x + bw, y + bh))
return out
def detect_black_triangle_papers(image: np.ndarray):
"""Infer paper boxes from the four small black corner marks."""
gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
mask = cv2.inRange(gray, 0, 100)
mask = cv2.morphologyEx(mask, cv2.MORPH_OPEN, np.ones((2, 2), np.uint8))
contours, _ = cv2.findContours(mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
points = []
for contour in contours:
x, y, bw, bh = cv2.boundingRect(contour)
area = cv2.contourArea(contour)
vertices = cv2.approxPolyDP(contour, 0.08 * cv2.arcLength(contour, True), True)
if 60 <= area <= 400 and 8 <= bw <= 24 and 8 <= bh <= 24:
if 3 <= len(vertices) <= 5 and 0.5 <= bw / max(bh, 1) <= 2.0:
points.append((x + bw / 2.0, y + bh / 2.0))
candidates = []
for group in itertools.combinations(points, 4):
xs = sorted(p[0] for p in group)
ys = sorted(p[1] for p in group)
span_x, span_y = xs[-1] - xs[0], ys[-1] - ys[0]
if span_x < 50 or span_y < 50 or not 0.45 < span_x / span_y < 1.5:
continue
corners = ((xs[0], ys[0]), (xs[-1], ys[0]),
(xs[0], ys[-1]), (xs[-1], ys[-1]))
error = max(min(np.hypot(p[0] - c[0], p[1] - c[1]) for c in corners)
for p in group) / max(span_x, span_y)
if error > 0.22:
continue
ex, ey = 0.12 * span_x, 0.12 * span_y
candidates.append((xs[0] - ex, ys[0] - ey,
xs[-1] + ex, ys[-1] + ey, error))
# A colored target ring supplies an independent center check. Hough is
# deliberately low-cost here because it runs only on the already small
# candidate list's source frame.
hsv = cv2.cvtColor(image, cv2.COLOR_BGR2HSV)
color = cv2.inRange(hsv, (0, 70, 45), (179, 255, 255))
color = cv2.morphologyEx(color, cv2.MORPH_OPEN, np.ones((5, 5), np.uint8))
ring_centers = []
for contour in cv2.findContours(color, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)[0]:
area = cv2.contourArea(contour)
if area < 150:
continue
moments = cv2.moments(contour)
if moments["m00"]:
ring_centers.append((moments["m10"] / moments["m00"], moments["m01"] / moments["m00"]))
checked = []
for box in candidates:
if not ring_centers:
checked.append(box)
continue
x0, y0, x1, y1, err = box
inside = any(x0 - .15 * (x1 - x0) <= cx <= x1 + .15 * (x1 - x0)
and y0 - .15 * (y1 - y0) <= cy <= y1 + .15 * (y1 - y0)
for cx, cy in ring_centers)
if inside:
checked.append(box)
return sorted(checked, key=lambda x: x[-1])
def iou(a, b):
x0, y0 = max(a[0], b[0]), max(a[1], b[1])
x1, y1 = min(a[2], b[2]), min(a[3], b[3])
inter = max(0, x1 - x0) * max(0, y1 - y0)
aa = max(0, a[2] - a[0]) * max(0, a[3] - a[1])
bb = max(0, b[2] - b[0]) * max(0, b[3] - b[1])
return inter / max(aa + bb - inter, 1)
def main():
ap = argparse.ArgumentParser()
ap.add_argument("dataset", help="directory containing jpg and YOLO txt files")
ap.add_argument("--iou", type=float, default=0.5)
ap.add_argument("--out", default="traditional_eval_results.csv",
help="CSV output path; relative paths are next to the dataset")
ap.add_argument("--vis-dir", default="traditional_eval_images",
help="directory for annotated result images; empty disables")
args = ap.parse_args()
stats = {0: [0, 0], 1: [0, 0]}
rows = []
# OpenCV on some Windows builds cannot decode non-ASCII filenames. Work
# relative to the dataset directory so the supplied Chinese path is safe.
dataset = os.path.abspath(args.dataset)
os.chdir(dataset)
# cwd is now the dataset, so a relative output avoids Windows console
# encoding issues with the Chinese parent path.
vis_dir = args.vis_dir if args.vis_dir else ""
if vis_dir:
os.makedirs(vis_dir, exist_ok=True)
files = glob.glob(os.path.join("**", "*.jpg"), recursive=True)
for image_path in files:
label_path = os.path.splitext(image_path)[0] + ".txt"
if not os.path.isfile(label_path):
continue
image = cv2.imread(image_path)
if image is None:
continue
h, w = image.shape[:2]
predictions = detect_black_triangle_papers(image)
vis = image.copy()
for p in predictions:
cv2.rectangle(vis, (int(p[0]), int(p[1])), (int(p[2]), int(p[3])), (0, 255, 255), 2)
for line in open(label_path, encoding="utf-8", errors="ignore"):
z = line.split()
if len(z) < 5 or int(float(z[0])) not in stats:
continue
cls, cx, cy, bw, bh = int(float(z[0])), *map(float, z[1:5])
truth = (int((cx - bw / 2) * w), int((cy - bh / 2) * h),
int((cx + bw / 2) * w), int((cy + bh / 2) * h))
best = max((iou(truth, p) for p in predictions), default=0.0)
best_box = max(predictions, key=lambda p: iou(truth, p), default=())
stats[cls][0] += 1
stats[cls][1] += int(best >= args.iou)
rows.append({
"image": image_path,
"class_id": cls,
"truth_xyxy": ",".join(map(str, truth[:4])),
"pred_xyxy": ",".join(map(str, best_box[:4])) if best_box else "",
"iou": f"{best:.4f}",
"pass": int(best >= args.iou),
})
color = (0, 255, 0) if best >= args.iou else (0, 0, 255)
cv2.rectangle(vis, truth[:2], truth[2:4], color, 2)
cv2.putText(vis, f"GT {cls} IoU {best:.2f}",
(truth[0], max(16, truth[1] - 4)),
cv2.FONT_HERSHEY_SIMPLEX, 0.45, color, 1, cv2.LINE_AA)
if vis_dir:
name = os.path.splitext(os.path.basename(image_path))[0] + "_result.jpg"
cv2.imwrite(os.path.join(vis_dir, name), vis)
total = sum(v[0] for v in stats.values())
good = sum(v[1] for v in stats.values())
print(f"paper objects: {good}/{total} = {good / max(total, 1):.2%} (IoU >= {args.iou})")
for cls, (n, ok) in stats.items():
print(f"class {cls}: {ok}/{n} = {ok / max(n, 1):.2%}")
out_path = args.out if os.path.isabs(args.out) else os.path.join(dataset, args.out)
with open(out_path, "w", newline="", encoding="utf-8-sig") as fp:
writer = csv.DictWriter(fp, fieldnames=("image", "class_id", "truth_xyxy",
"pred_xyxy", "iou", "pass"))
writer.writeheader()
writer.writerows(rows)
print(f"details csv: {out_path}")
if __name__ == "__main__":
main()
-7
View File
@@ -31,10 +31,3 @@
# 2.15.18 wifi连接成功重新登录
# 2.16.4 优化射箭延迟
# 2.17.0 yolo标靶类别识别
# 2.17.1 26-08-19 17:39 压力传感修改 增量方式
# 2.17.2 26-08-24 17:56 靶纸识别模型更替
# 2.17.3 26-08-25 9:57 原图拍摄开关
# 2.17.4 26-08-25 14:57 模型修改
+1 -1
View File
@@ -4,6 +4,6 @@
应用版本号
每次 OTA 更新时,只需要更新这个文件中的版本号
"""
VERSION = '2.18.10'
VERSION = '3.1.16'
-55
View File
@@ -908,11 +908,6 @@ def _save_worker_loop():
item = _save_queue.get()
if item is None:
break
if isinstance(item, dict) and item.get("kind") == "raw":
_save_raw_image_impl(
item["img_cv"], item["shot_id"], item["photo_dir"]
)
else:
_save_shot_image_impl(*item)
except Exception as e:
logger = logger_manager.logger
@@ -941,56 +936,6 @@ def start_save_shot_worker():
logger.info("[VISION] 存图 worker 线程已启动")
def _save_raw_image_impl(img_cv, shot_id, photo_dir):
"""保存相机完整原始帧,不添加任何检测标注。"""
logger = logger_manager.logger
try:
os.makedirs(photo_dir, exist_ok=True)
filename = os.path.join(photo_dir, f"shot_{shot_id}_raw.jpg")
image.cv2image(img_cv, False, False).save(filename)
prune_old_images_in_dir(
photo_dir,
getattr(config, "RAW_IMAGE_MAX_IMAGES", config.MAX_IMAGES),
logger,
"[VISION-RAW]",
)
if logger:
logger.info(f"[VISION-RAW] 已保存纯原图: {filename}")
return filename
except Exception as e:
if logger:
logger.error(f"[VISION-RAW] 保存纯原图失败: {e}")
return None
def enqueue_save_raw_shot(frame, shot_id, photo_dir=None):
"""立即复制相机帧并异步保存,避免后续识别和绘图修改原图。"""
if not getattr(config, "SAVE_RAW_IMAGE_ENABLED", False):
return
if photo_dir is None:
photo_dir = getattr(
config, "RAW_IMAGE_DIR", os.path.join(config.PHOTO_DIR, "raw")
)
try:
img_copy = np.copy(image.image2cv(frame, False, False))
_save_queue.put_nowait(
{
"kind": "raw",
"img_cv": img_copy,
"shot_id": shot_id,
"photo_dir": photo_dir,
}
)
except queue.Full:
logger = logger_manager.logger
if logger:
logger.warning("[VISION-RAW] 存图队列已满,跳过本次纯原图保存")
except Exception as e:
logger = logger_manager.logger
if logger:
logger.error(f"[VISION-RAW] 复制纯原图失败: {e}")
def enqueue_save_shot(result_img, center, radius, method, ellipse_params,
laser_point, distance_m, shot_id=None, photo_dir=None,
yolo_roi_xyxy=None, force_save=False):