Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b62327413f | ||
|
|
165eeff64e | ||
|
|
a184ff7d55 | ||
|
|
054e9e6d90 | ||
|
|
ae339889c2 | ||
|
|
b94b0f2e55 | ||
|
|
e82941a161 | ||
|
|
6556cfcf74 | ||
|
|
abbf30d7c0 | ||
|
|
5cf752bb3f | ||
|
|
6d8de56bfa | ||
|
|
aee1a92760 |
@@ -0,0 +1 @@
|
|||||||
|
*.sh text eol=lf
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
/cpp_ext/build/
|
/cpp_ext/build/
|
||||||
/.cursor/
|
/.cursor/
|
||||||
/dist/
|
/dist/
|
||||||
|
.idea
|
||||||
Generated
+8
@@ -0,0 +1,8 @@
|
|||||||
|
# 默认忽略的文件
|
||||||
|
/shelf/
|
||||||
|
/workspace.xml
|
||||||
|
# 基于编辑器的 HTTP 客户端请求
|
||||||
|
/httpRequests/
|
||||||
|
# Datasource local storage ignored files
|
||||||
|
/dataSources/
|
||||||
|
/dataSources.local.xml
|
||||||
Generated
+12
@@ -0,0 +1,12 @@
|
|||||||
|
<?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
@@ -0,0 +1,6 @@
|
|||||||
|
<component name="InspectionProjectProfileManager">
|
||||||
|
<settings>
|
||||||
|
<option name="USE_PROJECT_PROFILE" value="false" />
|
||||||
|
<version value="1.0" />
|
||||||
|
</settings>
|
||||||
|
</component>
|
||||||
Generated
+7
@@ -0,0 +1,7 @@
|
|||||||
|
<?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
@@ -0,0 +1,8 @@
|
|||||||
|
<?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
@@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="VcsDirectoryMappings">
|
||||||
|
<mapping directory="" vcs="Git" />
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
Vendored
+3
@@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"cmake.sourceDirectory": "E:/code/code/code/new/new/new/archery/cpp_ext"
|
||||||
|
}
|
||||||
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.15.15
|
version: 2.16.1
|
||||||
author: t11
|
author: t11
|
||||||
icon: ''
|
icon: ''
|
||||||
desc: t11
|
desc: t11
|
||||||
@@ -12,20 +12,18 @@ 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_270139.cvimodel
|
|
||||||
- model_270139.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
|
||||||
|
|||||||
+7
-6
@@ -76,10 +76,11 @@ class ATClient:
|
|||||||
"""
|
"""
|
||||||
expect_b = expect.encode() if isinstance(expect, str) else expect
|
expect_b = expect.encode() if isinstance(expect, str) else expect
|
||||||
with self._cmd_lock:
|
with self._cmd_lock:
|
||||||
# 初始化等待
|
with self._q_lock:
|
||||||
self._waiting = True
|
# 初始化等待
|
||||||
self._expect = expect_b
|
self._waiting = True
|
||||||
self._resp = b""
|
self._expect = expect_b
|
||||||
|
self._resp = b""
|
||||||
|
|
||||||
# 发送
|
# 发送
|
||||||
if cmd:
|
if cmd:
|
||||||
@@ -300,8 +301,8 @@ class ATClient:
|
|||||||
if len(self._rx) > 512 * 1024:
|
if len(self._rx) > 512 * 1024:
|
||||||
self._rx = self._rx[-256 * 1024:]
|
self._rx = self._rx[-256 * 1024:]
|
||||||
else:
|
else:
|
||||||
if len(self._rx) > 16384:
|
if len(self._rx) > 32768:
|
||||||
self._rx = self._rx[-4096:]
|
self._rx = self._rx[-16384:]
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,47 @@
|
|||||||
|
#!/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
|
||||||
@@ -24,7 +24,7 @@ TRIANGLE_DETECT_SCALE = 0.4
|
|||||||
# SERVER_IP = "stcp.shelingxingqiu.com"
|
# SERVER_IP = "stcp.shelingxingqiu.com"
|
||||||
SERVER_IP = "www.shelingxingqiu.com"
|
SERVER_IP = "www.shelingxingqiu.com"
|
||||||
SERVER_PORT = 50005
|
SERVER_PORT = 50005
|
||||||
HEARTBEAT_INTERVAL = 15 # 心跳间隔(秒)
|
HEARTBEAT_INTERVAL = 5 # 心跳间隔(秒)
|
||||||
|
|
||||||
# WiFi 质量评估(开机先尝试 WiFi;质量差且 4G 可用则切到 4G,本次上电直至关机锁定 4G)
|
# WiFi 质量评估(开机先尝试 WiFi;质量差且 4G 可用则切到 4G,本次上电直至关机锁定 4G)
|
||||||
WIFI_QUALITY_RTT_SAMPLES = 3 # 到业务服务器 TCP 建连耗时采样次数,取中位数
|
WIFI_QUALITY_RTT_SAMPLES = 3 # 到业务服务器 TCP 建连耗时采样次数,取中位数
|
||||||
@@ -341,7 +341,16 @@ PIN_MAPPINGS = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
# ==================== 电源配置 ====================
|
# ==================== 电源配置 ====================
|
||||||
AUTO_POWER_OFF_IN_SECONDS = 0 # 自动关机时间(秒),0表示不自动关机
|
AUTO_POWER_OFF_IN_SECONDS = 10 * 60 # 自动关机时间(秒),0表示不自动关机
|
||||||
|
|
||||||
|
# 实机数据:正常放电约为正电流,插入充电线后约为负电流。
|
||||||
|
CHARGING_SHUTDOWN_ENABLED = True # True=充电时退出应用,False=关闭充电关机功能
|
||||||
|
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 = APP_DIR + "/charging_exit.sh"
|
||||||
|
|
||||||
BATTERY_SOC_LPF_ALPHA = 0.5
|
BATTERY_SOC_LPF_ALPHA = 0.5
|
||||||
BATTERY_SOC_AVG_WINDOW = 5
|
BATTERY_SOC_AVG_WINDOW = 5
|
||||||
|
|||||||
+7
-8
@@ -29,7 +29,7 @@ class HardwareManager:
|
|||||||
self._adc_obj = None # ADC对象
|
self._adc_obj = None # ADC对象
|
||||||
self._at_client = None # AT客户端
|
self._at_client = None # AT客户端
|
||||||
|
|
||||||
self._last_active_ticks = None # 上次活跃时刻(ticks_ms,单调递增,不受校时影响)
|
self._last_active_time = 0 # 用于记录用户的最后一次活跃的时间
|
||||||
self._stop_timer = False # 用于停止定时器的标志
|
self._stop_timer = False # 用于停止定时器的标志
|
||||||
|
|
||||||
self._initialized = True
|
self._initialized = True
|
||||||
@@ -111,7 +111,7 @@ class HardwareManager:
|
|||||||
|
|
||||||
def start_idle_timer(self):
|
def start_idle_timer(self):
|
||||||
self._stop_timer = False
|
self._stop_timer = False
|
||||||
self._last_active_ticks = time.ticks_ms()
|
self._last_active_time = time.time()
|
||||||
|
|
||||||
def stop_idle_timer(self):
|
def stop_idle_timer(self):
|
||||||
self._stop_timer = True
|
self._stop_timer = True
|
||||||
@@ -119,13 +119,12 @@ class HardwareManager:
|
|||||||
def get_idle_time_in_sec(self):
|
def get_idle_time_in_sec(self):
|
||||||
if self._stop_timer:
|
if self._stop_timer:
|
||||||
return 0
|
return 0
|
||||||
if self._last_active_ticks is None:
|
diff = time.time() - self._last_active_time
|
||||||
|
if diff < 0:
|
||||||
|
# 时间可能被重置了,重新计时
|
||||||
|
self._last_active_time = time.time()
|
||||||
return 0
|
return 0
|
||||||
diff_ms = time.ticks_diff(time.ticks_ms(), self._last_active_ticks)
|
return diff
|
||||||
if diff_ms < 0:
|
|
||||||
self._last_active_ticks = time.ticks_ms()
|
|
||||||
return 0
|
|
||||||
return diff_ms / 1000.0
|
|
||||||
|
|
||||||
|
|
||||||
# 创建全局单例实例
|
# 创建全局单例实例
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ from version import VERSION
|
|||||||
# from logger import init_logging, get_logger, stop_logging
|
# from logger import init_logging, get_logger, stop_logging
|
||||||
from logger_manager import logger_manager
|
from logger_manager import logger_manager
|
||||||
from time_sync import sync_system_time_from_4g
|
from time_sync import sync_system_time_from_4g
|
||||||
from power import init_ina226
|
from power import charging_shutdown_monitor, init_ina226
|
||||||
from laser_manager import laser_manager
|
from laser_manager import laser_manager
|
||||||
from vision import start_save_shot_worker
|
from vision import start_save_shot_worker
|
||||||
from network import network_manager
|
from network import network_manager
|
||||||
@@ -122,9 +122,16 @@ def cmd_str():
|
|||||||
|
|
||||||
# 1. 初始化日志系统
|
# 1. 初始化日志系统
|
||||||
import logging
|
import logging
|
||||||
logger_manager.init_logging(log_level=logging.DEBUG)
|
logger_manager.init_logging(log_level=logging.WARNING)
|
||||||
logger = logger_manager.logger
|
logger = logger_manager.logger
|
||||||
|
|
||||||
|
# 充电关机独立读取 INA226,不依赖 TCP 连接或心跳流程。
|
||||||
|
try:
|
||||||
|
_thread.start_new_thread(charging_shutdown_monitor, ())
|
||||||
|
except Exception as e:
|
||||||
|
if logger:
|
||||||
|
logger.error(f"[CHARGE] 启动独立监测线程失败: {e}")
|
||||||
|
|
||||||
# 补充:因为初始化的时候,激光会亮,先关了它
|
# 补充:因为初始化的时候,激光会亮,先关了它
|
||||||
# laser_manager.turn_off_laser()
|
# laser_manager.turn_off_laser()
|
||||||
|
|
||||||
@@ -275,11 +282,6 @@ def cmd_str():
|
|||||||
hardware_manager.start_idle_timer()
|
hardware_manager.start_idle_timer()
|
||||||
|
|
||||||
if logger:
|
if logger:
|
||||||
_auto_power_off = int(getattr(config, "AUTO_POWER_OFF_IN_SECONDS", 0) or 0)
|
|
||||||
if _auto_power_off <= 0:
|
|
||||||
logger.info("[MAIN] 自动关机已禁用 (AUTO_POWER_OFF_IN_SECONDS=0)")
|
|
||||||
else:
|
|
||||||
logger.info(f"[MAIN] 自动关机: {_auto_power_off} 秒无活动")
|
|
||||||
logger.info("系统准备完成...")
|
logger.info("系统准备完成...")
|
||||||
|
|
||||||
last_adc_trigger = 0
|
last_adc_trigger = 0
|
||||||
@@ -288,37 +290,35 @@ def cmd_str():
|
|||||||
|
|
||||||
pressure_buf = []
|
pressure_buf = []
|
||||||
pressure_sum = 0
|
pressure_sum = 0
|
||||||
pressure_abs_sum = 0
|
|
||||||
pressure_min = 4095
|
pressure_min = 4095
|
||||||
pressure_max = 0
|
pressure_max = 0
|
||||||
pressure_t0_ms = None
|
pressure_t0_ms = None
|
||||||
last_avg_abs = 0
|
|
||||||
|
# 压力突变检测(上升沿触发)
|
||||||
|
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, pressure_abs_sum, last_avg_abs
|
nonlocal pressure_buf, pressure_sum, pressure_min, pressure_max, pressure_t0_ms, logger
|
||||||
if not pressure_buf:
|
if not pressure_buf:
|
||||||
return
|
return
|
||||||
if config.AIR_PRESSURE_lOG:
|
if config.AIR_PRESSURE_lOG:
|
||||||
t1_ms = time.ticks_ms()
|
t1_ms = time.ticks_ms()
|
||||||
n = len(pressure_buf)
|
n = len(pressure_buf)
|
||||||
avg = (pressure_sum / n) if n else 0
|
avg = (pressure_sum / n) if n else 0
|
||||||
avg_abs = (pressure_abs_sum / n) if n else 0
|
|
||||||
line = (
|
line = (
|
||||||
f"[气压批量] reason={reason} "
|
f"[气压批量] reason={reason} "
|
||||||
f"t0={pressure_t0_ms} t1={t1_ms} n={n} "
|
f"t0={pressure_t0_ms} t1={t1_ms} n={n} "
|
||||||
f"min={pressure_min} max={pressure_max} avg={avg:.1f} avg_abs={avg_abs:.3f} "
|
f"min={pressure_min} max={pressure_max} avg={avg:.1f} "
|
||||||
f"values={','.join(map(str, pressure_buf))}"
|
f"values={','.join(map(str, pressure_buf))}"
|
||||||
f" convert value (kpa): {(max(pressure_buf, key=lambda x: x[1])[1] - last_avg_abs) / (5 - 2.5) * config.AIR_PRESSURE_HARDWARE_MAX:.1f}"
|
|
||||||
)
|
)
|
||||||
if logger:
|
if logger:
|
||||||
logger.debug(line)
|
logger.debug(line)
|
||||||
else:
|
else:
|
||||||
print(line)
|
print(line)
|
||||||
last_avg_abs = avg_abs
|
|
||||||
# 无论是否记录日志,都必须清空 buffer,否则内存泄漏
|
# 无论是否记录日志,都必须清空 buffer,否则内存泄漏
|
||||||
pressure_buf = []
|
pressure_buf = []
|
||||||
pressure_sum = 0
|
pressure_sum = 0
|
||||||
pressure_abs_sum = 0
|
|
||||||
pressure_min = 4095
|
pressure_min = 4095
|
||||||
pressure_max = 0
|
pressure_max = 0
|
||||||
pressure_t0_ms = None
|
pressure_t0_ms = None
|
||||||
@@ -340,13 +340,11 @@ 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} 秒")
|
||||||
if (
|
if hardware_manager.get_idle_time_in_sec() > config.AUTO_POWER_OFF_IN_SECONDS:
|
||||||
config.AUTO_POWER_OFF_IN_SECONDS > 0
|
|
||||||
and hardware_manager.get_idle_time_in_sec() > config.AUTO_POWER_OFF_IN_SECONDS
|
|
||||||
):
|
|
||||||
logger.info("[MAIN] 超过设定时间未检测活动,自动关机")
|
logger.info("[MAIN] 超过设定时间未检测活动,自动关机")
|
||||||
network_manager.safe_enqueue({"poweroff": "超过设定时间未检测活动,自动关机"}, 2)
|
network_manager.safe_enqueue({"poweroff": "超过设定时间未检测活动,自动关机"}, 2)
|
||||||
time.sleep_ms(100)
|
time.sleep_ms(100)
|
||||||
@@ -359,12 +357,10 @@ def cmd_str():
|
|||||||
if network_manager.manual_trigger_flag:
|
if network_manager.manual_trigger_flag:
|
||||||
network_manager.clear_manual_trigger()
|
network_manager.clear_manual_trigger()
|
||||||
adc_val = config.ADC_TRIGGER_THRESHOLD + 1
|
adc_val = config.ADC_TRIGGER_THRESHOLD + 1
|
||||||
adc_abs_val = 10
|
|
||||||
if logger:
|
if logger:
|
||||||
logger.info("[TEST] TCP命令触发射箭")
|
logger.info("[TEST] TCP命令触发射箭")
|
||||||
else:
|
else:
|
||||||
adc_val = hardware_manager.adc_obj.read()
|
adc_val = hardware_manager.adc_obj.read()
|
||||||
adc_abs_val = hardware_manager.adc_obj.read_vol()
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger = logger_manager.logger
|
logger = logger_manager.logger
|
||||||
if logger:
|
if logger:
|
||||||
@@ -375,27 +371,35 @@ def cmd_str():
|
|||||||
# ====== 气压采样缓存(每次循环都记录,批量输出日志)======
|
# ====== 气压采样缓存(每次循环都记录,批量输出日志)======
|
||||||
if pressure_t0_ms is None:
|
if pressure_t0_ms is None:
|
||||||
pressure_t0_ms = current_time
|
pressure_t0_ms = current_time
|
||||||
pressure_buf.append((adc_val, adc_abs_val))
|
pressure_buf.append(adc_val)
|
||||||
pressure_sum += adc_val
|
pressure_sum += adc_val
|
||||||
pressure_abs_sum += adc_abs_val
|
|
||||||
if adc_val < pressure_min:
|
if adc_val < pressure_min:
|
||||||
pressure_min = adc_val
|
pressure_min = adc_val
|
||||||
if adc_val > pressure_max:
|
if adc_val > pressure_max:
|
||||||
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 >= 2000:
|
|
||||||
# print(f"adc :{adc_val}")
|
# 压力突变检测:当前值与前一次值的差值超过阈值时触发
|
||||||
if adc_val >= config.ADC_TRIGGER_THRESHOLD:
|
adc_diff = adc_val - prev_adc_val
|
||||||
|
if adc_diff >= ADC_JUMP_THRESHOLD and prev_adc_val > 0 :
|
||||||
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")
|
logger.info(f"[MAIN] 扳机触发过于频繁, {diff_ms}ms")
|
||||||
|
prev_adc_val = adc_val
|
||||||
continue
|
continue
|
||||||
last_adc_trigger = current_time
|
last_adc_trigger = current_time
|
||||||
# 触发前先把缓存刷出来,避免波形被长耗时处理截断
|
# 触发前先把缓存刷出来
|
||||||
_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:
|
||||||
@@ -404,17 +408,19 @@ 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)
|
|
||||||
continue
|
prev_adc_val = adc_val
|
||||||
else:
|
|
||||||
if config.SHOW_CAMERA_PHOTO_WHILE_SHOOTING:
|
# 未触发时的显示逻辑
|
||||||
try:
|
# 未触发时的显示逻辑
|
||||||
camera_manager.show(camera_manager.read_frame())
|
if config.SHOW_CAMERA_PHOTO_WHILE_SHOOTING:
|
||||||
except Exception as e:
|
try:
|
||||||
logger = logger_manager.logger
|
camera_manager.show(camera_manager.read_frame())
|
||||||
if logger:
|
except Exception as e:
|
||||||
logger.error(f"[MAIN] 显示异常: {e}")
|
logger = logger_manager.logger
|
||||||
time.sleep_ms(5)
|
if logger:
|
||||||
|
logger.error(f"[MAIN] 显示异常: {e}")
|
||||||
|
time.sleep_ms(1)
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
# 主循环的顶层异常捕获,防止程序静默退出
|
# 主循环的顶层异常捕获,防止程序静默退出
|
||||||
|
|||||||
+89
-25
@@ -474,10 +474,10 @@ class NetworkManager:
|
|||||||
def select_network(self, prefer_wifi=None):
|
def select_network(self, prefer_wifi=None):
|
||||||
"""
|
"""
|
||||||
自动选择网络(WiFi优先)
|
自动选择网络(WiFi优先)
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
prefer_wifi: 是否优先使用WiFi(None表示使用默认策略)
|
prefer_wifi: 是否优先使用WiFi(None表示使用默认策略)
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
"wifi" 或 "4g" 或 None(无可用网络)
|
"wifi" 或 "4g" 或 None(无可用网络)
|
||||||
"""
|
"""
|
||||||
@@ -624,6 +624,11 @@ class NetworkManager:
|
|||||||
password = inner_data.get("password")
|
password = inner_data.get("password")
|
||||||
ota_res_url = inner_data.get("url")
|
ota_res_url = inner_data.get("url")
|
||||||
try:
|
try:
|
||||||
|
for _f in ("/etc/wpa_supplicant.conf", "/boot/wpa_supplicant.conf", "/boot/wifi.ssid", "/boot/wifi.pass"):
|
||||||
|
try:
|
||||||
|
os.remove(_f)
|
||||||
|
except OSError:
|
||||||
|
pass
|
||||||
w = network.wifi.Wifi()
|
w = network.wifi.Wifi()
|
||||||
e = w.connect(ssid, password, wait=True, timeout=15)
|
e = w.connect(ssid, password, wait=True, timeout=15)
|
||||||
err.check_raise(e, "connect wifi failed")
|
err.check_raise(e, "connect wifi failed")
|
||||||
@@ -665,6 +670,11 @@ class NetworkManager:
|
|||||||
ssid = inner_data.get("ssid")
|
ssid = inner_data.get("ssid")
|
||||||
password = inner_data.get("password")
|
password = inner_data.get("password")
|
||||||
try:
|
try:
|
||||||
|
for _f in ("/etc/wpa_supplicant.conf", "/boot/wpa_supplicant.conf", "/boot/wifi.ssid", "/boot/wifi.pass"):
|
||||||
|
try:
|
||||||
|
os.remove(_f)
|
||||||
|
except OSError:
|
||||||
|
pass
|
||||||
w = network.wifi.Wifi()
|
w = network.wifi.Wifi()
|
||||||
e = w.connect(ssid, password, wait=True, timeout=15)
|
e = w.connect(ssid, password, wait=True, timeout=15)
|
||||||
err.check_raise(e, "connect wifi failed")
|
err.check_raise(e, "connect wifi failed")
|
||||||
@@ -678,6 +688,11 @@ class NetworkManager:
|
|||||||
},
|
},
|
||||||
2,
|
2,
|
||||||
)
|
)
|
||||||
|
self._session_force_4g = False
|
||||||
|
self.disconnect_server()
|
||||||
|
self._tcp_connected = False
|
||||||
|
self._network_type = None
|
||||||
|
self.logger.info("[conn wifi] WiFi已连接,等待主循环重新登录")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.logger.error(f"cmd600 失败: {e}")
|
self.logger.error(f"cmd600 失败: {e}")
|
||||||
self.safe_enqueue(
|
self.safe_enqueue(
|
||||||
@@ -694,10 +709,16 @@ 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)
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
bool: 是否连接成功
|
bool: 是否连接成功
|
||||||
"""
|
"""
|
||||||
@@ -706,7 +727,7 @@ class NetworkManager:
|
|||||||
if self._network_type == "wifi":
|
if self._network_type == "wifi":
|
||||||
return self._check_wifi_connection()
|
return self._check_wifi_connection()
|
||||||
elif self._network_type == "4g":
|
elif self._network_type == "4g":
|
||||||
return True # 4G连接状态由AT命令维护
|
return self._check_4g_connection()
|
||||||
return False
|
return False
|
||||||
|
|
||||||
# 自动选择网络
|
# 自动选择网络
|
||||||
@@ -723,6 +744,37 @@ class NetworkManager:
|
|||||||
return self._connect_tcp_via_4g()
|
return self._connect_tcp_via_4g()
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
def _check_4g_connection(self):
|
||||||
|
"""检查4G TCP连接是否仍然有效(通过查询PDP地址验证网络附着状态)"""
|
||||||
|
try:
|
||||||
|
atc = hardware_manager.at_client
|
||||||
|
if atc is None:
|
||||||
|
return False
|
||||||
|
if not self._uart4g_lock.acquire(timeout=3000):
|
||||||
|
# 获取锁超时说明有其他操作在进行,视为连接仍有效
|
||||||
|
return True
|
||||||
|
try:
|
||||||
|
r = atc.send("AT+CGPADDR=1", "OK", 3000)
|
||||||
|
m = re.search(r'\+CGPADDR:\s*1,"([^"]+)"', r)
|
||||||
|
ip = m.group(1) if m else ""
|
||||||
|
if ip and ip != "0.0.0.0":
|
||||||
|
return True
|
||||||
|
# 无IP或IP无效,尝试重新激活PDP
|
||||||
|
self.logger.warning("[4G-TCP] PDP地址无效,尝试重新激活")
|
||||||
|
atc.send("AT+MIPCALL=1,1", "OK", 15000)
|
||||||
|
r2 = atc.send("AT+CGPADDR=1", "OK", 3000)
|
||||||
|
m2 = re.search(r'\+CGPADDR:\s*1,"([^"]+)"', r2)
|
||||||
|
ip2 = m2.group(1) if m2 else ""
|
||||||
|
if ip2 and ip2 != "0.0.0.0":
|
||||||
|
return True
|
||||||
|
self.logger.error("[4G-TCP] 重新激活PDP仍无有效IP,连接已断开")
|
||||||
|
return False
|
||||||
|
finally:
|
||||||
|
self._uart4g_lock.release()
|
||||||
|
except Exception as e:
|
||||||
|
self.logger.warning(f"[4G-TCP] 连接检查异常: {e}")
|
||||||
|
return True # 异常时不误判断线
|
||||||
|
|
||||||
def _wrap_wifi_tls(self, plain_sock, hostname):
|
def _wrap_wifi_tls(self, plain_sock, hostname):
|
||||||
"""
|
"""
|
||||||
在已建立的 TCP socket 上做 TLS(WiFi 走主机 ssl 库;4G 仍用模组 AT+SSL)。
|
在已建立的 TCP socket 上做 TLS(WiFi 走主机 ssl 库;4G 仍用模组 AT+SSL)。
|
||||||
@@ -849,6 +901,12 @@ 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:
|
||||||
@@ -973,11 +1031,11 @@ class NetworkManager:
|
|||||||
def tcp_send_raw(self, data: bytes, max_retries=2) -> bool:
|
def tcp_send_raw(self, data: bytes, max_retries=2) -> bool:
|
||||||
"""
|
"""
|
||||||
统一的TCP发送接口(自动选择WiFi或4G)
|
统一的TCP发送接口(自动选择WiFi或4G)
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
data: 要发送的数据
|
data: 要发送的数据
|
||||||
max_retries: 最大重试次数
|
max_retries: 最大重试次数
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
bool: 是否发送成功
|
bool: 是否发送成功
|
||||||
"""
|
"""
|
||||||
@@ -1070,6 +1128,8 @@ class NetworkManager:
|
|||||||
total += n
|
total += n
|
||||||
|
|
||||||
hardware_manager.uart4g.write(b"\x1A")
|
hardware_manager.uart4g.write(b"\x1A")
|
||||||
|
with hardware_manager.at_client._q_lock:
|
||||||
|
hardware_manager.at_client._rx = b""
|
||||||
r = hardware_manager.at_client.send("", "OK", 8000)
|
r = hardware_manager.at_client.send("", "OK", 8000)
|
||||||
if ("SEND OK" in r) or ("OK" in r) or ("+MIPSEND" in r):
|
if ("SEND OK" in r) or ("OK" in r) or ("+MIPSEND" in r):
|
||||||
return True
|
return True
|
||||||
@@ -1148,10 +1208,10 @@ class NetworkManager:
|
|||||||
def receive_tcp_data_via_wifi(self, timeout_ms=100):
|
def receive_tcp_data_via_wifi(self, timeout_ms=100):
|
||||||
"""
|
"""
|
||||||
通过WiFi接收TCP数据
|
通过WiFi接收TCP数据
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
timeout_ms: 超时时间(毫秒)
|
timeout_ms: 超时时间(毫秒)
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
bytes: 接收到的数据,如果没有数据则返回 b""
|
bytes: 接收到的数据,如果没有数据则返回 b""
|
||||||
"""
|
"""
|
||||||
@@ -1162,6 +1222,14 @@ 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:
|
||||||
@@ -1189,7 +1257,7 @@ class NetworkManager:
|
|||||||
def _upload_log_file(self, upload_url, wifi_ssid=None, wifi_password=None, include_rotated=True, max_files=None,
|
def _upload_log_file(self, upload_url, wifi_ssid=None, wifi_password=None, include_rotated=True, max_files=None,
|
||||||
archive_format="tgz"):
|
archive_format="tgz"):
|
||||||
"""上传日志文件到指定URL
|
"""上传日志文件到指定URL
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
upload_url: 上传目标URL,例如 "https://example.com/upload/"
|
upload_url: 上传目标URL,例如 "https://example.com/upload/"
|
||||||
wifi_ssid: WiFi SSID(可选,如果未连接WiFi则尝试连接)
|
wifi_ssid: WiFi SSID(可选,如果未连接WiFi则尝试连接)
|
||||||
@@ -1197,7 +1265,7 @@ class NetworkManager:
|
|||||||
include_rotated: 是否包含轮转日志(app.log.1 等)
|
include_rotated: 是否包含轮转日志(app.log.1 等)
|
||||||
max_files: 最多打包多少个日志文件(包含 app.log 本身),None=按 backupCount 自动推断
|
max_files: 最多打包多少个日志文件(包含 app.log 本身),None=按 backupCount 自动推断
|
||||||
archive_format: 打包格式:tgz 或 zip
|
archive_format: 打包格式:tgz 或 zip
|
||||||
|
|
||||||
Note:
|
Note:
|
||||||
该功能仅在 WiFi 连接时可用,4G 网络暂不支持文件上传
|
该功能仅在 WiFi 连接时可用,4G 网络暂不支持文件上传
|
||||||
"""
|
"""
|
||||||
@@ -1762,16 +1830,9 @@ 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
|
||||||
@@ -1784,7 +1845,7 @@ class NetworkManager:
|
|||||||
continue
|
continue
|
||||||
|
|
||||||
if not self.connect_server():
|
if not self.connect_server():
|
||||||
time.sleep_ms(5000)
|
time.sleep_ms(1000)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# 发送登录包
|
# 发送登录包
|
||||||
@@ -1805,7 +1866,7 @@ class NetworkManager:
|
|||||||
self.disconnect_server()
|
self.disconnect_server()
|
||||||
except:
|
except:
|
||||||
pass
|
pass
|
||||||
time.sleep_ms(2000)
|
time.sleep_ms(500)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
self.logger.info("➡️ 登录包已发送,等待确认...")
|
self.logger.info("➡️ 登录包已发送,等待确认...")
|
||||||
@@ -2148,7 +2209,7 @@ class NetworkManager:
|
|||||||
self.logger.info(f"password: {password}")
|
self.logger.info(f"password: {password}")
|
||||||
ota_manager._start_update_thread()
|
ota_manager._start_update_thread()
|
||||||
self._spawn_cmd_thread(ota_manager.handle_wifi_and_update,
|
self._spawn_cmd_thread(ota_manager.handle_wifi_and_update,
|
||||||
(ssid, password, ota_url))
|
(ssid, password, ota_url))
|
||||||
elif inner_cmd == 6:
|
elif inner_cmd == 6:
|
||||||
try:
|
try:
|
||||||
ip = os.popen(
|
ip = os.popen(
|
||||||
@@ -2249,7 +2310,8 @@ class NetworkManager:
|
|||||||
item_is_high = False
|
item_is_high = False
|
||||||
|
|
||||||
if item:
|
if item:
|
||||||
msg_type, data_dict = item
|
msg_type, data_dict = item[:2]
|
||||||
|
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):
|
||||||
# 发送失败:将消息放回队首(队列满则丢弃)
|
# 发送失败:将消息放回队首(队列满则丢弃)
|
||||||
@@ -2266,6 +2328,8 @@ class NetworkManager:
|
|||||||
except:
|
except:
|
||||||
pass
|
pass
|
||||||
break
|
break
|
||||||
|
if sent_event is not None:
|
||||||
|
sent_event.set()
|
||||||
|
|
||||||
# 发送激光校准结果
|
# 发送激光校准结果
|
||||||
if logged_in:
|
if logged_in:
|
||||||
@@ -2294,8 +2358,8 @@ class NetworkManager:
|
|||||||
pass
|
pass
|
||||||
break
|
break
|
||||||
else:
|
else:
|
||||||
# 不立即断开,让下一轮心跳再试;同时缩短一点等待,提升恢复速度
|
# 不立即断开,让下一轮心跳再试
|
||||||
time.sleep_ms(200)
|
time.sleep_ms(50)
|
||||||
continue
|
continue
|
||||||
else:
|
else:
|
||||||
send_hartbeat_fail_count = 0
|
send_hartbeat_fail_count = 0
|
||||||
@@ -2325,8 +2389,8 @@ class NetworkManager:
|
|||||||
self._send_event.clear()
|
self._send_event.clear()
|
||||||
|
|
||||||
self._tcp_connected = False
|
self._tcp_connected = False
|
||||||
self.logger.error("连接异常,2秒后重连...")
|
self.logger.error("连接异常,50ms后重连...")
|
||||||
time.sleep_ms(200)
|
time.sleep_ms(50)
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
# TCP主循环的顶层异常捕获,防止线程静默退出
|
# TCP主循环的顶层异常捕获,防止线程静默退出
|
||||||
|
|||||||
@@ -5,6 +5,8 @@
|
|||||||
提供电压、电流监测和充电状态检测
|
提供电压、电流监测和充电状态检测
|
||||||
"""
|
"""
|
||||||
import config
|
import config
|
||||||
|
import os
|
||||||
|
import subprocess
|
||||||
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
|
||||||
|
|
||||||
@@ -85,7 +87,7 @@ def get_bus_voltage():
|
|||||||
def get_current():
|
def get_current():
|
||||||
"""
|
"""
|
||||||
读取电流(单位:mA)
|
读取电流(单位:mA)
|
||||||
正数表示充电,负数表示放电
|
当前电源板实测:正数表示放电,负数表示充电。
|
||||||
|
|
||||||
INA226 电流计算公式:
|
INA226 电流计算公式:
|
||||||
Current = (Current Register Value) × Current_LSB
|
Current = (Current Register Value) × Current_LSB
|
||||||
@@ -96,13 +98,13 @@ def get_current():
|
|||||||
return 0.0
|
return 0.0
|
||||||
raw = read_register(config.REG_CURRENT)
|
raw = read_register(config.REG_CURRENT)
|
||||||
# INA226 电流寄存器是16位有符号整数
|
# INA226 电流寄存器是16位有符号整数
|
||||||
# 最高位是符号位:0=正(充电),1=负(放电)
|
# 最高位是符号位;电流方向含义取决于电源板的采样电阻接线方向。
|
||||||
# 计算 Current_LSB(根据 CALIBRATION_VALUE)
|
# 计算 Current_LSB(根据 CALIBRATION_VALUE)
|
||||||
current_lsb = 0.001 * config.CALIBRATION_VALUE / 4096 # 单位:A
|
current_lsb = 0.001 * config.CALIBRATION_VALUE / 4096 # 单位:A
|
||||||
# 处理有符号数:如果最高位为1,转换为负数
|
# 处理有符号数:如果最高位为1,转换为负数
|
||||||
if raw & 0x8000: # 最高位为1,表示负数(放电)
|
if raw & 0x8000:
|
||||||
signed_raw = raw - 0x10000 # 转换为有符号整数
|
signed_raw = raw - 0x10000 # 转换为有符号整数
|
||||||
else: # 最高位为0,表示正数(充电)
|
else:
|
||||||
signed_raw = raw
|
signed_raw = raw
|
||||||
# 转换为毫安
|
# 转换为毫安
|
||||||
current_ma = signed_raw * current_lsb * 1000
|
current_ma = signed_raw * current_lsb * 1000
|
||||||
@@ -129,7 +131,7 @@ def is_charging(threshold_ma=10.0):
|
|||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
current = get_current()
|
current = get_current()
|
||||||
is_charge = current > threshold_ma
|
is_charge = current < -abs(float(threshold_ma))
|
||||||
return is_charge
|
return is_charge
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger = logger_manager.logger
|
logger = logger_manager.logger
|
||||||
@@ -140,6 +142,115 @@ 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):
|
||||||
"""
|
"""
|
||||||
根据电压估算电池百分比(高密度查表插值 + 滤波)。
|
根据电压估算电池百分比(高密度查表插值 + 滤波)。
|
||||||
|
|||||||
+1
-1
@@ -320,8 +320,8 @@ def process_shot(adc_val):
|
|||||||
logger = logger_manager.logger
|
logger = logger_manager.logger
|
||||||
|
|
||||||
try:
|
try:
|
||||||
network_manager.safe_enqueue({"shoot_event": "start"}, msg_type=2, high=True)
|
|
||||||
frame = camera_manager.read_frame()
|
frame = camera_manager.read_frame()
|
||||||
|
network_manager.safe_enqueue({"shoot_event": "start"}, msg_type=2, high=True)
|
||||||
|
|
||||||
# 调用算法分析
|
# 调用算法分析
|
||||||
analysis_result = analyze_shot(frame)
|
analysis_result = analyze_shot(frame)
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,144 @@
|
|||||||
|
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()
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
#!/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
|
||||||
@@ -0,0 +1,139 @@
|
|||||||
|
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()
|
||||||
+11
-1
@@ -26,4 +26,14 @@
|
|||||||
# 2.15.13 优化算法
|
# 2.15.13 优化算法
|
||||||
# 2.15.14 优化算法
|
# 2.15.14 优化算法
|
||||||
# 2.15.15 优化wifi连接
|
# 2.15.15 优化wifi连接
|
||||||
# 2.15.16 修复不关机,空闲计时改用 ticks_ms(不受校时影响),启动时打印自动关机配置
|
# 2.15.16 修复wifi连接问题
|
||||||
|
# 2.15.17 修复wifi连接问题
|
||||||
|
# 2.15.18 wifi连接成功重新登录
|
||||||
|
# 2.15.20 加了充电关机,激光也同时关闭
|
||||||
|
# 2.15.21 测试4g 扩大了缓存池和改了心跳时间
|
||||||
|
# 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.15.16'
|
VERSION = '2.16.1'
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -631,7 +631,7 @@ def detect_circle_v3(frame, laser_point=None, img_cv=None):
|
|||||||
min_r = min(rc["radius"], yellow_radius)
|
min_r = min(rc["radius"], yellow_radius)
|
||||||
max_r = max(rc["radius"], yellow_radius)
|
max_r = max(rc["radius"], yellow_radius)
|
||||||
size_ratio = min_r / max_r if max_r > 0 else 0
|
size_ratio = min_r / max_r if max_r > 0 else 0
|
||||||
if dist_centers < max_dist and size_ratio > 0.5:
|
if dist_centers < max_dist and size_ratio >= 0.3:
|
||||||
if logger:
|
if logger:
|
||||||
logger.info(f"[target] -> 找到匹配的红圈: 黄心({yellow_center}), "
|
logger.info(f"[target] -> 找到匹配的红圈: 黄心({yellow_center}), "
|
||||||
f"红心({rc['center']}), 距离:{dist_centers:.1f}, "
|
f"红心({rc['center']}), 距离:{dist_centers:.1f}, "
|
||||||
|
|||||||
@@ -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 秒测量一次 RTT 和 RSSI)
|
启动 WiFi 质量后台监测线程(每 5 秒检查 STA 关联状态和 RSSI)
|
||||||
只在 WiFi 连接时运行,不影响业务发送性能
|
只在 WiFi 连接时运行,不影响业务发送性能
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
@@ -591,34 +591,38 @@ class WiFiManager:
|
|||||||
def _quality_monitor_loop(self):
|
def _quality_monitor_loop(self):
|
||||||
"""
|
"""
|
||||||
WiFi 质量监测循环(后台线程)
|
WiFi 质量监测循环(后台线程)
|
||||||
每 5 秒测量一次 RTT 和 RSSI,发现质量差则触发切换
|
每 5 秒检查 STA 关联状态和 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(1 个样本,快速测量)
|
# RTT 测量当前禁用;STA 关联状态用于判断物理 WiFi 链路是否仍存在。
|
||||||
# rtt_ms, reachable = self._measure_wifi_tcp_rtt_ms(
|
# 不能把禁用的 RTT 伪装成 0ms,否则关闭热点后会一直被判为正常。
|
||||||
# self._server_ip, self._server_port,
|
reachable = self.is_sta_associated()
|
||||||
# samples=1, per_sample_timeout_ms=600
|
rtt_ms = None
|
||||||
# )
|
|
||||||
|
|
||||||
# 获取 RSSI
|
# 获取 RSSI
|
||||||
rssi_dbm = self._get_wifi_rssi_dbm()
|
rssi_dbm = self._get_wifi_rssi_dbm()
|
||||||
|
|
||||||
# 更新缓存
|
# 更新缓存
|
||||||
# 不使用 RTT 测量
|
self._last_wifi_rtt_ms = rtt_ms
|
||||||
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(f"[WiFi Monitor] - RTT={rtt_ms:.0f}ms, RSSI={_rssi_s}dBm")
|
self.logger.debug(
|
||||||
|
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) or (_rtt is None) or (_rtt == float("inf")):
|
if not _reachable:
|
||||||
|
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)
|
||||||
|
|
||||||
@@ -628,13 +632,8 @@ class WiFiManager:
|
|||||||
|
|
||||||
for retry_idx in range(2):
|
for retry_idx in range(2):
|
||||||
time.sleep_ms(1000)
|
time.sleep_ms(1000)
|
||||||
# 不使用 RTT 测量
|
reachable2 = self.is_sta_associated()
|
||||||
rtt2 = 0
|
rtt2 = None
|
||||||
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()
|
||||||
|
|
||||||
# 更新缓存,便于外部查看最新状态
|
# 更新缓存,便于外部查看最新状态
|
||||||
@@ -643,14 +642,10 @@ class WiFiManager:
|
|||||||
|
|
||||||
bad2 = _is_bad_now(reachable2, rtt2, rssi2)
|
bad2 = _is_bad_now(reachable2, rtt2, rssi2)
|
||||||
try:
|
try:
|
||||||
_rtt_disp = (
|
_rtt_disp = f"{rtt2:.0f}ms" if rtt2 is not None else "n/a"
|
||||||
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:.0f}ms, rssi={rssi2}, bad={bad2}"
|
f"rtt={_rtt_disp}, rssi={rssi2}, bad={bad2}"
|
||||||
)
|
)
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|||||||
Reference in New Issue
Block a user