Compare commits
16
Commits
2.16.1
..
a00baa1770
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a00baa1770 | ||
|
|
179b30a944 | ||
|
|
42026d43e5 | ||
|
|
8a83deddd3 | ||
|
|
6a1d3fe2bd | ||
|
|
1fee464924 | ||
|
|
06994c5905 | ||
|
|
23755f48ae | ||
|
|
5f509488c5 | ||
|
|
c0bb245c8c | ||
|
|
9cfc871645 | ||
|
|
27f96d8bce | ||
|
|
80e780b931 | ||
|
|
3683033abf | ||
|
|
f0df9ad915 | ||
|
|
3fcd38f417 |
@@ -1 +0,0 @@
|
|||||||
*.sh text eol=lf
|
|
||||||
@@ -1,4 +1,3 @@
|
|||||||
/cpp_ext/build/
|
/cpp_ext/build/
|
||||||
/.cursor/
|
/.cursor/
|
||||||
/dist/
|
/dist/
|
||||||
.idea
|
|
||||||
Generated
-8
@@ -1,8 +0,0 @@
|
|||||||
# 默认忽略的文件
|
|
||||||
/shelf/
|
|
||||||
/workspace.xml
|
|
||||||
# 基于编辑器的 HTTP 客户端请求
|
|
||||||
/httpRequests/
|
|
||||||
# Datasource local storage ignored files
|
|
||||||
/dataSources/
|
|
||||||
/dataSources.local.xml
|
|
||||||
Generated
-12
@@ -1,12 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<module type="PYTHON_MODULE" version="4">
|
|
||||||
<component name="NewModuleRootManager">
|
|
||||||
<content url="file://$MODULE_DIR$" />
|
|
||||||
<orderEntry type="jdk" jdkName="yolov8" jdkType="Python SDK" />
|
|
||||||
<orderEntry type="sourceFolder" forTests="false" />
|
|
||||||
</component>
|
|
||||||
<component name="PyDocumentationSettings">
|
|
||||||
<option name="format" value="PLAIN" />
|
|
||||||
<option name="myDocStringFormat" value="Plain" />
|
|
||||||
</component>
|
|
||||||
</module>
|
|
||||||
-6
@@ -1,6 +0,0 @@
|
|||||||
<component name="InspectionProjectProfileManager">
|
|
||||||
<settings>
|
|
||||||
<option name="USE_PROJECT_PROFILE" value="false" />
|
|
||||||
<version value="1.0" />
|
|
||||||
</settings>
|
|
||||||
</component>
|
|
||||||
Generated
-7
@@ -1,7 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<project version="4">
|
|
||||||
<component name="Black">
|
|
||||||
<option name="sdkName" value="yolov8" />
|
|
||||||
</component>
|
|
||||||
<component name="ProjectRootManager" version="2" project-jdk-name="yolov8" project-jdk-type="Python SDK" />
|
|
||||||
</project>
|
|
||||||
Generated
-8
@@ -1,8 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<project version="4">
|
|
||||||
<component name="ProjectModuleManager">
|
|
||||||
<modules>
|
|
||||||
<module fileurl="file://$PROJECT_DIR$/.idea/archery.iml" filepath="$PROJECT_DIR$/.idea/archery.iml" />
|
|
||||||
</modules>
|
|
||||||
</component>
|
|
||||||
</project>
|
|
||||||
Generated
-6
@@ -1,6 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<project version="4">
|
|
||||||
<component name="VcsDirectoryMappings">
|
|
||||||
<mapping directory="" vcs="Git" />
|
|
||||||
</component>
|
|
||||||
</project>
|
|
||||||
Vendored
+1
-1
@@ -1,3 +1,3 @@
|
|||||||
{
|
{
|
||||||
"cmake.sourceDirectory": "E:/code/code/code/new/new/new/archery/cpp_ext"
|
"cmake.sourceDirectory": "E:/code/code/code/new/new/new/new/new/nw/archery - 副本/cpp_ext"
|
||||||
}
|
}
|
||||||
@@ -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.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -1,6 +1,6 @@
|
|||||||
id: t11
|
id: t11
|
||||||
name: t11
|
name: t11
|
||||||
version: 2.16.1
|
version: 2.15.35
|
||||||
author: t11
|
author: t11
|
||||||
icon: ''
|
icon: ''
|
||||||
desc: t11
|
desc: t11
|
||||||
@@ -12,18 +12,20 @@ files:
|
|||||||
- at_client.py
|
- at_client.py
|
||||||
- camera_manager.py
|
- camera_manager.py
|
||||||
- cameraParameters.xml
|
- cameraParameters.xml
|
||||||
- charging_exit.sh
|
|
||||||
- config.py
|
- config.py
|
||||||
- hardware.py
|
- hardware.py
|
||||||
- laser_detector.py
|
- laser_detector.py
|
||||||
- laser_manager.py
|
- laser_manager.py
|
||||||
- logger_manager.py
|
- logger_manager.py
|
||||||
- main.py
|
- main.py
|
||||||
|
- model_285484.cvimodel
|
||||||
|
- model_285484.mud
|
||||||
- network.py
|
- network.py
|
||||||
- ota_curl.sh
|
- ota_curl.sh
|
||||||
- ota_manager.py
|
- ota_manager.py
|
||||||
- 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
|
||||||
|
|||||||
@@ -1,47 +0,0 @@
|
|||||||
#!/bin/sh
|
|
||||||
|
|
||||||
# The application supplies its own PID. Refuse broad or malformed targets.
|
|
||||||
TARGET_PID="$1"
|
|
||||||
LASER_DEVICE="${2:-/dev/ttyS1}"
|
|
||||||
LASER_BAUD="${3:-9600}"
|
|
||||||
|
|
||||||
turn_off_laser() {
|
|
||||||
if [ ! -c "$LASER_DEVICE" ]; then
|
|
||||||
echo "[CHARGE] laser serial device not found: $LASER_DEVICE" >&2
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
stty -F "$LASER_DEVICE" "$LASER_BAUD" raw -echo 2>/dev/null || return 1
|
|
||||||
printf '\252\000\001\276\000\001\000\000\300' > "$LASER_DEVICE"
|
|
||||||
}
|
|
||||||
|
|
||||||
case "$TARGET_PID" in
|
|
||||||
''|*[!0-9]*)
|
|
||||||
echo "[CHARGE] invalid application pid: $TARGET_PID" >&2
|
|
||||||
exit 2
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
|
|
||||||
if [ "$TARGET_PID" -le 1 ]; then
|
|
||||||
echo "[CHARGE] refusing to terminate pid: $TARGET_PID" >&2
|
|
||||||
exit 2
|
|
||||||
fi
|
|
||||||
|
|
||||||
# First request laser-off while the application still owns the initialized UART.
|
|
||||||
turn_off_laser || true
|
|
||||||
|
|
||||||
kill -TERM "$TARGET_PID" 2>/dev/null || true
|
|
||||||
|
|
||||||
# Wait up to two seconds for a graceful exit, then force termination.
|
|
||||||
WAIT_COUNT=0
|
|
||||||
while kill -0 "$TARGET_PID" 2>/dev/null && [ "$WAIT_COUNT" -lt 20 ]; do
|
|
||||||
sleep 0.1
|
|
||||||
WAIT_COUNT=$((WAIT_COUNT + 1))
|
|
||||||
done
|
|
||||||
if kill -0 "$TARGET_PID" 2>/dev/null; then
|
|
||||||
kill -KILL "$TARGET_PID" 2>/dev/null || true
|
|
||||||
sleep 0.1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Send laser-off again after the application releases the UART.
|
|
||||||
turn_off_laser || true
|
|
||||||
@@ -96,6 +96,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]) # 激光测距查询命令
|
||||||
@@ -262,6 +267,16 @@ TRIANGLE_SAMPLE_PATCH_HALF_PX = 2
|
|||||||
# 开机阶段预加载 YOLO detector;detect 使用 dual_buff=False,避免返回上一帧结果。
|
# 开机阶段预加载 YOLO detector;detect 使用 dual_buff=False,避免返回上一帧结果。
|
||||||
TRIANGLE_YOLO_PRELOAD_ON_BOOT = False
|
TRIANGLE_YOLO_PRELOAD_ON_BOOT = False
|
||||||
|
|
||||||
|
# YOLO target size classification: class 0=20cm, class 1=40cm.
|
||||||
|
TARGET_CLASS_YOLO_ENABLE = True
|
||||||
|
TARGET_CLASS_YOLO_MODEL_PATH = APP_DIR + "/model_285484.mud"
|
||||||
|
TARGET_CLASS_YOLO_LABELS = (20, 40)
|
||||||
|
TARGET_CLASS_YOLO_CONF_TH = 0.50
|
||||||
|
TARGET_CLASS_YOLO_IOU_TH = 0.45
|
||||||
|
TARGET_CLASS_YOLO_RETRY_ON_EMPTY = False
|
||||||
|
TARGET_CLASS_YOLO_RETRY_CONF_TH = 0.25
|
||||||
|
TARGET_CLASS_YOLO_PRELOAD_ON_BOOT = True
|
||||||
|
|
||||||
# ── 第二段 YOLO:仅在 Stage1 裁切出的靶环图上推理(与合成 stage2 训练数据一致)→ 子框内传统算法取直角点 ──
|
# ── 第二段 YOLO:仅在 Stage1 裁切出的靶环图上推理(与合成 stage2 训练数据一致)→ 子框内传统算法取直角点 ──
|
||||||
# Stage1 靶环裁切内如何找黑三角标记(对比耗时时可切换):
|
# Stage1 靶环裁切内如何找黑三角标记(对比耗时时可切换):
|
||||||
# "yolo" — 调 Stage2 黑三角模型得子框,再子框内传统提取(需 TRIANGLE_BLACK_YOLO_ENABLE=True)。
|
# "yolo" — 调 Stage2 黑三角模型得子框,再子框内传统提取(需 TRIANGLE_BLACK_YOLO_ENABLE=True)。
|
||||||
@@ -317,6 +332,7 @@ MAX_CMD_THREADS = 10 # 并发命令线程上限(防止服务器下
|
|||||||
|
|
||||||
# ==================== 图像保存配置 ====================
|
# ==================== 图像保存配置 ====================
|
||||||
SAVE_IMAGE_ENABLED = False # 是否保存图像(True=保存,False=不保存)
|
SAVE_IMAGE_ENABLED = False # 是否保存图像(True=保存,False=不保存)
|
||||||
|
SAVE_IMAGE_ON_FAILURE = True # 检测失败时是否强制保存图像(供调试测试用)
|
||||||
PHOTO_DIR = "/root/phot" # 照片存储目录
|
PHOTO_DIR = "/root/phot" # 照片存储目录
|
||||||
MAX_IMAGES = 1000
|
MAX_IMAGES = 1000
|
||||||
# Stage2 调试目录(默认 PHOTO_DIR/stage2_roi)内 JPEG 最多保留张数;None 表示与 MAX_IMAGES 相同
|
# Stage2 调试目录(默认 PHOTO_DIR/stage2_roi)内 JPEG 最多保留张数;None 表示与 MAX_IMAGES 相同
|
||||||
@@ -337,20 +353,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 由电源板负责按键/关机信号,软件关机时输出高电平。
|
||||||
CHARGING_SHUTDOWN_ENABLED = True # True=充电时退出应用,False=关闭充电关机功能
|
|
||||||
CHARGING_DIAGNOSTIC_LOG_ENABLED = False
|
# 电源状态指示灯
|
||||||
CHARGING_CHECK_INTERVAL_MS = 5000
|
STATUS_LED_GREEN_GPIO = "GPIOA25"
|
||||||
CHARGING_CURRENT_THRESHOLD_MA = 100.0
|
STATUS_LED_RED_GPIO = "GPIOA23"
|
||||||
CHARGING_CONFIRM_COUNT = 2
|
STATUS_LED_GREEN_ENABLED = True
|
||||||
CHARGING_NOTIFY_TIMEOUT_MS = 30000
|
STATUS_LED_RED_ENABLED = True
|
||||||
CHARGING_EXIT_SCRIPT = APP_DIR + "/charging_exit.sh"
|
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
|
||||||
|
|||||||
+79
-1
@@ -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
@@ -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):
|
||||||
"""闪一下激光(非阻塞版本)"""
|
"""闪一下激光(非阻塞版本)"""
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ from version import VERSION
|
|||||||
# from logger import init_logging, get_logger, stop_logging
|
# from logger import init_logging, get_logger, stop_logging
|
||||||
from logger_manager import logger_manager
|
from logger_manager import logger_manager
|
||||||
from time_sync import sync_system_time_from_4g
|
from time_sync import sync_system_time_from_4g
|
||||||
from power import charging_shutdown_monitor, init_ina226
|
from power import init_ina226
|
||||||
from laser_manager import laser_manager
|
from laser_manager import laser_manager
|
||||||
from vision import start_save_shot_worker
|
from vision import start_save_shot_worker
|
||||||
from network import network_manager
|
from network import network_manager
|
||||||
@@ -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()
|
||||||
@@ -120,18 +124,11 @@ def cmd_str():
|
|||||||
|
|
||||||
# ==================== 第二阶段:软件初始化 ====================
|
# ==================== 第二阶段:软件初始化 ====================
|
||||||
|
|
||||||
# 1. 初始化日志系统
|
# 1. 初始化日志系统(WARNING级别,不打印/写入INFO和DEBUG日志,提高执行流畅度)
|
||||||
import logging
|
import logging
|
||||||
logger_manager.init_logging(log_level=logging.WARNING)
|
logger_manager.init_logging(log_level=logging.WARNING)
|
||||||
logger = logger_manager.logger
|
logger = logger_manager.logger
|
||||||
|
|
||||||
# 充电关机独立读取 INA226,不依赖 TCP 连接或心跳流程。
|
|
||||||
try:
|
|
||||||
_thread.start_new_thread(charging_shutdown_monitor, ())
|
|
||||||
except Exception as e:
|
|
||||||
if logger:
|
|
||||||
logger.error(f"[CHARGE] 启动独立监测线程失败: {e}")
|
|
||||||
|
|
||||||
# 补充:因为初始化的时候,激光会亮,先关了它
|
# 补充:因为初始化的时候,激光会亮,先关了它
|
||||||
# laser_manager.turn_off_laser()
|
# laser_manager.turn_off_laser()
|
||||||
|
|
||||||
@@ -169,7 +166,11 @@ def cmd_str():
|
|||||||
and _loc_black == "yolo"
|
and _loc_black == "yolo"
|
||||||
and bool(getattr(config, "TRIANGLE_BLACK_YOLO_PRELOAD_ON_BOOT", True))
|
and bool(getattr(config, "TRIANGLE_BLACK_YOLO_PRELOAD_ON_BOOT", True))
|
||||||
)
|
)
|
||||||
_preload_yolo = _preload_yolo or _need_black_preload
|
_need_target_preload = (
|
||||||
|
bool(getattr(config, "TARGET_CLASS_YOLO_ENABLE", False))
|
||||||
|
and bool(getattr(config, "TARGET_CLASS_YOLO_PRELOAD_ON_BOOT", True))
|
||||||
|
)
|
||||||
|
_preload_yolo = _preload_yolo or _need_black_preload or _need_target_preload
|
||||||
if _preload_yolo:
|
if _preload_yolo:
|
||||||
preload_yolo_detector(logger)
|
preload_yolo_detector(logger)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@@ -252,8 +253,8 @@ def cmd_str():
|
|||||||
# 4. 初始化设备ID(network_manager 内部会自动设置 device_id 和 password)
|
# 4. 初始化设备ID(network_manager 内部会自动设置 device_id 和 password)
|
||||||
network_manager.read_device_id()
|
network_manager.read_device_id()
|
||||||
|
|
||||||
# 5. 创建照片存储目录(如果启用图像保存)
|
# 5. 创建照片存储目录(如果启用图像保存或检测失败时强制保存)
|
||||||
if config.SAVE_IMAGE_ENABLED:
|
if config.SAVE_IMAGE_ENABLED or getattr(config, "SAVE_IMAGE_ON_FAILURE", False):
|
||||||
photo_dir = config.PHOTO_DIR
|
photo_dir = config.PHOTO_DIR
|
||||||
if photo_dir not in os.listdir("/root"):
|
if photo_dir not in os.listdir("/root"):
|
||||||
try:
|
try:
|
||||||
@@ -285,6 +286,12 @@ def cmd_str():
|
|||||||
logger.info("系统准备完成...")
|
logger.info("系统准备完成...")
|
||||||
|
|
||||||
last_adc_trigger = 0
|
last_adc_trigger = 0
|
||||||
|
# 读取一次ADC初始值,防止开机时传感器已有压力导致误触发
|
||||||
|
try:
|
||||||
|
last_adc_val = hardware_manager.adc_obj.read()
|
||||||
|
except Exception:
|
||||||
|
last_adc_val = 0
|
||||||
|
peak_adc_val = 0 # 当前周期内的压力峰值
|
||||||
# 气压采样:减少日志频率(每 N 个点输出一条),避免 logger.debug 拖慢采样
|
# 气压采样:减少日志频率(每 N 个点输出一条),避免 logger.debug 拖慢采样
|
||||||
PRESSURE_BATCH_SIZE = 100
|
PRESSURE_BATCH_SIZE = 100
|
||||||
|
|
||||||
@@ -294,10 +301,6 @@ def cmd_str():
|
|||||||
pressure_max = 0
|
pressure_max = 0
|
||||||
pressure_t0_ms = None
|
pressure_t0_ms = None
|
||||||
|
|
||||||
# 压力突变检测(上升沿触发)
|
|
||||||
prev_adc_val = 0 # 上一次ADC值
|
|
||||||
ADC_JUMP_THRESHOLD = 250 # 突变阈值:相邻两次采样差值超过此值视为突变
|
|
||||||
|
|
||||||
def _flush_pressure_buf(reason: str):
|
def _flush_pressure_buf(reason: str):
|
||||||
nonlocal pressure_buf, pressure_sum, pressure_min, pressure_max, pressure_t0_ms, logger
|
nonlocal pressure_buf, pressure_sum, pressure_min, pressure_max, pressure_t0_ms, logger
|
||||||
if not pressure_buf:
|
if not pressure_buf:
|
||||||
@@ -340,7 +343,6 @@ def cmd_str():
|
|||||||
time.sleep_ms(250)
|
time.sleep_ms(250)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# todo 去除或者不在这里检测
|
|
||||||
# 不在 OTA 状态下,检测是否空闲足够长,自动关机
|
# 不在 OTA 状态下,检测是否空闲足够长,自动关机
|
||||||
# print(f"[MAIN] 空闲时间: {hardware_manager.get_idle_time_in_sec() }秒")
|
# print(f"[MAIN] 空闲时间: {hardware_manager.get_idle_time_in_sec() }秒")
|
||||||
# print(f"配置关机时间:{config.AUTO_POWER_OFF_IN_SECONDS} 秒")
|
# print(f"配置关机时间:{config.AUTO_POWER_OFF_IN_SECONDS} 秒")
|
||||||
@@ -379,27 +381,24 @@ def cmd_str():
|
|||||||
pressure_max = adc_val
|
pressure_max = adc_val
|
||||||
if len(pressure_buf) >= PRESSURE_BATCH_SIZE:
|
if len(pressure_buf) >= PRESSURE_BATCH_SIZE:
|
||||||
_flush_pressure_buf("batch")
|
_flush_pressure_buf("batch")
|
||||||
|
# 峰值检测:压力从峰值下降时触发,确保捕获到最大冲击时刻
|
||||||
# 压力突变检测:当前值与前一次值的差值超过阈值时触发
|
if adc_val > peak_adc_val:
|
||||||
adc_diff = adc_val - prev_adc_val
|
peak_adc_val = adc_val # 更新峰值
|
||||||
if adc_diff >= ADC_JUMP_THRESHOLD and prev_adc_val > 0 :
|
if (peak_adc_val >= config.ADC_TRIGGER_THRESHOLD
|
||||||
|
and adc_val < peak_adc_val
|
||||||
|
and last_adc_val >= peak_adc_val):
|
||||||
|
# 封顶后下降沿触发:peak是最大值,当前值开始下降,且上次值还在peak位置
|
||||||
hardware_manager.start_idle_timer() # 重新计时
|
hardware_manager.start_idle_timer() # 重新计时
|
||||||
diff_ms = current_time - last_adc_trigger
|
diff_ms = current_time - last_adc_trigger
|
||||||
if diff_ms < 3000:
|
if diff_ms < 3000:
|
||||||
logger.info(f"[MAIN] 扳机触发过于频繁, {diff_ms}ms")
|
peak_adc_val = 0 # 去抖期间重置峰值
|
||||||
prev_adc_val = adc_val
|
time.sleep_ms(5)
|
||||||
continue
|
continue
|
||||||
last_adc_trigger = current_time
|
last_adc_trigger = current_time
|
||||||
# 触发前先把缓存刷出来
|
peak_adc_val = 0 # 触发后重置峰值
|
||||||
|
# 触发前先把缓存刷出来,避免波形被长耗时处理截断
|
||||||
_flush_pressure_buf("before_trigger")
|
_flush_pressure_buf("before_trigger")
|
||||||
|
|
||||||
if logger:
|
|
||||||
logger.info(
|
|
||||||
f"[TRIGGER] ▲ 压力突变触发! "
|
|
||||||
f"adc={adc_val}, prev={prev_adc_val}, "
|
|
||||||
f"diff=+{adc_diff}, t={current_time}ms"
|
|
||||||
)
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
process_shot(adc_val)
|
process_shot(adc_val)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@@ -408,19 +407,16 @@ def cmd_str():
|
|||||||
logger.error(f"[MAIN] 图像处理异常: {e}")
|
logger.error(f"[MAIN] 图像处理异常: {e}")
|
||||||
import traceback
|
import traceback
|
||||||
logger.error(traceback.format_exc())
|
logger.error(traceback.format_exc())
|
||||||
|
time.sleep_ms(100)
|
||||||
prev_adc_val = adc_val
|
continue
|
||||||
|
else:
|
||||||
# 未触发时的显示逻辑
|
if config.SHOW_CAMERA_PHOTO_WHILE_SHOOTING:
|
||||||
# 未触发时的显示逻辑
|
try:
|
||||||
if config.SHOW_CAMERA_PHOTO_WHILE_SHOOTING:
|
camera_manager.show(camera_manager.read_frame())
|
||||||
try:
|
except Exception as e:
|
||||||
camera_manager.show(camera_manager.read_frame())
|
pass
|
||||||
except Exception as e:
|
time.sleep_ms(5)
|
||||||
logger = logger_manager.logger
|
last_adc_val = adc_val
|
||||||
if logger:
|
|
||||||
logger.error(f"[MAIN] 显示异常: {e}")
|
|
||||||
time.sleep_ms(1)
|
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
# 主循环的顶层异常捕获,防止程序静默退出
|
# 主循环的顶层异常捕获,防止程序静默退出
|
||||||
|
|||||||
@@ -1,13 +0,0 @@
|
|||||||
|
|
||||||
[basic]
|
|
||||||
type = cvimodel
|
|
||||||
model = model_270139.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 = 黑三角和圆环
|
|
||||||
|
|
||||||
Binary file not shown.
Binary file not shown.
@@ -1,7 +1,7 @@
|
|||||||
|
|
||||||
[basic]
|
[basic]
|
||||||
type = cvimodel
|
type = cvimodel
|
||||||
model = model_270820.cvimodel
|
model = model_285484.cvimodel
|
||||||
|
|
||||||
[extra]
|
[extra]
|
||||||
model_type = yolov5
|
model_type = yolov5
|
||||||
@@ -9,5 +9,5 @@ input_type = rgb
|
|||||||
mean = 0, 0, 0
|
mean = 0, 0, 0
|
||||||
scale = 0.00392156862745098, 0.00392156862745098, 0.00392156862745098
|
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
|
anchors = 10, 13, 16, 30, 33, 23, 30, 61, 62, 45, 59, 119, 116, 90, 156, 198, 373, 326
|
||||||
labels = triangle
|
labels = 20, 40
|
||||||
|
|
||||||
+22
-26
@@ -18,7 +18,7 @@ import socket
|
|||||||
import config
|
import config
|
||||||
|
|
||||||
from hardware import hardware_manager
|
from hardware import hardware_manager
|
||||||
from power import get_bus_voltage, voltage_to_percent
|
from power import get_bus_voltage, voltage_to_percent, is_charging
|
||||||
from logger_manager import logger_manager
|
from logger_manager import logger_manager
|
||||||
from wifi import wifi_manager
|
from wifi import wifi_manager
|
||||||
import subprocess
|
import subprocess
|
||||||
@@ -669,6 +669,8 @@ class NetworkManager:
|
|||||||
self.logger.info(f"[conn wifi] cmd600 , data: {inner_data}")
|
self.logger.info(f"[conn wifi] cmd600 , data: {inner_data}")
|
||||||
ssid = inner_data.get("ssid")
|
ssid = inner_data.get("ssid")
|
||||||
password = inner_data.get("password")
|
password = inner_data.get("password")
|
||||||
|
# 停止旧的WiFi质量监测(无论当前是WiFi还是4G连接)
|
||||||
|
self._stop_wifi_quality_monitor()
|
||||||
try:
|
try:
|
||||||
for _f in ("/etc/wpa_supplicant.conf", "/boot/wpa_supplicant.conf", "/boot/wifi.ssid", "/boot/wifi.pass"):
|
for _f in ("/etc/wpa_supplicant.conf", "/boot/wpa_supplicant.conf", "/boot/wifi.ssid", "/boot/wifi.pass"):
|
||||||
try:
|
try:
|
||||||
@@ -709,12 +711,6 @@ class NetworkManager:
|
|||||||
"""线程安全地将消息加入队列(公共方法)"""
|
"""线程安全地将消息加入队列(公共方法)"""
|
||||||
self._enqueue((msg_type, data_dict), high)
|
self._enqueue((msg_type, data_dict), high)
|
||||||
|
|
||||||
def safe_enqueue_and_wait(self, data_dict, msg_type=2, high=False, timeout_ms=30000):
|
|
||||||
"""将消息加入队列,并等待网络线程确认已写入 TCP 连接。"""
|
|
||||||
sent_event = threading.Event()
|
|
||||||
self._enqueue((msg_type, data_dict, sent_event), high)
|
|
||||||
return bool(sent_event.wait(max(0, int(timeout_ms)) / 1000.0))
|
|
||||||
|
|
||||||
def connect_server(self):
|
def connect_server(self):
|
||||||
"""
|
"""
|
||||||
连接到服务器(自动选择WiFi或4G)
|
连接到服务器(自动选择WiFi或4G)
|
||||||
@@ -901,12 +897,6 @@ class NetworkManager:
|
|||||||
"""检查WiFi TCP连接是否仍然有效"""
|
"""检查WiFi TCP连接是否仍然有效"""
|
||||||
if not wifi_manager.wifi_socket:
|
if not wifi_manager.wifi_socket:
|
||||||
return False
|
return False
|
||||||
# TLS socket 无法可靠使用 MSG_PEEK,但物理 WiFi 链路仍可通过 STA 关联状态判断。
|
|
||||||
if not wifi_manager.is_sta_associated():
|
|
||||||
self.logger.warning("[WIFI-TCP] STA 已断开,关闭 WiFi TCP 并重新选网")
|
|
||||||
wifi_manager.disconnect_wifi()
|
|
||||||
self._tcp_connected = False
|
|
||||||
return False
|
|
||||||
# TLS(ssl.wrap_socket/SSLContext.wrap_socket) 后的 socket 往往不支持 MSG_PEEK/MSG_DONTWAIT。
|
# TLS(ssl.wrap_socket/SSLContext.wrap_socket) 后的 socket 往往不支持 MSG_PEEK/MSG_DONTWAIT。
|
||||||
# 这种情况下“主动探测”反而容易误报断线;让真正的 send/recv 去判定更稳。
|
# 这种情况下“主动探测”反而容易误报断线;让真正的 send/recv 去判定更稳。
|
||||||
try:
|
try:
|
||||||
@@ -1222,14 +1212,6 @@ class NetworkManager:
|
|||||||
# 这里保持 socket 为非阻塞模式(连接时已 setblocking(False))。
|
# 这里保持 socket 为非阻塞模式(连接时已 setblocking(False))。
|
||||||
# 不要反复 settimeout(),否则会把 socket 切回"阻塞+超时",并导致 conncheck 误报 timed out。
|
# 不要反复 settimeout(),否则会把 socket 切回"阻塞+超时",并导致 conncheck 误报 timed out。
|
||||||
data = wifi_manager.wifi_socket.recv(4096) # 每次最多接收4KB(无数据会抛 BlockingIOError)
|
data = wifi_manager.wifi_socket.recv(4096) # 每次最多接收4KB(无数据会抛 BlockingIOError)
|
||||||
if data == b"":
|
|
||||||
self.logger.warning("[WIFI-TCP] 对端已关闭连接")
|
|
||||||
try:
|
|
||||||
wifi_manager.wifi_socket.close()
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
wifi_manager.wifi_socket = None
|
|
||||||
self._tcp_connected = False
|
|
||||||
return data
|
return data
|
||||||
|
|
||||||
except BlockingIOError:
|
except BlockingIOError:
|
||||||
@@ -1830,9 +1812,16 @@ class NetworkManager:
|
|||||||
self.logger.info("[NET] TCP主线程启动")
|
self.logger.info("[NET] TCP主线程启动")
|
||||||
|
|
||||||
send_hartbeat_fail_count = 0
|
send_hartbeat_fail_count = 0
|
||||||
|
last_charging_check = 0
|
||||||
|
CHARGING_CHECK_INTERVAL = 5000 # 5秒检查一次充电状态
|
||||||
|
|
||||||
while True:
|
while True:
|
||||||
try:
|
try:
|
||||||
|
# 检查充电状态(每5秒检查一次)
|
||||||
|
current_time = time.ticks_ms()
|
||||||
|
if current_time - last_charging_check > CHARGING_CHECK_INTERVAL:
|
||||||
|
last_charging_check = current_time
|
||||||
|
|
||||||
# OTA 期间不要 connect/登录/心跳/发送
|
# OTA 期间不要 connect/登录/心跳/发送
|
||||||
try:
|
try:
|
||||||
from ota_manager import ota_manager
|
from ota_manager import ota_manager
|
||||||
@@ -2156,7 +2145,17 @@ class NetworkManager:
|
|||||||
"netType": self.network_type,
|
"netType": self.network_type,
|
||||||
}
|
}
|
||||||
self.safe_enqueue(battery_data, 2)
|
self.safe_enqueue(battery_data, 2)
|
||||||
self.logger.info(f"电量上报: {battery_percent}%")
|
self.logger.info(f"电量上报: {battery_percent}% 充电: {is_charging()}")
|
||||||
|
if getattr(config, "CHARGING_AUTO_POWER_OFF_ENABLED", False) and is_charging():
|
||||||
|
self.safe_enqueue(
|
||||||
|
{
|
||||||
|
"cmd": 700,
|
||||||
|
},
|
||||||
|
2,
|
||||||
|
)
|
||||||
|
elif inner_cmd == 700:
|
||||||
|
self.logger.warning("服务器下发关机!!!")
|
||||||
|
exit(-1)
|
||||||
elif inner_cmd == 5: # OTA 升级
|
elif inner_cmd == 5: # OTA 升级
|
||||||
inner_data = data_obj.get("data", {}) if isinstance(data_obj, dict) else {}
|
inner_data = data_obj.get("data", {}) if isinstance(data_obj, dict) else {}
|
||||||
ssid = inner_data.get("ssid")
|
ssid = inner_data.get("ssid")
|
||||||
@@ -2310,8 +2309,7 @@ class NetworkManager:
|
|||||||
item_is_high = False
|
item_is_high = False
|
||||||
|
|
||||||
if item:
|
if item:
|
||||||
msg_type, data_dict = item[:2]
|
msg_type, data_dict = item
|
||||||
sent_event = item[2] if len(item) > 2 else None
|
|
||||||
pkt = self._netcore.make_packet(msg_type, data_dict)
|
pkt = self._netcore.make_packet(msg_type, data_dict)
|
||||||
if not self.tcp_send_raw(pkt):
|
if not self.tcp_send_raw(pkt):
|
||||||
# 发送失败:将消息放回队首(队列满则丢弃)
|
# 发送失败:将消息放回队首(队列满则丢弃)
|
||||||
@@ -2328,8 +2326,6 @@ class NetworkManager:
|
|||||||
except:
|
except:
|
||||||
pass
|
pass
|
||||||
break
|
break
|
||||||
if sent_event is not None:
|
|
||||||
sent_event.set()
|
|
||||||
|
|
||||||
# 发送激光校准结果
|
# 发送激光校准结果
|
||||||
if logged_in:
|
if logged_in:
|
||||||
|
|||||||
@@ -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()
|
||||||
@@ -7,11 +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:
|
||||||
@@ -33,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}")
|
||||||
@@ -43,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]
|
||||||
|
|
||||||
|
|
||||||
@@ -142,115 +151,6 @@ def is_charging(threshold_ma=10.0):
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
def charging_shutdown_monitor():
|
|
||||||
"""独立监测 INA226;连续确认充电后通知服务器并退出应用。"""
|
|
||||||
logger = logger_manager.logger
|
|
||||||
shutdown_enabled = bool(getattr(config, "CHARGING_SHUTDOWN_ENABLED", False))
|
|
||||||
diagnostic_enabled = bool(getattr(config, "CHARGING_DIAGNOSTIC_LOG_ENABLED", False))
|
|
||||||
if not shutdown_enabled and not diagnostic_enabled:
|
|
||||||
if logger:
|
|
||||||
logger.info("[CHARGE] 充电退出监测已禁用")
|
|
||||||
return
|
|
||||||
|
|
||||||
interval_ms = max(100, int(getattr(config, "CHARGING_CHECK_INTERVAL_MS", 5000)))
|
|
||||||
threshold_ma = float(getattr(config, "CHARGING_CURRENT_THRESHOLD_MA", 10.0))
|
|
||||||
confirm_required = max(1, int(getattr(config, "CHARGING_CONFIRM_COUNT", 2)))
|
|
||||||
confirm_count = 0
|
|
||||||
|
|
||||||
if logger:
|
|
||||||
logger.info(
|
|
||||||
f"[CHARGE] 独立监测线程启动: interval={interval_ms}ms, "
|
|
||||||
f"threshold={threshold_ma:.1f}mA, confirm={confirm_required}, "
|
|
||||||
f"shutdown={'on' if shutdown_enabled else 'off'}"
|
|
||||||
)
|
|
||||||
|
|
||||||
while True:
|
|
||||||
current_ma = get_current()
|
|
||||||
if diagnostic_enabled and logger:
|
|
||||||
voltage = get_bus_voltage()
|
|
||||||
logger.info(
|
|
||||||
f"[CHARGE-DIAG] INA226 voltage={voltage:.3f}V, "
|
|
||||||
f"current={current_ma:.1f}mA"
|
|
||||||
)
|
|
||||||
|
|
||||||
if not shutdown_enabled:
|
|
||||||
maix_time.sleep_ms(interval_ms)
|
|
||||||
continue
|
|
||||||
|
|
||||||
if current_ma < -abs(threshold_ma):
|
|
||||||
confirm_count += 1
|
|
||||||
if logger:
|
|
||||||
logger.info(
|
|
||||||
f"[CHARGE] INA226 充电电流 {current_ma:.1f}mA "
|
|
||||||
f"({confirm_count}/{confirm_required})"
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
confirm_count = 0
|
|
||||||
|
|
||||||
if confirm_count >= confirm_required:
|
|
||||||
script_path = getattr(
|
|
||||||
config,
|
|
||||||
"CHARGING_EXIT_SCRIPT",
|
|
||||||
config.APP_DIR + "/charging_exit.sh",
|
|
||||||
)
|
|
||||||
if not os.path.isfile(script_path):
|
|
||||||
if logger:
|
|
||||||
logger.error(f"[CHARGE] 退出脚本不存在: {script_path}")
|
|
||||||
confirm_count = 0
|
|
||||||
else:
|
|
||||||
if logger:
|
|
||||||
logger.warning(
|
|
||||||
f"[CHARGE] 已连续确认充电,通知服务器后退出应用: current={current_ma:.1f}mA"
|
|
||||||
)
|
|
||||||
try:
|
|
||||||
from network import network_manager
|
|
||||||
|
|
||||||
notify_timeout_ms = max(
|
|
||||||
0,
|
|
||||||
int(getattr(config, "CHARGING_NOTIFY_TIMEOUT_MS", 30000)),
|
|
||||||
)
|
|
||||||
notification_sent = network_manager.safe_enqueue_and_wait(
|
|
||||||
{"poweroff": "充电中"},
|
|
||||||
2,
|
|
||||||
high=True,
|
|
||||||
timeout_ms=notify_timeout_ms,
|
|
||||||
)
|
|
||||||
if notification_sent:
|
|
||||||
if logger:
|
|
||||||
logger.info("[CHARGE] 充电状态已发送到服务器")
|
|
||||||
elif logger:
|
|
||||||
logger.warning(
|
|
||||||
f"[CHARGE] 等待服务器发送超时({notify_timeout_ms}ms),继续执行退出"
|
|
||||||
)
|
|
||||||
except Exception as e:
|
|
||||||
if logger:
|
|
||||||
logger.error(f"[CHARGE] 充电状态上报失败,继续执行退出: {e}")
|
|
||||||
try:
|
|
||||||
from laser_manager import laser_manager
|
|
||||||
|
|
||||||
laser_manager.turn_off_laser()
|
|
||||||
if logger:
|
|
||||||
logger.info("[CHARGE] 激光关闭命令已发送")
|
|
||||||
except Exception as e:
|
|
||||||
if logger:
|
|
||||||
logger.error(f"[CHARGE] Python 关闭激光失败,交由退出脚本兜底: {e}")
|
|
||||||
try:
|
|
||||||
subprocess.Popen([
|
|
||||||
"/bin/sh",
|
|
||||||
script_path,
|
|
||||||
str(os.getpid()),
|
|
||||||
str(getattr(config, "DISTANCE_SERIAL_DEVICE", "/dev/ttyS1")),
|
|
||||||
str(getattr(config, "DISTANCE_SERIAL_BAUDRATE", 9600)),
|
|
||||||
])
|
|
||||||
return
|
|
||||||
except Exception as e:
|
|
||||||
if logger:
|
|
||||||
logger.error(f"[CHARGE] 调用退出脚本失败: {e}")
|
|
||||||
confirm_count = 0
|
|
||||||
|
|
||||||
maix_time.sleep_ms(interval_ms)
|
|
||||||
|
|
||||||
|
|
||||||
def voltage_to_percent(voltage):
|
def voltage_to_percent(voltage):
|
||||||
"""
|
"""
|
||||||
根据电压估算电池百分比(高密度查表插值 + 滤波)。
|
根据电压估算电池百分比(高密度查表插值 + 滤波)。
|
||||||
@@ -270,7 +170,7 @@ def voltage_to_percent(voltage):
|
|||||||
return 0
|
return 0
|
||||||
if v <= 0:
|
if v <= 0:
|
||||||
return 0
|
return 0
|
||||||
return int(int(_BATTERY_MONITOR.get_soc(v) * 10) / 10) # 截断而不是四舍五入
|
return int(int(_BATTERY_MONITOR.get_soc(v) * 10) / 10) # 截断而不是四舍五入
|
||||||
|
|
||||||
|
|
||||||
class BatteryMonitor:
|
class BatteryMonitor:
|
||||||
|
|||||||
+36
-1
@@ -321,8 +321,22 @@ def process_shot(adc_val):
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
frame = camera_manager.read_frame()
|
frame = camera_manager.read_frame()
|
||||||
|
|
||||||
|
# 网络事件移到拍照之后,避免阻塞拍照
|
||||||
network_manager.safe_enqueue({"shoot_event": "start"}, msg_type=2, high=True)
|
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
|
||||||
|
try:
|
||||||
|
from target_roi_yolo import try_get_target_class_from_yolo
|
||||||
|
|
||||||
|
target_class_result = try_get_target_class_from_yolo(frame, logger=logger)
|
||||||
|
if logger:
|
||||||
|
logger.info(f"[YOLO-TARGET] 当前箭业务结果: {target_class_result}")
|
||||||
|
except Exception as exc:
|
||||||
|
if logger:
|
||||||
|
logger.warning(f"[YOLO-TARGET] 当前箭分类失败,按未知处理: {exc}")
|
||||||
|
|
||||||
# 调用算法分析
|
# 调用算法分析
|
||||||
analysis_result = analyze_shot(frame)
|
analysis_result = analyze_shot(frame)
|
||||||
|
|
||||||
@@ -382,11 +396,25 @@ def process_shot(adc_val):
|
|||||||
srv_y = round(float(dy), 4) if dy is not None else 200.0
|
srv_y = round(float(dy), 4) if dy is not None else 200.0
|
||||||
|
|
||||||
# 构造上报数据
|
# 构造上报数据
|
||||||
|
target_label = (
|
||||||
|
target_class_result.get("label")
|
||||||
|
if isinstance(target_class_result, dict)
|
||||||
|
else None
|
||||||
|
)
|
||||||
|
target_confidence = (
|
||||||
|
target_class_result.get("confidence")
|
||||||
|
if isinstance(target_class_result, dict)
|
||||||
|
else None
|
||||||
|
)
|
||||||
inner_data = {
|
inner_data = {
|
||||||
"shot_id": shot_id,
|
"shot_id": shot_id,
|
||||||
"x": srv_x,
|
"x": srv_x,
|
||||||
"y": srv_y,
|
"y": srv_y,
|
||||||
"r": 20.0, # 保留字段(服务端当前忽略,物理外环半径 cm)
|
"r": 20.0, # 保留字段(服务端当前忽略,物理外环半径 cm)
|
||||||
|
"target_class": target_label,
|
||||||
|
"target_class_confidence": (
|
||||||
|
float(target_confidence) if target_confidence is not None else None
|
||||||
|
),
|
||||||
"d": round((distance_m or 0.0) * 100),
|
"d": round((distance_m or 0.0) * 100),
|
||||||
"d_laser": round((laser_distance_m or 0.0) * 100),
|
"d_laser": round((laser_distance_m or 0.0) * 100),
|
||||||
"d_laser_quality": laser_signal_quality,
|
"d_laser_quality": laser_signal_quality,
|
||||||
@@ -414,6 +442,11 @@ def process_shot(adc_val):
|
|||||||
inner_data["ellipse_center_y"] = None
|
inner_data["ellipse_center_y"] = None
|
||||||
|
|
||||||
report_data = {"cmd": 1, "data": inner_data}
|
report_data = {"cmd": 1, "data": inner_data}
|
||||||
|
if logger:
|
||||||
|
logger.info(
|
||||||
|
f"[REPORT-TARGET] enqueue shot_id={shot_id}, "
|
||||||
|
f"target_class={target_label}, confidence={target_confidence}"
|
||||||
|
)
|
||||||
network_manager.safe_enqueue(report_data, msg_type=2, high=True)
|
network_manager.safe_enqueue(report_data, msg_type=2, high=True)
|
||||||
|
|
||||||
# 数据上报后再画标注,不干扰检测阶段的原始画面
|
# 数据上报后再画标注,不干扰检测阶段的原始画面
|
||||||
@@ -518,6 +551,7 @@ def process_shot(adc_val):
|
|||||||
laser_manager.flash_laser(config.FLASH_LASER_DURATION_MS)
|
laser_manager.flash_laser(config.FLASH_LASER_DURATION_MS)
|
||||||
|
|
||||||
# 保存图像(异步队列,与 main.py 一致)
|
# 保存图像(异步队列,与 main.py 一致)
|
||||||
|
_force_save = (dx is None and dy is None) and getattr(config, "SAVE_IMAGE_ON_FAILURE", False)
|
||||||
enqueue_save_shot(
|
enqueue_save_shot(
|
||||||
result_img,
|
result_img,
|
||||||
center,
|
center,
|
||||||
@@ -527,8 +561,9 @@ def process_shot(adc_val):
|
|||||||
(x, y),
|
(x, y),
|
||||||
distance_m,
|
distance_m,
|
||||||
shot_id=shot_id,
|
shot_id=shot_id,
|
||||||
photo_dir=config.PHOTO_DIR if config.SAVE_IMAGE_ENABLED else None,
|
photo_dir=config.PHOTO_DIR if (config.SAVE_IMAGE_ENABLED or _force_save) else None,
|
||||||
yolo_roi_xyxy=yolo_roi_xyxy if draw_yolo_roi else None,
|
yolo_roi_xyxy=yolo_roi_xyxy if draw_yolo_roi else None,
|
||||||
|
force_save=_force_save,
|
||||||
)
|
)
|
||||||
|
|
||||||
if logger:
|
if logger:
|
||||||
|
|||||||
+143
-1
@@ -89,6 +89,29 @@ def _stage2_roi_crop_save_worker(
|
|||||||
_detector_by_path = {}
|
_detector_by_path = {}
|
||||||
|
|
||||||
|
|
||||||
|
def _resolve_model_path(model_path: str):
|
||||||
|
"""Resolve a model in either the installed app or MaixVision run directory."""
|
||||||
|
model_path = (model_path or "").strip()
|
||||||
|
if model_path and os.path.isfile(model_path):
|
||||||
|
return model_path
|
||||||
|
if not model_path:
|
||||||
|
return ""
|
||||||
|
name = os.path.basename(model_path)
|
||||||
|
module_dir = os.path.dirname(os.path.abspath(__file__))
|
||||||
|
candidates = (
|
||||||
|
os.path.join(module_dir, name),
|
||||||
|
os.path.join(module_dir, "test", name),
|
||||||
|
os.path.join("/tmp/maixpy_run", name),
|
||||||
|
os.path.join("/tmp/maixpy_run", "test", name),
|
||||||
|
os.path.join(os.getcwd(), name),
|
||||||
|
os.path.join(os.getcwd(), "test", name),
|
||||||
|
)
|
||||||
|
for candidate in candidates:
|
||||||
|
if os.path.isfile(candidate):
|
||||||
|
return candidate
|
||||||
|
return model_path
|
||||||
|
|
||||||
|
|
||||||
def reset_yolo_detector_cache():
|
def reset_yolo_detector_cache():
|
||||||
"""切换模型路径时可调用(通常不必)。"""
|
"""切换模型路径时可调用(通常不必)。"""
|
||||||
global _detector_by_path
|
global _detector_by_path
|
||||||
@@ -175,6 +198,23 @@ def preload_yolo_detector(logger=None):
|
|||||||
% (_loc_black,)
|
% (_loc_black,)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if bool(getattr(cfg, "TARGET_CLASS_YOLO_ENABLE", False)) and bool(
|
||||||
|
getattr(cfg, "TARGET_CLASS_YOLO_PRELOAD_ON_BOOT", True)
|
||||||
|
):
|
||||||
|
class_model_path = _resolve_model_path(
|
||||||
|
getattr(cfg, "TARGET_CLASS_YOLO_MODEL_PATH", "") or ""
|
||||||
|
)
|
||||||
|
class_detector = _get_detector(class_model_path)
|
||||||
|
if class_detector is None:
|
||||||
|
if logger:
|
||||||
|
logger.warning(
|
||||||
|
f"[YOLO-TARGET] 预加载失败:无法加载模型 {class_model_path}"
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
ok = True
|
||||||
|
if logger:
|
||||||
|
logger.info(f"[YOLO-TARGET] 靶规格模型已预加载: {class_model_path}")
|
||||||
|
|
||||||
return ok
|
return ok
|
||||||
|
|
||||||
|
|
||||||
@@ -206,8 +246,10 @@ def _det_obj_class_id(o):
|
|||||||
if v is None:
|
if v is None:
|
||||||
continue
|
continue
|
||||||
try:
|
try:
|
||||||
|
if callable(v):
|
||||||
|
v = v()
|
||||||
return int(float(v))
|
return int(float(v))
|
||||||
except (TypeError, ValueError):
|
except (TypeError, ValueError, AttributeError):
|
||||||
continue
|
continue
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@@ -242,6 +284,106 @@ def _normalize_objs(objs):
|
|||||||
return out
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
def _det_obj_score(o):
|
||||||
|
"""Return confidence across supported Maix YOLO result formats."""
|
||||||
|
for key in ("score", "confidence", "conf", "prob"):
|
||||||
|
if hasattr(o, key):
|
||||||
|
try:
|
||||||
|
value = getattr(o, key)
|
||||||
|
if callable(value):
|
||||||
|
value = value()
|
||||||
|
value = float(value)
|
||||||
|
if value == value:
|
||||||
|
return value
|
||||||
|
except (TypeError, ValueError, AttributeError):
|
||||||
|
pass
|
||||||
|
return 0.0
|
||||||
|
|
||||||
|
|
||||||
|
def try_get_target_class_from_yolo(maix_frame, logger=None):
|
||||||
|
"""Classify the current target as 20cm or 40cm; return None if unknown."""
|
||||||
|
try:
|
||||||
|
import config as cfg
|
||||||
|
except Exception:
|
||||||
|
return None
|
||||||
|
|
||||||
|
if not bool(getattr(cfg, "TARGET_CLASS_YOLO_ENABLE", False)):
|
||||||
|
return None
|
||||||
|
model_path = _resolve_model_path(
|
||||||
|
getattr(cfg, "TARGET_CLASS_YOLO_MODEL_PATH", "") or ""
|
||||||
|
)
|
||||||
|
if not os.path.isfile(model_path):
|
||||||
|
if logger:
|
||||||
|
logger.warning(f"[YOLO-TARGET] 模型文件不存在: {model_path}")
|
||||||
|
return None
|
||||||
|
detector = _get_detector(model_path)
|
||||||
|
if detector is None:
|
||||||
|
if logger:
|
||||||
|
logger.warning("[YOLO-TARGET] 无法加载 nn.YOLOv5")
|
||||||
|
return None
|
||||||
|
|
||||||
|
conf_th = float(getattr(cfg, "TARGET_CLASS_YOLO_CONF_TH", 0.5))
|
||||||
|
iou_th = float(getattr(cfg, "TARGET_CLASS_YOLO_IOU_TH", 0.45))
|
||||||
|
labels = getattr(cfg, "TARGET_CLASS_YOLO_LABELS", (20, 40))
|
||||||
|
if isinstance(labels, str):
|
||||||
|
labels = tuple(x.strip() for x in labels.split(",") if x.strip())
|
||||||
|
labels = tuple(labels)
|
||||||
|
|
||||||
|
def _detect(threshold):
|
||||||
|
try:
|
||||||
|
raw = detector.detect(maix_frame, conf_th=threshold, iou_th=iou_th)
|
||||||
|
except Exception as exc:
|
||||||
|
if logger:
|
||||||
|
logger.warning(f"[YOLO-TARGET] detect 异常: {exc}")
|
||||||
|
return []
|
||||||
|
return _normalize_objs(raw if raw is not None else [])
|
||||||
|
|
||||||
|
def _candidates(objs):
|
||||||
|
found = []
|
||||||
|
for obj in objs:
|
||||||
|
class_id = _det_obj_class_id(obj)
|
||||||
|
if class_id is None or class_id < 0 or class_id >= len(labels):
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
label = int(float(labels[class_id]))
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
continue
|
||||||
|
if label in (20, 40):
|
||||||
|
found.append((label, class_id, _det_obj_score(obj)))
|
||||||
|
return found
|
||||||
|
|
||||||
|
objects = _detect(conf_th)
|
||||||
|
candidates = _candidates(objects)
|
||||||
|
if logger and objects:
|
||||||
|
logger.info(
|
||||||
|
"[YOLO-TARGET] 原始框=%d, 解析类别=%s"
|
||||||
|
% (
|
||||||
|
len(objects),
|
||||||
|
[(_det_obj_class_id(o), _det_obj_score(o)) for o in objects[:8]],
|
||||||
|
)
|
||||||
|
)
|
||||||
|
if not candidates and bool(
|
||||||
|
getattr(cfg, "TARGET_CLASS_YOLO_RETRY_ON_EMPTY", False)
|
||||||
|
):
|
||||||
|
retry_th = float(getattr(cfg, "TARGET_CLASS_YOLO_RETRY_CONF_TH", conf_th))
|
||||||
|
if 0 < retry_th < conf_th:
|
||||||
|
candidates = _candidates(_detect(retry_th))
|
||||||
|
|
||||||
|
if not candidates:
|
||||||
|
if logger:
|
||||||
|
logger.warning("[YOLO-TARGET] 当前帧未识别到 20/40,按未知处理")
|
||||||
|
return None
|
||||||
|
|
||||||
|
label, class_id, confidence = max(candidates, key=lambda item: item[2])
|
||||||
|
result = {"label": label, "class_id": class_id, "confidence": confidence}
|
||||||
|
if logger:
|
||||||
|
logger.info(
|
||||||
|
f"[YOLO-TARGET] 当前帧分类={label}, class_id={class_id}, "
|
||||||
|
f"conf={confidence:.3f}"
|
||||||
|
)
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
def _det_to_src_xyxy(o, coord_mode: str, src_w: int, src_h: int, net_w: int, net_h: int):
|
def _det_to_src_xyxy(o, coord_mode: str, src_w: int, src_h: int, net_w: int, net_h: int):
|
||||||
"""把单个检测框转为全图坐标系下的 xyxy(半开区间语义与后续 clip 一致)。"""
|
"""把单个检测框转为全图坐标系下的 xyxy(半开区间语义与后续 clip 一致)。"""
|
||||||
x, y, w, h = float(o.x), float(o.y), float(o.w), float(o.h)
|
x, y, w, h = float(o.x), float(o.y), float(o.w), float(o.h)
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -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.
@@ -1,144 +0,0 @@
|
|||||||
import importlib.util
|
|
||||||
from pathlib import Path
|
|
||||||
import sys
|
|
||||||
import types
|
|
||||||
import unittest
|
|
||||||
from unittest import mock
|
|
||||||
|
|
||||||
|
|
||||||
class _StopMonitor(Exception):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class _FakeTime:
|
|
||||||
now_ms = 0
|
|
||||||
stop_at_ms = None
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def reset(cls, stop_at_ms=None):
|
|
||||||
cls.now_ms = 0
|
|
||||||
cls.stop_at_ms = stop_at_ms
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def ticks_ms(cls):
|
|
||||||
return cls.now_ms
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def sleep_ms(cls, milliseconds):
|
|
||||||
cls.now_ms += milliseconds
|
|
||||||
if cls.stop_at_ms is not None and cls.now_ms >= cls.stop_at_ms:
|
|
||||||
raise _StopMonitor()
|
|
||||||
|
|
||||||
|
|
||||||
def _load_power_module():
|
|
||||||
module_path = Path(__file__).resolve().parents[1] / "power.py"
|
|
||||||
module_name = "power_charging_shutdown_test"
|
|
||||||
maix_module = types.ModuleType("maix")
|
|
||||||
maix_module.time = _FakeTime
|
|
||||||
|
|
||||||
previous_maix = sys.modules.get("maix")
|
|
||||||
sys.modules["maix"] = maix_module
|
|
||||||
try:
|
|
||||||
spec = importlib.util.spec_from_file_location(module_name, module_path)
|
|
||||||
module = importlib.util.module_from_spec(spec)
|
|
||||||
spec.loader.exec_module(module)
|
|
||||||
return module
|
|
||||||
finally:
|
|
||||||
if previous_maix is None:
|
|
||||||
sys.modules.pop("maix", None)
|
|
||||||
else:
|
|
||||||
sys.modules["maix"] = previous_maix
|
|
||||||
|
|
||||||
|
|
||||||
power = _load_power_module()
|
|
||||||
|
|
||||||
|
|
||||||
class ChargingShutdownTests(unittest.TestCase):
|
|
||||||
def setUp(self):
|
|
||||||
self.config_patch = mock.patch.multiple(
|
|
||||||
power.config,
|
|
||||||
CHARGING_SHUTDOWN_ENABLED=True,
|
|
||||||
CHARGING_DIAGNOSTIC_LOG_ENABLED=False,
|
|
||||||
CHARGING_CHECK_INTERVAL_MS=5000,
|
|
||||||
CHARGING_CURRENT_THRESHOLD_MA=100.0,
|
|
||||||
CHARGING_CONFIRM_COUNT=2,
|
|
||||||
CHARGING_NOTIFY_TIMEOUT_MS=30000,
|
|
||||||
CHARGING_EXIT_SCRIPT="/tmp/charging_exit.sh",
|
|
||||||
)
|
|
||||||
self.config_patch.start()
|
|
||||||
self.network_manager = mock.Mock()
|
|
||||||
self.network_manager.safe_enqueue_and_wait.return_value = True
|
|
||||||
network_module = types.ModuleType("network")
|
|
||||||
network_module.network_manager = self.network_manager
|
|
||||||
self.network_module_patch = mock.patch.dict(
|
|
||||||
sys.modules,
|
|
||||||
{"network": network_module},
|
|
||||||
)
|
|
||||||
self.network_module_patch.start()
|
|
||||||
_FakeTime.reset()
|
|
||||||
|
|
||||||
def tearDown(self):
|
|
||||||
self.network_module_patch.stop()
|
|
||||||
self.config_patch.stop()
|
|
||||||
|
|
||||||
def test_two_charging_samples_notify_server_and_exit(self):
|
|
||||||
popen_calls = []
|
|
||||||
with (
|
|
||||||
mock.patch.object(power, "get_current", return_value=-200.0),
|
|
||||||
mock.patch.object(power.os.path, "isfile", return_value=True),
|
|
||||||
mock.patch.object(
|
|
||||||
power.subprocess,
|
|
||||||
"Popen",
|
|
||||||
side_effect=lambda args: popen_calls.append(args),
|
|
||||||
),
|
|
||||||
):
|
|
||||||
power.charging_shutdown_monitor()
|
|
||||||
|
|
||||||
self.assertEqual(_FakeTime.now_ms, 5000)
|
|
||||||
self.assertEqual(len(popen_calls), 1)
|
|
||||||
self.network_manager.safe_enqueue_and_wait.assert_called_once_with(
|
|
||||||
{"poweroff": "充电中"}, 2, high=True, timeout_ms=30000
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_discharging_does_not_notify_or_exit(self):
|
|
||||||
_FakeTime.reset(stop_at_ms=10000)
|
|
||||||
popen_calls = []
|
|
||||||
|
|
||||||
with (
|
|
||||||
mock.patch.object(power, "get_current", return_value=200.0),
|
|
||||||
mock.patch.object(power.os.path, "isfile", return_value=True),
|
|
||||||
mock.patch.object(
|
|
||||||
power.subprocess,
|
|
||||||
"Popen",
|
|
||||||
side_effect=lambda args: popen_calls.append(args),
|
|
||||||
),
|
|
||||||
self.assertRaises(_StopMonitor),
|
|
||||||
):
|
|
||||||
power.charging_shutdown_monitor()
|
|
||||||
|
|
||||||
self.assertEqual(popen_calls, [])
|
|
||||||
self.network_manager.safe_enqueue_and_wait.assert_not_called()
|
|
||||||
|
|
||||||
def test_failed_sample_resets_confirmation_count(self):
|
|
||||||
popen_calls = []
|
|
||||||
currents = iter((-200.0, 0.0, -200.0, -200.0))
|
|
||||||
with (
|
|
||||||
mock.patch.object(power, "get_current", side_effect=lambda: next(currents)),
|
|
||||||
mock.patch.object(power.os.path, "isfile", return_value=True),
|
|
||||||
mock.patch.object(
|
|
||||||
power.subprocess,
|
|
||||||
"Popen",
|
|
||||||
side_effect=lambda args: popen_calls.append(args),
|
|
||||||
),
|
|
||||||
):
|
|
||||||
power.charging_shutdown_monitor()
|
|
||||||
|
|
||||||
self.assertEqual(_FakeTime.now_ms, 15000)
|
|
||||||
self.assertEqual(len(popen_calls), 1)
|
|
||||||
self.network_manager.safe_enqueue_and_wait.assert_called_once_with(
|
|
||||||
{"poweroff": "充电中"}, 2, high=True, timeout_ms=30000
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
unittest.main()
|
|
||||||
@@ -154,11 +154,11 @@ def detect_circle_v3(frame, laser_point=None):
|
|||||||
max_r = max(red_radius, yellow_radius)
|
max_r = max(red_radius, yellow_radius)
|
||||||
size_ratio = min_r / max_r if max_r > 0 else 0
|
size_ratio = min_r / max_r if max_r > 0 else 0
|
||||||
print(f"Debug -> 圆心距={distance:.1f}(阈值={max_distance:.1f}), "
|
print(f"Debug -> 圆心距={distance:.1f}(阈值={max_distance:.1f}), "
|
||||||
f"大小比={size_ratio:.2f}(阈值=0.5), "
|
f"大小比={size_ratio:.2f}(阈值=0.4), "
|
||||||
f"距离OK={distance < max_distance}, 大小OK={size_ratio > 0.5}")
|
f"距离OK={distance < max_distance}, 大小OK={size_ratio >= 0.4}")
|
||||||
|
|
||||||
# 允许红圈在黄圈外侧或内侧,只要大小相近(较小/较大 >= 0.5)
|
# 允许红圈在黄圈外侧或内侧,只要大小相近(较小/较大 >= 0.5)
|
||||||
if distance < max_distance and size_ratio > 0.5:
|
if distance < max_distance and size_ratio >= 0.4:
|
||||||
found_valid_red = True
|
found_valid_red = True
|
||||||
print(
|
print(
|
||||||
f"[target] -> 找到匹配的红圈: 黄心({yellow_center}), 红心({red_center}), 距离:{distance:.1f}, 黄半径:{yellow_radius}, 红半径:{red_radius}")
|
f"[target] -> 找到匹配的红圈: 黄心({yellow_center}), 红心({red_center}), 距离:{distance:.1f}, 黄半径:{yellow_radius}, 红半径:{red_radius}")
|
||||||
@@ -598,7 +598,7 @@ if __name__ == "__main__":
|
|||||||
|
|
||||||
# 1. 设置要测试的图片路径
|
# 1. 设置要测试的图片路径
|
||||||
# 建议将图片放在与脚本同级目录,或者使用绝对路径
|
# 建议将图片放在与脚本同级目录,或者使用绝对路径
|
||||||
TARGET_IMAGE = "/root/phot/None_314_258_0_0041.bmp"
|
TARGET_IMAGE = "/root/phot/shot_1830921_0_no_target.jpg"
|
||||||
|
|
||||||
TARGET_DIR = "/root/phot" # 修改为你想要读取的目录路径
|
TARGET_DIR = "/root/phot" # 修改为你想要读取的目录路径
|
||||||
|
|
||||||
|
|||||||
@@ -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()
|
||||||
@@ -1,59 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
"""Read the digital voltage level on the MaixCAM P21 pin.
|
|
||||||
|
|
||||||
P21 is a digital GPIO pin, not the MaixCAM analog ADC input. Therefore this
|
|
||||||
script can only distinguish LOW and HIGH. For a continuous voltage value,
|
|
||||||
connect the signal to the board's B3/ADC pin and use ADC channel 0 instead.
|
|
||||||
|
|
||||||
Do not apply more than 3.3 V to P21. Always connect the signal ground to the
|
|
||||||
MaixCAM ground.
|
|
||||||
"""
|
|
||||||
|
|
||||||
from maix import app, gpio, pinmap, time
|
|
||||||
|
|
||||||
|
|
||||||
PIN = "P21"
|
|
||||||
IO_HIGH_VOLTAGE = 3.3
|
|
||||||
SAMPLE_INTERVAL_MS = 200
|
|
||||||
|
|
||||||
|
|
||||||
def find_gpio_function(pin):
|
|
||||||
"""Return the GPIO function supported by the requested physical pin."""
|
|
||||||
functions = pinmap.get_pin_functions(pin)
|
|
||||||
gpio_functions = [name for name in functions if name.startswith("GPIO")]
|
|
||||||
|
|
||||||
print(f"{pin} supported functions: {', '.join(functions)}")
|
|
||||||
if not gpio_functions:
|
|
||||||
raise RuntimeError(f"{pin} does not provide a GPIO input function")
|
|
||||||
|
|
||||||
return gpio_functions[0]
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
|
||||||
gpio_function = find_gpio_function(PIN)
|
|
||||||
pinmap.set_pin_function(PIN, gpio_function)
|
|
||||||
voltage_input = gpio.GPIO(gpio_function, gpio.Mode.IN)
|
|
||||||
|
|
||||||
print(f"Reading {PIN} through {gpio_function}")
|
|
||||||
print("P21 only reports LOW/HIGH; displayed voltage is an estimate.")
|
|
||||||
print("Press the MaixCAM exit key to stop.")
|
|
||||||
|
|
||||||
while not app.need_exit():
|
|
||||||
level = voltage_input.value()
|
|
||||||
estimated_voltage = IO_HIGH_VOLTAGE if level else 0.0
|
|
||||||
state = "HIGH" if level else "LOW"
|
|
||||||
print(
|
|
||||||
f"{PIN}: level={level}, state={state}, "
|
|
||||||
f"estimated_voltage={estimated_voltage:.1f} V"
|
|
||||||
)
|
|
||||||
time.sleep_ms(SAMPLE_INTERVAL_MS)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
try:
|
|
||||||
main()
|
|
||||||
except Exception as error:
|
|
||||||
print(f"P21 voltage detection failed: {error}")
|
|
||||||
print("Check that this MaixCAM model exposes P21 as a GPIO pin.")
|
|
||||||
raise
|
|
||||||
@@ -1,139 +0,0 @@
|
|||||||
import json
|
|
||||||
import sys
|
|
||||||
import types
|
|
||||||
import unittest
|
|
||||||
|
|
||||||
|
|
||||||
class _FakeTime:
|
|
||||||
@staticmethod
|
|
||||||
def sleep(_seconds):
|
|
||||||
pass
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def sleep_ms(_milliseconds):
|
|
||||||
pass
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def ticks_ms():
|
|
||||||
return 0
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def ticks_diff(left, right):
|
|
||||||
return left - right
|
|
||||||
|
|
||||||
|
|
||||||
class _FakeLogger:
|
|
||||||
def debug(self, *_args, **_kwargs):
|
|
||||||
pass
|
|
||||||
|
|
||||||
def info(self, *_args, **_kwargs):
|
|
||||||
pass
|
|
||||||
|
|
||||||
def warning(self, *_args, **_kwargs):
|
|
||||||
pass
|
|
||||||
|
|
||||||
def error(self, *_args, **_kwargs):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class _FakeSocket:
|
|
||||||
def __init__(self, recv_data=b""):
|
|
||||||
self.recv_data = recv_data
|
|
||||||
self.closed = False
|
|
||||||
|
|
||||||
def close(self):
|
|
||||||
self.closed = True
|
|
||||||
|
|
||||||
def recv(self, _size, *_flags):
|
|
||||||
return self.recv_data
|
|
||||||
|
|
||||||
|
|
||||||
class _StopAfterCallback:
|
|
||||||
def __init__(self):
|
|
||||||
self.stopped = False
|
|
||||||
|
|
||||||
def is_set(self):
|
|
||||||
return self.stopped
|
|
||||||
|
|
||||||
|
|
||||||
maix_module = types.ModuleType("maix")
|
|
||||||
maix_module.time = _FakeTime
|
|
||||||
maix_module.network = types.SimpleNamespace()
|
|
||||||
maix_module.err = types.SimpleNamespace()
|
|
||||||
sys.modules.setdefault("maix", maix_module)
|
|
||||||
sys.modules.setdefault("ujson", json)
|
|
||||||
|
|
||||||
netcore_module = types.ModuleType("archery_netcore")
|
|
||||||
netcore_module.get_config = lambda: {"SERVER_IP": "127.0.0.1", "SERVER_PORT": 1234}
|
|
||||||
netcore_module.parse_packet = lambda _packet: (0, {})
|
|
||||||
netcore_module.make_packet = lambda *_args, **_kwargs: b""
|
|
||||||
netcore_module.actions_for_inner_cmd = lambda *_args, **_kwargs: []
|
|
||||||
sys.modules["archery_netcore"] = netcore_module
|
|
||||||
|
|
||||||
hardware_module = types.ModuleType("hardware")
|
|
||||||
hardware_module.hardware_manager = types.SimpleNamespace()
|
|
||||||
sys.modules["hardware"] = hardware_module
|
|
||||||
|
|
||||||
power_module = types.ModuleType("power")
|
|
||||||
power_module.get_bus_voltage = lambda: 0
|
|
||||||
power_module.voltage_to_percent = lambda _voltage: 0
|
|
||||||
sys.modules["power"] = power_module
|
|
||||||
|
|
||||||
import logger_manager
|
|
||||||
import wifi
|
|
||||||
import network
|
|
||||||
|
|
||||||
|
|
||||||
class WiFiFailoverTests(unittest.TestCase):
|
|
||||||
def setUp(self):
|
|
||||||
logger_manager.logger_manager._logger = _FakeLogger()
|
|
||||||
|
|
||||||
def test_monitor_switches_when_sta_association_is_lost(self):
|
|
||||||
manager = wifi.wifi_manager
|
|
||||||
stop_event = _StopAfterCallback()
|
|
||||||
callbacks = []
|
|
||||||
|
|
||||||
manager._wifi_socket = _FakeSocket()
|
|
||||||
manager._wifi_quality_stop_event = stop_event
|
|
||||||
manager._network_type_callback = lambda: "wifi"
|
|
||||||
manager.is_sta_associated = lambda: False
|
|
||||||
manager._get_wifi_rssi_dbm = lambda: None
|
|
||||||
|
|
||||||
def on_poor_quality():
|
|
||||||
callbacks.append(True)
|
|
||||||
stop_event.stopped = True
|
|
||||||
|
|
||||||
manager._on_poor_quality_callback = on_poor_quality
|
|
||||||
manager._quality_monitor_loop()
|
|
||||||
|
|
||||||
self.assertEqual(callbacks, [True])
|
|
||||||
self.assertIsNone(manager.last_wifi_rtt_ms)
|
|
||||||
|
|
||||||
def test_tls_connection_check_rejects_lost_sta_association(self):
|
|
||||||
manager = network.network_manager
|
|
||||||
sock = _FakeSocket()
|
|
||||||
wifi.wifi_manager._wifi_socket = sock
|
|
||||||
wifi.wifi_manager._wifi_connected = True
|
|
||||||
wifi.wifi_manager._wifi_ip = "192.168.1.2"
|
|
||||||
wifi.wifi_manager.is_sta_associated = lambda: False
|
|
||||||
manager._tcp_connected = True
|
|
||||||
|
|
||||||
self.assertFalse(manager._check_wifi_connection())
|
|
||||||
self.assertTrue(sock.closed)
|
|
||||||
self.assertIsNone(wifi.wifi_manager.wifi_socket)
|
|
||||||
self.assertFalse(manager.tcp_connected)
|
|
||||||
|
|
||||||
def test_receive_eof_marks_wifi_tcp_disconnected(self):
|
|
||||||
manager = network.network_manager
|
|
||||||
sock = _FakeSocket(recv_data=b"")
|
|
||||||
wifi.wifi_manager._wifi_socket = sock
|
|
||||||
manager._tcp_connected = True
|
|
||||||
|
|
||||||
self.assertEqual(manager.receive_tcp_data_via_wifi(), b"")
|
|
||||||
self.assertTrue(sock.closed)
|
|
||||||
self.assertIsNone(wifi.wifi_manager.wifi_socket)
|
|
||||||
self.assertFalse(manager.tcp_connected)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
unittest.main()
|
|
||||||
@@ -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()
|
||||||
@@ -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())
|
||||||
+2
-8
@@ -29,11 +29,5 @@
|
|||||||
# 2.15.16 修复wifi连接问题
|
# 2.15.16 修复wifi连接问题
|
||||||
# 2.15.17 修复wifi连接问题
|
# 2.15.17 修复wifi连接问题
|
||||||
# 2.15.18 wifi连接成功重新登录
|
# 2.15.18 wifi连接成功重新登录
|
||||||
# 2.15.20 加了充电关机,激光也同时关闭
|
# 2.16.4 优化射箭延迟
|
||||||
# 2.15.21 测试4g 扩大了缓存池和改了心跳时间
|
# 2.17.0 yolo标靶类别识别
|
||||||
# 2.15.22 修复了4g网络和wifi切换问题
|
|
||||||
# 2.15.23 合并充电关机与稳定版网络修复
|
|
||||||
# 2.15.24 空改测试
|
|
||||||
# 2.15.25 修复整合后关机失败和ota格式更新问题
|
|
||||||
# 2.15.26
|
|
||||||
# 2.16.1 修复压力传感触发判断按照增量
|
|
||||||
+1
-1
@@ -4,6 +4,6 @@
|
|||||||
应用版本号
|
应用版本号
|
||||||
每次 OTA 更新时,只需要更新这个文件中的版本号
|
每次 OTA 更新时,只需要更新这个文件中的版本号
|
||||||
"""
|
"""
|
||||||
VERSION = '2.16.1'
|
VERSION = '2.18.0'
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -631,7 +631,7 @@ def detect_circle_v3(frame, laser_point=None, img_cv=None):
|
|||||||
min_r = min(rc["radius"], yellow_radius)
|
min_r = min(rc["radius"], yellow_radius)
|
||||||
max_r = max(rc["radius"], yellow_radius)
|
max_r = max(rc["radius"], yellow_radius)
|
||||||
size_ratio = min_r / max_r if max_r > 0 else 0
|
size_ratio = min_r / max_r if max_r > 0 else 0
|
||||||
if dist_centers < max_dist and size_ratio >= 0.3:
|
if dist_centers < max_dist and size_ratio >= 0.4:
|
||||||
if logger:
|
if logger:
|
||||||
logger.info(f"[target] -> 找到匹配的红圈: 黄心({yellow_center}), "
|
logger.info(f"[target] -> 找到匹配的红圈: 黄心({yellow_center}), "
|
||||||
f"红心({rc['center']}), 距离:{dist_centers:.1f}, "
|
f"红心({rc['center']}), 距离:{dist_centers:.1f}, "
|
||||||
@@ -797,12 +797,12 @@ def estimate_pixel(physical_distance_cm, target_distance_m):
|
|||||||
|
|
||||||
def _save_shot_image_impl(img_cv, center, radius, method, ellipse_params,
|
def _save_shot_image_impl(img_cv, center, radius, method, ellipse_params,
|
||||||
laser_point, distance_m, shot_id=None, photo_dir=None,
|
laser_point, distance_m, shot_id=None, photo_dir=None,
|
||||||
yolo_roi_xyxy=None):
|
yolo_roi_xyxy=None, force_save=False):
|
||||||
"""
|
"""
|
||||||
内部实现:在 img_cv (numpy HWC RGB) 上绘制标注并保存。
|
内部实现:在 img_cv (numpy HWC RGB) 上绘制标注并保存。
|
||||||
由 save_shot_image(同步)和存图 worker(异步)调用。
|
由 save_shot_image(同步)和存图 worker(异步)调用。
|
||||||
"""
|
"""
|
||||||
if not config.SAVE_IMAGE_ENABLED:
|
if not config.SAVE_IMAGE_ENABLED and not force_save:
|
||||||
return None
|
return None
|
||||||
if photo_dir is None:
|
if photo_dir is None:
|
||||||
photo_dir = config.PHOTO_DIR
|
photo_dir = config.PHOTO_DIR
|
||||||
@@ -938,11 +938,12 @@ def start_save_shot_worker():
|
|||||||
|
|
||||||
def enqueue_save_shot(result_img, center, radius, method, ellipse_params,
|
def enqueue_save_shot(result_img, center, radius, method, ellipse_params,
|
||||||
laser_point, distance_m, shot_id=None, photo_dir=None,
|
laser_point, distance_m, shot_id=None, photo_dir=None,
|
||||||
yolo_roi_xyxy=None):
|
yolo_roi_xyxy=None, force_save=False):
|
||||||
"""
|
"""
|
||||||
将存图任务放入队列,由 worker 异步保存。主线程传入 result_img 的复制,不阻塞。
|
将存图任务放入队列,由 worker 异步保存。主线程传入 result_img 的复制,不阻塞。
|
||||||
|
force_save=True 时,忽略 SAVE_IMAGE_ENABLED 配置强制保存(用于检测失败时的调试图像)。
|
||||||
"""
|
"""
|
||||||
if not config.SAVE_IMAGE_ENABLED:
|
if not config.SAVE_IMAGE_ENABLED and not force_save:
|
||||||
return
|
return
|
||||||
if photo_dir is None:
|
if photo_dir is None:
|
||||||
photo_dir = config.PHOTO_DIR
|
photo_dir = config.PHOTO_DIR
|
||||||
@@ -965,6 +966,7 @@ def enqueue_save_shot(result_img, center, radius, method, ellipse_params,
|
|||||||
shot_id,
|
shot_id,
|
||||||
photo_dir,
|
photo_dir,
|
||||||
yolo_roi_xyxy,
|
yolo_roi_xyxy,
|
||||||
|
force_save,
|
||||||
)
|
)
|
||||||
try:
|
try:
|
||||||
_save_queue.put_nowait(task)
|
_save_queue.put_nowait(task)
|
||||||
@@ -976,12 +978,12 @@ def enqueue_save_shot(result_img, center, radius, method, ellipse_params,
|
|||||||
|
|
||||||
def save_shot_image(result_img, center, radius, method, ellipse_params,
|
def save_shot_image(result_img, center, radius, method, ellipse_params,
|
||||||
laser_point, distance_m, shot_id=None, photo_dir=None,
|
laser_point, distance_m, shot_id=None, photo_dir=None,
|
||||||
yolo_roi_xyxy=None):
|
yolo_roi_xyxy=None, force_save=False):
|
||||||
"""
|
"""
|
||||||
保存射击图像(带标注)。同步调用,会阻塞。
|
保存射击图像(带标注)。同步调用,会阻塞。
|
||||||
主流程建议使用 enqueue_save_shot;此处保留供校准、测试等场景使用。
|
主流程建议使用 enqueue_save_shot;此处保留供校准、测试等场景使用。
|
||||||
"""
|
"""
|
||||||
if not config.SAVE_IMAGE_ENABLED:
|
if not config.SAVE_IMAGE_ENABLED and not force_save:
|
||||||
return None
|
return None
|
||||||
if photo_dir is None:
|
if photo_dir is None:
|
||||||
photo_dir = config.PHOTO_DIR
|
photo_dir = config.PHOTO_DIR
|
||||||
@@ -998,6 +1000,7 @@ def save_shot_image(result_img, center, radius, method, ellipse_params,
|
|||||||
shot_id,
|
shot_id,
|
||||||
photo_dir,
|
photo_dir,
|
||||||
yolo_roi_xyxy,
|
yolo_roi_xyxy,
|
||||||
|
force_save,
|
||||||
)
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger = logger_manager.logger
|
logger = logger_manager.logger
|
||||||
|
|||||||
@@ -541,7 +541,7 @@ class WiFiManager:
|
|||||||
|
|
||||||
def start_quality_monitor(self, network_type_callback, on_poor_quality_callback):
|
def start_quality_monitor(self, network_type_callback, on_poor_quality_callback):
|
||||||
"""
|
"""
|
||||||
启动 WiFi 质量后台监测线程(每 5 秒检查 STA 关联状态和 RSSI)
|
启动 WiFi 质量后台监测线程(每 5 秒测量一次 RTT 和 RSSI)
|
||||||
只在 WiFi 连接时运行,不影响业务发送性能
|
只在 WiFi 连接时运行,不影响业务发送性能
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
@@ -591,38 +591,34 @@ class WiFiManager:
|
|||||||
def _quality_monitor_loop(self):
|
def _quality_monitor_loop(self):
|
||||||
"""
|
"""
|
||||||
WiFi 质量监测循环(后台线程)
|
WiFi 质量监测循环(后台线程)
|
||||||
每 5 秒检查 STA 关联状态和 RSSI,发现断链或质量差则触发切换
|
每 5 秒测量一次 RTT 和 RSSI,发现质量差则触发切换
|
||||||
"""
|
"""
|
||||||
while not self._wifi_quality_stop_event.is_set():
|
while not self._wifi_quality_stop_event.is_set():
|
||||||
try:
|
try:
|
||||||
# 只在 WiFi 连接时才测量
|
# 只在 WiFi 连接时才测量
|
||||||
network_type = self._network_type_callback()
|
network_type = self._network_type_callback()
|
||||||
if network_type == "wifi" and self._wifi_socket:
|
if network_type == "wifi" and self._wifi_socket:
|
||||||
# RTT 测量当前禁用;STA 关联状态用于判断物理 WiFi 链路是否仍存在。
|
# # 测量 RTT(1 个样本,快速测量)
|
||||||
# 不能把禁用的 RTT 伪装成 0ms,否则关闭热点后会一直被判为正常。
|
# rtt_ms, reachable = self._measure_wifi_tcp_rtt_ms(
|
||||||
reachable = self.is_sta_associated()
|
# self._server_ip, self._server_port,
|
||||||
rtt_ms = None
|
# samples=1, per_sample_timeout_ms=600
|
||||||
|
# )
|
||||||
|
|
||||||
# 获取 RSSI
|
# 获取 RSSI
|
||||||
rssi_dbm = self._get_wifi_rssi_dbm()
|
rssi_dbm = self._get_wifi_rssi_dbm()
|
||||||
|
|
||||||
# 更新缓存
|
# 更新缓存
|
||||||
self._last_wifi_rtt_ms = rtt_ms
|
# 不使用 RTT 测量
|
||||||
|
rtt_ms = 0
|
||||||
|
reachable = True
|
||||||
|
self._last_wifi_rtt_ms = rtt_ms if reachable else None
|
||||||
self._last_wifi_rssi_dbm = rssi_dbm
|
self._last_wifi_rssi_dbm = rssi_dbm
|
||||||
_rtt_s = f"{rtt_ms:.0f}ms" if rtt_ms is not None else "n/a"
|
|
||||||
_rssi_s = f"{rssi_dbm:.0f}" if rssi_dbm is not None else "n/a"
|
_rssi_s = f"{rssi_dbm:.0f}" if rssi_dbm is not None else "n/a"
|
||||||
self.logger.debug(
|
self.logger.debug(f"[WiFi Monitor] - RTT={rtt_ms:.0f}ms, RSSI={_rssi_s}dBm")
|
||||||
f"[WiFi Monitor] - associated={reachable}, RTT={_rtt_s}, RSSI={_rssi_s}dBm"
|
|
||||||
)
|
|
||||||
|
|
||||||
# 判断质量是否差(切换前做 2 次快速复测,防止瞬时抖动)
|
# 判断质量是否差(切换前做 2 次快速复测,防止瞬时抖动)
|
||||||
def _is_bad_now(_reachable, _rtt, _rssi):
|
def _is_bad_now(_reachable, _rtt, _rssi):
|
||||||
if not _reachable:
|
if (not _reachable) or (_rtt is None) or (_rtt == float("inf")):
|
||||||
return True
|
|
||||||
# RTT 未启用时不参与质量判断;链路状态仍由 STA 关联保证。
|
|
||||||
if _rtt is None:
|
|
||||||
return False
|
|
||||||
if _rtt == float("inf"):
|
|
||||||
return True
|
return True
|
||||||
return self._is_wifi_quality_bad(_rtt, _rssi)
|
return self._is_wifi_quality_bad(_rtt, _rssi)
|
||||||
|
|
||||||
@@ -632,8 +628,13 @@ class WiFiManager:
|
|||||||
|
|
||||||
for retry_idx in range(2):
|
for retry_idx in range(2):
|
||||||
time.sleep_ms(1000)
|
time.sleep_ms(1000)
|
||||||
reachable2 = self.is_sta_associated()
|
# 不使用 RTT 测量
|
||||||
rtt2 = None
|
rtt2 = 0
|
||||||
|
reachable2 = True
|
||||||
|
# rtt2, reachable2 = self._measure_wifi_tcp_rtt_ms(
|
||||||
|
# self._server_ip, self._server_port,
|
||||||
|
# samples=1, per_sample_timeout_ms=600
|
||||||
|
# )
|
||||||
rssi2 = self._get_wifi_rssi_dbm()
|
rssi2 = self._get_wifi_rssi_dbm()
|
||||||
|
|
||||||
# 更新缓存,便于外部查看最新状态
|
# 更新缓存,便于外部查看最新状态
|
||||||
@@ -642,10 +643,14 @@ class WiFiManager:
|
|||||||
|
|
||||||
bad2 = _is_bad_now(reachable2, rtt2, rssi2)
|
bad2 = _is_bad_now(reachable2, rtt2, rssi2)
|
||||||
try:
|
try:
|
||||||
_rtt_disp = f"{rtt2:.0f}ms" if rtt2 is not None else "n/a"
|
_rtt_disp = (
|
||||||
|
rtt2
|
||||||
|
if rtt2 is not None and rtt2 != float("inf")
|
||||||
|
else -1
|
||||||
|
)
|
||||||
self.logger.info(
|
self.logger.info(
|
||||||
f"[WiFi Monitor] 复测{retry_idx+1}/2: reachable={reachable2}, "
|
f"[WiFi Monitor] 复测{retry_idx+1}/2: reachable={reachable2}, "
|
||||||
f"rtt={_rtt_disp}, rssi={rssi2}, bad={bad2}"
|
f"rtt={_rtt_disp:.0f}ms, rssi={rssi2}, bad={bad2}"
|
||||||
)
|
)
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|||||||
Reference in New Issue
Block a user