Author SHA1 Message Date
linyimin 563f76745a pref: clean code 2026-09-01 15:04:04 +08:00
linyimin 48368316d7 fix: 修改版本号 2026-09-01 14:10:47 +08:00
linyimin a6547e5c32 fix: 摄像头翻转 2026-09-01 11:53:15 +08:00
linyimin 2cf223a9da fix 2026-09-01 11:53:09 +08:00
yrx ffeb82cf8f 18 2026-09-01 11:48:26 +08:00
yrx 179b30a944 two 2026-09-01 11:07:46 +08:00
45 changed files with 1183 additions and 496 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 from maix import app, uart, pinmap, time
import hashlib import hashlib
import hmac import hmac
import re
import ujson import ujson
# ========== 配置 ========== # ========== 配置 ==========
@@ -130,53 +131,109 @@ def generate_token(device_id):
return "Arrow_" + hmac.new((SALT + device_id).encode(), SALT2.encode(), hashlib.sha256).hexdigest() return "Arrow_" + hmac.new((SALT + device_id).encode(), SALT2.encode(), hashlib.sha256).hexdigest()
def send_cmd(cmd_str, timeout_ms=3000): def send_cmd(cmd_str, timeout_ms=3000):
"""发送 AT 指令并等待 OK / ERROR""" """发送 AT 指令并返回完整响应;超时返回已收到的内容。"""
print("[AT] =>", cmd_str) print("[AT] =>", cmd_str)
http_serial.write((cmd_str + "\r\n").encode()) http_serial.write((cmd_str + "\r\n").encode())
buffer = b"" buffer = b""
start = time.ticks_ms() 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) data = http_serial.read(128)
if data: if data:
buffer += data buffer += data
try: try:
decoded = buffer.decode() decoded = buffer.decode("utf-8", "ignore")
print("<= ", decoded.strip()) if "OK" in decoded or "+CME ERROR" in decoded or "ERROR" in decoded:
if "OK" in decoded: print("[AT] <=", decoded.strip())
return True return decoded
if "+CME ERROR" in decoded or "ERROR" in decoded:
return False
except: except:
pass pass
time.sleep_ms(10) 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 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): def create_http_instance(url):
cmd = f'AT+MHTTPCREATE="{url}"' cmd = f'AT+MHTTPCREATE="{url}"'
if send_cmd(cmd): response = send_cmd(cmd, 8000)
# 尝试提取 instance ID(如果模块返回) match = re.search(r"\+MHTTPCREATE:\s*(\d+)", response)
# 注意:部分模块不会返回 ID,可忽略,直接用 0 或 1 if not response_ok(response) or not match:
return True print("❌ 创建 HTTP 实例失败,模组响应:", response.strip() or "<empty>")
return False return None
return int(match.group(1))
def send_http_request(url, api_path, token, device_id, json_data): def send_http_request(url, api_path, token, device_id, json_data):
# 1. 创建 HTTP 实例 # 1. 创建 HTTP 实例
if not create_http_instance(url): instance_id = create_http_instance(url)
print("❌ 创建 HTTP 实例失败") if instance_id is None:
return False return False
# 2. 设置 Headers(假设实例 ID 为 0,或根据模块默认) # 2. 设置 Headers
instance_id = 0 # 大多数模块默认实例为 0;若支持多实例,需解析返回值 commands = (
send_cmd(f'AT+MHTTPCFG="header",{instance_id},"Content-Type: application/json"') f'AT+MHTTPCFG="header",{instance_id},"Content-Type: application/json"',
send_cmd(f'AT+MHTTPCFG="header",{instance_id},"Authorization: {token}"') f'AT+MHTTPCFG="header",{instance_id},"Authorization: {token}"',
send_cmd(f'AT+MHTTPCFG="header",{instance_id},"DeviceId: {device_id}"') 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 # 3. 发送 Body
json_str = ujson.dumps(json_data) 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 请求 # 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 请求已发送") print("✅ HTTP 请求已发送")
return True return True
else: else:
@@ -199,7 +256,7 @@ def read_response(timeout_ms=5000):
print("🚀 启动直接上传流程...") print("🚀 启动直接上传流程...")
token = generate_token(device_id) token = generate_token(device_id)
print("🔑 Token:", token) print("🔑 Token 已生成:", token[:12] + "...")
# 构造模拟数据 # 构造模拟数据
timestamp = int(time.time() * 1000) 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() read_response()
else: else:
print("💥 上传流程失败") 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.
+2 -1
View File
@@ -1,6 +1,6 @@
id: t11 id: t11
name: t11 name: t11
version: 2.17.15 version: 2.18.2
author: t11 author: t11
icon: '' icon: ''
desc: t11 desc: t11
@@ -25,6 +25,7 @@ files:
- ota_manager.py - ota_manager.py
- power.py - power.py
- server.pem - server.pem
- set_autostart.py
- shoot_manager.py - shoot_manager.py
- shot_id_generator.py - shot_id_generator.py
- target_roi_yolo.py - target_roi_yolo.py
+26 -3
View File
@@ -98,6 +98,11 @@ ADC_LASER_THRESHOLD = 3000
# ==================== 激光配置 ==================== # ==================== 激光配置 ====================
MODULE_ADDR = 0x00 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_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]) 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]) # 激光测距查询命令 DISTANCE_QUERY_CMD = bytes([0xAA, MODULE_ADDR, 0x00, 0x20, 0x00, 0x01, 0x00, 0x00, 0x21]) # 激光测距查询命令
@@ -236,7 +241,7 @@ TRIANGLE_BLACKHAT_KERNEL_FRAC = 0.018 # 核大小 ≈ min(h,w)*frac,取奇数
# ── YOLO(NPU) 靶环 ROI → 裁剪后再跑三角形(减小 CPU 处理面积)────────────────── # ── YOLO(NPU) 靶环 ROI → 裁剪后再跑三角形(减小 CPU 处理面积)──────────────────
# 日志里 net_in=W×H 来自 .mud 模型(det.input_width/height),不是这里配置的。 # 日志里 net_in=W×H 来自 .mud 模型(det.input_width/height),不是这里配置的。
TRIANGLE_YOLO_ROI_ENABLE = True TRIANGLE_YOLO_ROI_ENABLE = True
TRIANGLE_YOLO_MODEL_PATH = APP_DIR + "/model_317211.mud" TRIANGLE_YOLO_MODEL_PATH = APP_DIR + "/model_317828.mud"
# 参与 ROI 的类别:多类时只填「整靶/靶环」的 id;不要填角标类,否则 union 仍可对,但 largest 会偏小。 # 参与 ROI 的类别:多类时只填「整靶/靶环」的 id;不要填角标类,否则 union 仍可对,但 largest 会偏小。
TRIANGLE_YOLO_RING_CLASS_IDS = (0,) TRIANGLE_YOLO_RING_CLASS_IDS = (0,)
TRIANGLE_YOLO_CONF_TH = 0.9 TRIANGLE_YOLO_CONF_TH = 0.9
@@ -332,13 +337,13 @@ SAVE_IMAGE_ENABLED = True # 是否保存图像(True=保存,False=不保存
SAVE_IMAGE_ON_FAILURE = False # 检测失败时是否强制保存图像(供调试测试用) SAVE_IMAGE_ON_FAILURE = False # 检测失败时是否强制保存图像(供调试测试用)
PHOTO_DIR = "/root/phot" # 照片存储目录 PHOTO_DIR = "/root/phot" # 照片存储目录
MAX_IMAGES = 1000 MAX_IMAGES = 1000
SAVE_RAW_IMAGE_ENABLED = True # 额外保存完整原始帧(不画框、不画点、不裁剪) SAVE_RAW_IMAGE_ENABLED = False # 原图保存功能保留,但当前关闭
RAW_IMAGE_DIR = PHOTO_DIR + "/raw" RAW_IMAGE_DIR = PHOTO_DIR + "/raw"
RAW_IMAGE_MAX_IMAGES = MAX_IMAGES RAW_IMAGE_MAX_IMAGES = MAX_IMAGES
# Stage2 调试目录(默认 PHOTO_DIR/stage2_roi)内 JPEG 最多保留张数;None 表示与 MAX_IMAGES 相同 # Stage2 调试目录(默认 PHOTO_DIR/stage2_roi)内 JPEG 最多保留张数;None 表示与 MAX_IMAGES 相同
TRIANGLE_BLACK_YOLO_STAGE2_ROI_MAX_IMAGES = None TRIANGLE_BLACK_YOLO_STAGE2_ROI_MAX_IMAGES = None
SHOW_CAMERA_PHOTO_WHILE_SHOOTING = True # 是否在拍摄时显示摄像头图像(True=显示,False=不显示),建议在连着USB测试过程中打开 SHOW_CAMERA_PHOTO_WHILE_SHOOTING = False # 关闭拍摄时显示
# ==================== OTA配置 ==================== # ==================== OTA配置 ====================
MAX_BACKUPS = 5 MAX_BACKUPS = 5
@@ -353,11 +358,29 @@ PIN_MAPPINGS = {
"A28": "UART2_TX", "A28": "UART2_TX",
"A15": "I2C5_SCL", "A15": "I2C5_SCL",
"A27": "I2C5_SDA", "A27": "I2C5_SDA",
"A14": "GPIOA14", # 激光开关:低开、高关
"A24": "GPIOA24", # 电源板关机控制 "A24": "GPIOA24", # 电源板关机控制
"A25": "GPIOA25", # 电源状态绿灯
"A23": "GPIOA23", # 电源状态红灯
} }
# ==================== 电源配置 ==================== # ==================== 电源配置 ====================
AUTO_POWER_OFF_IN_SECONDS = 10 * 60 # 自动关机时间(秒),0表示不自动关机 AUTO_POWER_OFF_IN_SECONDS = 10 * 60 # 自动关机时间(秒),0表示不自动关机
# 充电时自动关机暂时禁用;需要恢复时改为 True。
CHARGING_AUTO_POWER_OFF_ENABLED = False
# 一代电源控制:A24 由电源板负责按键/关机信号,软件关机时输出高电平。
# 电源状态指示灯
STATUS_LED_GREEN_GPIO = "GPIOA25"
STATUS_LED_RED_GPIO = "GPIOA23"
STATUS_LED_GREEN_ENABLED = True
STATUS_LED_RED_ENABLED = True
STATUS_LED_ACTIVE_LEVEL = 1
STATUS_LED_LOW_BATTERY_PERCENT = 10
STATUS_LED_FULL_BATTERY_PERCENT = 90
STATUS_LED_CHARGING_BLINK_MS = 500
STATUS_LED_POLL_MS = 1000
BATTERY_SOC_LPF_ALPHA = 0.5 BATTERY_SOC_LPF_ALPHA = 0.5
BATTERY_SOC_AVG_WINDOW = 5 BATTERY_SOC_AVG_WINDOW = 5
+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()
+79 -1
View File
@@ -5,6 +5,7 @@
提供硬件对象的统一管理和访问 提供硬件对象的统一管理和访问
""" """
from maix import time from maix import time
import _thread
import config import config
from at_client import ATClient from at_client import ATClient
@@ -28,6 +29,7 @@ class HardwareManager:
self._bus = None # I2C总线 self._bus = None # I2C总线
self._adc_obj = None # ADC对象 self._adc_obj = None # ADC对象
self._at_client = None # AT客户端 self._at_client = None # AT客户端
self._status_led_monitor_started = False
self._last_active_time = 0 # 用于记录用户的最后一次活跃的时间 self._last_active_time = 0 # 用于记录用户的最后一次活跃的时间
self._stop_timer = False # 用于停止定时器的标志 self._stop_timer = False # 用于停止定时器的标志
@@ -104,11 +106,87 @@ class HardwareManager:
# 物理引脚是 A24,对应 GPIO 功能是 GPIOA24 # 物理引脚是 A24,对应 GPIO 功能是 GPIOA24
# 注意:这里需要先在 config.PIN_MAPPINGS 中配置好 "A24": "GPIOA24" # 注意:这里需要先在 config.PIN_MAPPINGS 中配置好 "A24": "GPIOA24"
from maix import gpio from maix import gpio
# 输出高电平关闭 # 一代电源板关机信号为高电平
gpio.GPIO("GPIOA24", gpio.Mode.OUT).value(1) gpio.GPIO("GPIOA24", gpio.Mode.OUT).value(1)
except Exception as e: except Exception as e:
print(f"关机失败: {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): def start_idle_timer(self):
self._stop_timer = False self._stop_timer = False
self._last_active_time = time.time() self._last_active_time = time.time()
+44 -72
View File
@@ -31,6 +31,7 @@ class LaserManager:
# 私有状态 # 私有状态
self._serial = None # 激光串口,由 laser_manager 自己持有 self._serial = None # 激光串口,由 laser_manager 自己持有
self._laser_gpio = None # A14 激光开关,低电平开启、高电平关闭
self._calibration_active = False self._calibration_active = False
self._calibration_result = None self._calibration_result = None
self._calibration_lock = threading.Lock() 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): def init(self, serial_device=None, baudrate=None):
""" """
初始化激光模块(包括串口) 初始化激光模块(A14 开关 + 测距串口)
初始化完成后主动发送关闭命令,防止 UART 初始化噪声误触发激光 初始化时先将 A14 拉高关闭激光,防止开机误触发
Args: Args:
serial_device: 串口设备路径,默认使用 config.DISTANCE_SERIAL_DEVICE serial_device: 串口设备路径,默认使用 config.DISTANCE_SERIAL_DEVICE
@@ -82,23 +94,11 @@ class LaserManager:
device = serial_device or config.DISTANCE_SERIAL_DEVICE device = serial_device or config.DISTANCE_SERIAL_DEVICE
baud = baudrate or config.DISTANCE_SERIAL_BAUDRATE baud = baudrate or config.DISTANCE_SERIAL_BAUDRATE
self.init_control_gpio()
self._serial = uart.UART(device, baud) self._serial = uart.UART(device, baud)
print(f"[LASER] 激光串口初始化完成: device={device}, baudrate={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): def load_laser_point(self):
@@ -147,66 +147,38 @@ class LaserManager:
return False return False
def turn_on_laser(self): def turn_on_laser(self):
"""发送指令开启激光,并读取回包(部分模块支持)""" """A14 输出低电平,开启激光。"""
if self._serial is None: if self._laser_gpio is None:
self.logger.error("[LASER] 激光串口未初始化,请先调用 init()") if self.logger:
return None self.logger.error("[LASER] A14 GPIO 未初始化,请先调用 init()")
return False
# 打印调试信息
self.logger.info(f"[LASER] 发送开启命令: {config.LASER_ON_CMD.hex()}")
# 清空接收缓冲区
try: try:
self._serial.read(-1) # 清空缓冲区 self._laser_gpio.value(config.LASER_CONTROL_ON_LEVEL)
except: self._laser_turned_on = True
pass if self.logger:
self.logger.info("[LASER] A14=LOW,激光开启")
# 发送命令 return True
written = self._serial.write(config.LASER_ON_CMD) except Exception as e:
self.logger.info(f"[LASER] 写入字节数: {written}") if self.logger:
self.logger.error(f"[LASER] A14 开启激光失败: {e}")
time.sleep_ms(60) return False
# 读取回包
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_turned_on = True
return resp
def turn_off_laser(self): def turn_off_laser(self):
"""发送指令关闭激光""" """A14 输出高电平,关闭激光"""
if self._serial is None: if self._laser_gpio is None:
self.logger.error("[LASER] 激光串口未初始化,请先调用 init()") if self.logger:
return None self.logger.error("[LASER] A14 GPIO 未初始化,请先调用 init()")
return False
# 打印调试信息
self.logger.info(f"[LASER] 发送关闭命令: {config.LASER_OFF_CMD.hex()}")
# 清空接收缓冲区
try: try:
self._serial.read(-1) self._laser_gpio.value(config.LASER_CONTROL_OFF_LEVEL)
except: self._laser_turned_on = False
pass if self.logger:
self.logger.info("[LASER] A14=HIGH,激光关闭")
# 发送命令 return True
written = self._serial.write(config.LASER_OFF_CMD) except Exception as e:
self.logger.info(f"[LASER] 写入字节数: {written}") if self.logger:
self.logger.error(f"[LASER] A14 关闭激光失败: {e}")
time.sleep_ms(60) return False
# 读取回包
resp = self._serial.read(20)
if resp:
self.logger.info(f"[LASER] 收到回包 ({len(resp)}字节): {resp.hex()}")
else:
self.logger.warning("🔇 无回包")
self._laser_turned_on = False
return resp
def flash_laser(self, duration_ms=1000): def flash_laser(self, duration_ms=1000):
"""闪一下激光(非阻塞版本)""" """闪一下激光(非阻塞版本)"""
+7 -3
View File
@@ -76,12 +76,14 @@ def laser_calibration_worker():
import traceback import traceback
traceback.print_exc() traceback.print_exc()
time.sleep_ms(1000) # 等待1秒后继续 time.sleep_ms(1000) # 等待1秒后继续
def cmd_str(): def cmd_str():
"""主程序入口""" """主程序入口"""
# ==================== 第一阶段:硬件初始化 ==================== # ==================== 第一阶段:硬件初始化 ====================
# 按照 main104.py 的顺序,先完成所有硬件初始化 # 按照 main104.py 的顺序,先完成所有硬件初始化
# 开机第一步先拉高 A14 关闭激光,避免其他硬件初始化期间误亮。
laser_manager.init_control_gpio()
# 1. 引脚功能映射 # 1. 引脚功能映射
for pin, func in config.PIN_MAPPINGS.items(): for pin, func in config.PIN_MAPPINGS.items():
try: try:
@@ -103,6 +105,8 @@ def cmd_str():
print(f"[BOOT] init_ina226 开始 wall_s={_w_boot:.3f}") print(f"[BOOT] init_ina226 开始 wall_s={_w_boot:.3f}")
init_ina226() init_ina226()
print(f"[BOOT] init_ina226 结束 wall +{int(round((wall_time.time() - _w_boot) * 1000))} ms") print(f"[BOOT] init_ina226 结束 wall +{int(round((wall_time.time() - _w_boot) * 1000))} ms")
# 启动 A25 绿灯和 A23 红灯状态指示。
hardware_manager.start_status_led_monitor()
# 4. 初始化显示和相机 # 4. 初始化显示和相机
_w_boot = wall_time.time() _w_boot = wall_time.time()
@@ -168,7 +172,7 @@ def cmd_str():
and bool(getattr(config, "TARGET_CLASS_YOLO_PRELOAD_ON_BOOT", True)) and bool(getattr(config, "TARGET_CLASS_YOLO_PRELOAD_ON_BOOT", True))
) )
_preload_yolo = _preload_yolo or _need_black_preload or _need_target_preload _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 and not os.path.exists(f"{config.APP_DIR}/ota_pending.json"):
preload_yolo_detector(logger) preload_yolo_detector(logger)
elif _preload_yolo and logger: elif _preload_yolo and logger:
logger.warning("[YOLO] ota_pending.json found; skip model preload until rollback check") logger.warning("[YOLO] ota_pending.json found; skip model preload until rollback check")
@@ -177,7 +181,7 @@ def cmd_str():
logger.warning(f"[YOLO-ROI] 启动预加载异常(不影响后续射箭): {e}") logger.warning(f"[YOLO-ROI] 启动预加载异常(不影响后续射箭): {e}")
# 3. 启动时检查:是否需要恢复备份 # 3. 启动时检查:是否需要恢复备份
pending_path = _ota_pending_path pending_path = f"{config.APP_DIR}/ota_pending.json"
if os.path.exists(pending_path): if os.path.exists(pending_path):
try: try:
with open(pending_path, 'r', encoding='utf-8') as f: with open(pending_path, 'r', encoding='utf-8') as f:
Binary file not shown.
-13
View File
@@ -1,13 +0,0 @@
[basic]
type = cvimodel
model = model_317189.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 = circle, triangle
Binary file not shown.
-13
View File
@@ -1,13 +0,0 @@
[basic]
type = cvimodel
model = model_317211.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 = circle, triangle
Binary file not shown.
-13
View File
@@ -1,13 +0,0 @@
[basic]
type = cvimodel
model = model_317423.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, 10, 40
Binary file not shown.
-13
View File
@@ -1,13 +0,0 @@
[basic]
type = cvimodel
model = model_317704.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 = 40, circle, triangle
+1 -1
View File
@@ -2146,7 +2146,7 @@ class NetworkManager:
} }
self.safe_enqueue(battery_data, 2) self.safe_enqueue(battery_data, 2)
self.logger.info(f"电量上报: {battery_percent}% 充电: {is_charging()}") self.logger.info(f"电量上报: {battery_percent}% 充电: {is_charging()}")
if is_charging(): if getattr(config, "CHARGING_AUTO_POWER_OFF_ENABLED", False) and is_charging():
self.safe_enqueue( self.safe_enqueue(
{ {
"cmd": 700, "cmd": 700,
+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()
+12 -2
View File
@@ -7,10 +7,12 @@
import config import config
import os import os
import subprocess import subprocess
import _thread
from logger_manager import logger_manager from logger_manager import logger_manager
from maix import time as maix_time from maix import time as maix_time
_INA226_PRESENT = None _INA226_PRESENT = None
_INA226_LOCK = _thread.allocate_lock()
def _ina226_ready() -> bool: def _ina226_ready() -> bool:
@@ -32,7 +34,11 @@ def write_register(reg, value):
data = [(value >> 8) & 0xFF, value & 0xFF] data = [(value >> 8) & 0xFF, value & 0xFF]
# 某些底层驱动在失败时只打印 “write failed” 并返回 -1,而不是抛异常; # 某些底层驱动在失败时只打印 “write failed” 并返回 -1,而不是抛异常;
# 为避免误判“初始化成功”导致后续 readfrom_mem SIGSEGV,这里把失败显式转成异常。 # 为避免误判“初始化成功”导致后续 readfrom_mem SIGSEGV,这里把失败显式转成异常。
ret = hardware_manager.bus.writeto_mem(config.INA226_ADDR, reg, bytes(data)) _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 isinstance(ret, int) and ret < 0:
if logger: if logger:
logger.error(f"[INA226] writeto_mem 失败: addr=0x{config.INA226_ADDR:02X} reg=0x{reg:02X} ret={ret}") 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): def read_register(reg):
"""读取INA226寄存器""" """读取INA226寄存器"""
from hardware import hardware_manager from hardware import hardware_manager
data = hardware_manager.bus.readfrom_mem(config.INA226_ADDR, reg, 2) _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] return (data[0] << 8) | data[1]
+3 -8
View File
@@ -8,12 +8,7 @@ from laser_manager import laser_manager
from logger_manager import logger_manager from logger_manager import logger_manager
from network import network_manager from network import network_manager
from triangle_target import load_camera_from_xml, load_triangle_positions, try_triangle_scoring from triangle_target import load_camera_from_xml, load_triangle_positions, try_triangle_scoring
from vision import ( from vision import estimate_distance, detect_circle_v3, enqueue_save_shot, enqueue_save_raw_shot
estimate_distance,
detect_circle_v3,
enqueue_save_shot,
enqueue_save_raw_shot,
)
from maix import image, time from maix import image, time
# 缓存相机标定与三角形位置,避免每次射箭重复读磁盘 # 缓存相机标定与三角形位置,避免每次射箭重复读磁盘
@@ -327,7 +322,7 @@ def process_shot(adc_val):
try: try:
frame = camera_manager.read_frame() frame = camera_manager.read_frame()
# Copy the untouched frame before any detection or drawing. # 在任何检测和绘图之前复制原始帧;默认由配置关闭,不增加量产开销。
from shot_id_generator import shot_id_generator from shot_id_generator import shot_id_generator
shot_id = shot_id_generator.generate_id() shot_id = shot_id_generator.generate_id()
enqueue_save_raw_shot(frame, shot_id) enqueue_save_raw_shot(frame, shot_id)
@@ -447,13 +442,13 @@ def process_shot(adc_val):
inner_data["ellipse_center_x"] = None inner_data["ellipse_center_x"] = None
inner_data["ellipse_center_y"] = None inner_data["ellipse_center_y"] = None
# 记录这组 inner_data 即将进入上报队列的本地时间,精确到毫秒。
upload_time_ms = int(time_std.time() * 1000) upload_time_ms = int(time_std.time() * 1000)
upload_time_sec, upload_time_millis = divmod(upload_time_ms, 1000) upload_time_sec, upload_time_millis = divmod(upload_time_ms, 1000)
inner_data["upload_time"] = ( inner_data["upload_time"] = (
time_std.strftime("%Y-%m-%d %H:%M:%S", time_std.localtime(upload_time_sec)) time_std.strftime("%Y-%m-%d %H:%M:%S", time_std.localtime(upload_time_sec))
+ f".{upload_time_millis:03d}" + f".{upload_time_millis:03d}"
) )
report_data = {"cmd": 1, "data": inner_data} report_data = {"cmd": 1, "data": inner_data}
if logger: if logger:
logger.info( logger.info(
+3 -12
View File
@@ -126,19 +126,10 @@ def _get_detector(model_path: str):
return _detector_by_path[model_path] return _detector_by_path[model_path]
try: try:
from maix import nn from maix import nn
except Exception: except ImportError:
return None return None
# YOLO is an optional capability. A broken/incompatible model must not _detector_by_path[model_path] = nn.YOLOv5(model=model_path, dual_buff=False)
# abort boot (especially before the OTA rollback check). return _detector_by_path[model_path]
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
def preload_yolo_detector(logger=None): def preload_yolo_detector(logger=None):
Binary file not shown.
+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()
-3
View File
@@ -32,9 +32,6 @@
# 2.16.4 优化射箭延迟 # 2.16.4 优化射箭延迟
# 2.17.0 yolo标靶类别识别 # 2.17.0 yolo标靶类别识别
# 2.17.1 26-08-19 1739 压力传感修改 增量方式 # 2.17.1 26-08-19 1739 压力传感修改 增量方式
# 2.17.2 26-08-24 1756 靶纸识别模型更替 # 2.17.2 26-08-24 1756 靶纸识别模型更替
# 2.17.3 26-08-25 957 原图拍摄开关 # 2.17.3 26-08-25 957 原图拍摄开关
# 2.17.4 26-08-25 1457 模型修改 # 2.17.4 26-08-25 1457 模型修改
+1 -1
View File
@@ -4,6 +4,6 @@
应用版本号 应用版本号
每次 OTA 更新时只需要更新这个文件中的版本号 每次 OTA 更新时只需要更新这个文件中的版本号
""" """
VERSION = '2.17.15' VERSION = '2.18.2'
+11 -17
View File
@@ -902,16 +902,14 @@ def _save_shot_image_impl(img_cv, center, radius, method, ellipse_params,
def _save_worker_loop(): def _save_worker_loop():
"""存图 worker从队列取任务并调用 _save_shot_image_impl""" """存图 worker处理标注图和可选的纯原图任务"""
while True: while True:
try: try:
item = _save_queue.get() item = _save_queue.get()
if item is None: if item is None:
break break
if isinstance(item, dict) and item.get("kind") == "raw": if isinstance(item, dict) and item.get("kind") == "raw":
_save_raw_image_impl( _save_raw_image_impl(item["img_cv"], item["shot_id"], item["photo_dir"])
item["img_cv"], item["shot_id"], item["photo_dir"]
)
else: else:
_save_shot_image_impl(*item) _save_shot_image_impl(*item)
except Exception as e: except Exception as e:
@@ -942,7 +940,7 @@ def start_save_shot_worker():
def _save_raw_image_impl(img_cv, shot_id, photo_dir): def _save_raw_image_impl(img_cv, shot_id, photo_dir):
"""保存相机完整原始帧,不添加任何检测标注""" """保存未标注、未裁剪的完整原始帧"""
logger = logger_manager.logger logger = logger_manager.logger
try: try:
os.makedirs(photo_dir, exist_ok=True) os.makedirs(photo_dir, exist_ok=True)
@@ -964,23 +962,19 @@ def _save_raw_image_impl(img_cv, shot_id, photo_dir):
def enqueue_save_raw_shot(frame, shot_id, photo_dir=None): def enqueue_save_raw_shot(frame, shot_id, photo_dir=None):
"""立即复制相机帧并异步保存,避免后续识别和绘图修改原图""" """复制并异步保存原始帧;由 SAVE_RAW_IMAGE_ENABLED 控制是否启用"""
if not getattr(config, "SAVE_RAW_IMAGE_ENABLED", False): if not getattr(config, "SAVE_RAW_IMAGE_ENABLED", False):
return return
if photo_dir is None: if photo_dir is None:
photo_dir = getattr( photo_dir = getattr(config, "RAW_IMAGE_DIR", os.path.join(config.PHOTO_DIR, "raw"))
config, "RAW_IMAGE_DIR", os.path.join(config.PHOTO_DIR, "raw")
)
try: try:
img_copy = np.copy(image.image2cv(frame, False, False)) img_copy = np.copy(image.image2cv(frame, False, False))
_save_queue.put_nowait( _save_queue.put_nowait({
{ "kind": "raw",
"kind": "raw", "img_cv": img_copy,
"img_cv": img_copy, "shot_id": shot_id,
"shot_id": shot_id, "photo_dir": photo_dir,
"photo_dir": photo_dir, })
}
)
except queue.Full: except queue.Full:
logger = logger_manager.logger logger = logger_manager.logger
if logger: if logger: