tiandytdd
This commit is contained in:
1 parent
ddea6d0578
commit
5746fa10db
5 files changed
+122
-20
No files matched your search
@@ -28,6 +28,11 @@
|
||||
这是当前型号的控制方式,不是标定程序预先排了多个扫描点,也不能单凭这个现象判定电机故障。
|
||||
标定时应等待程序自己完成到位回读,不要同时在Web端控制云台。
|
||||
|
||||
`interactive.sh`启动时会先执行`docker exec smart-cam smart-camctl control pause`,
|
||||
同时停止扫图、巡航和自动追踪对云台的新控制。向导正常完成、发生错误或用户
|
||||
按`Ctrl+C`退出时,Shell的`EXIT` trap都会执行`smart-camctl control resume`,
|
||||
恢复巡航和自动追踪。
|
||||
|
||||
## 目录结构
|
||||
|
||||
```text
|
||||
@@ -295,8 +300,8 @@ zoom
|
||||
2. 只更新`scan`中的七个运动参数,保留定时时间、等待时间、容差和HTTP设置;
|
||||
3. 用`python3 -m json.tool`验证新JSON;
|
||||
4. 原子替换配置,不另外备份原配置;
|
||||
5. 执行`docker restart smart-cam`;
|
||||
6. 等待scan HTTP服务重新可用。
|
||||
5. 若宿主机存在`smart-cam.service`,通过systemd重启,避免systemd与`docker restart`同时管理容器;否则执行`docker restart smart-cam`;
|
||||
6. 等待scan HTTP服务连续稳定15秒后再进入试扫。
|
||||
|
||||
回复`3`不会替换配置。
|
||||
|
||||
|
||||
+45
-1
@@ -2,4 +2,48 @@
|
||||
set -euo pipefail
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
export PYTHONDONTWRITEBYTECODE=1
|
||||
exec python3 "$SCRIPT_DIR/interactive_calibrator.py"
|
||||
|
||||
control_paused=false
|
||||
wait_for_smart_cam() {
|
||||
local deadline=$((SECONDS + 180))
|
||||
local stable_checks=0
|
||||
while (( SECONDS < deadline )); do
|
||||
if docker inspect -f '{{.State.Running}}' smart-cam 2>/dev/null | grep -qx true \
|
||||
&& docker exec smart-cam curl -fsS --max-time 5 -o /dev/null \
|
||||
http://127.0.0.1:18080/health 2>/dev/null; then
|
||||
stable_checks=$((stable_checks + 1))
|
||||
if (( stable_checks >= 8 )); then
|
||||
return 0
|
||||
fi
|
||||
else
|
||||
stable_checks=0
|
||||
fi
|
||||
sleep 2
|
||||
done
|
||||
return 1
|
||||
}
|
||||
|
||||
resume_control() {
|
||||
local exit_code=$?
|
||||
trap - EXIT
|
||||
if [[ "$control_paused" == true ]]; then
|
||||
printf '\n正在恢复smart-cam巡航和自动追踪……\n'
|
||||
if ! wait_for_smart_cam \
|
||||
|| ! docker exec smart-cam smart-camctl control resume; then
|
||||
printf '错误:标定已结束,但自动恢复失败。请立即手动执行:\n' >&2
|
||||
printf ' docker exec smart-cam smart-camctl control resume\n' >&2
|
||||
exit_code=1
|
||||
fi
|
||||
fi
|
||||
exit "$exit_code"
|
||||
}
|
||||
trap resume_control EXIT
|
||||
|
||||
printf '正在暂停smart-cam的扫图、巡航和自动追踪,准备独占云台……\n'
|
||||
if ! docker exec smart-cam smart-camctl control pause; then
|
||||
printf '错误:无法暂停smart-cam自动控制,本次不启动标定。\n' >&2
|
||||
exit 1
|
||||
fi
|
||||
control_paused=true
|
||||
|
||||
python3 "$SCRIPT_DIR/interactive_calibrator.py"
|
||||
+33
-13
@@ -10,6 +10,7 @@ from datetime import datetime
|
||||
import io
|
||||
import json
|
||||
import math
|
||||
import os
|
||||
from pathlib import Path
|
||||
import re
|
||||
import select
|
||||
@@ -906,16 +907,33 @@ def deploy_config(scan: dict) -> None:
|
||||
finally:
|
||||
local_temp.unlink(missing_ok=True)
|
||||
|
||||
subprocess.run(["docker", "restart", "smart-cam"], check=True,
|
||||
stdout=subprocess.DEVNULL)
|
||||
deadline = time.monotonic() + 90
|
||||
while time.monotonic() < deadline:
|
||||
try:
|
||||
core.api("GET", "/api/v1/scan/status", timeout=5)
|
||||
return
|
||||
except (SystemExit, subprocess.SubprocessError):
|
||||
time.sleep(2)
|
||||
raise RuntimeError("smart-cam重启后90秒内scan HTTP服务未就绪")
|
||||
unit = subprocess.run(
|
||||
["systemctl", "show", "-p", "LoadState", "--value", "smart-cam.service"],
|
||||
text=True, capture_output=True,
|
||||
)
|
||||
if unit.returncode == 0 and unit.stdout.strip() == "loaded":
|
||||
restart = ["systemctl", "restart", "smart-cam.service"]
|
||||
if os.geteuid() != 0:
|
||||
restart = ["sudo", "-n", *restart]
|
||||
result = subprocess.run(restart, text=True, capture_output=True)
|
||||
if result.returncode:
|
||||
detail = result.stderr.strip() or result.stdout.strip()
|
||||
raise RuntimeError(
|
||||
"宿主机由smart-cam.service管理容器,但无法通过systemd重启。"
|
||||
"请使用root执行本向导,或为systemctl restart smart-cam.service配置sudo权限。"
|
||||
f"\n{detail}"
|
||||
)
|
||||
else:
|
||||
subprocess.run(
|
||||
["docker", "restart", "smart-cam"], check=True,
|
||||
stdout=subprocess.DEVNULL,
|
||||
)
|
||||
|
||||
if not core.wait_for_scan_http_stable(timeout=180, stable_seconds=15):
|
||||
raise RuntimeError(
|
||||
"smart-cam重启后未能连续稳定运行15秒。请执行 "
|
||||
"systemctl status smart-cam.service 和 docker logs --tail 100 smart-cam 查因。"
|
||||
)
|
||||
|
||||
|
||||
def run_test_scan() -> dict:
|
||||
@@ -1068,9 +1086,11 @@ def acquire_calibration_control() -> bool:
|
||||
say("已临时暂停巡航;标定结束或正常中断时会自动恢复。", color=YELLOW)
|
||||
|
||||
try:
|
||||
# This is a synchronization barrier: GET waits behind any PTZ command
|
||||
# already in flight when patrol was paused, then claims manual control.
|
||||
core.get_ptz()
|
||||
# This synchronization barrier waits behind the one PTZ command that
|
||||
# was already in flight when patrol was paused. Tiandy emulated
|
||||
# absolute moves can legitimately take up to 135 seconds.
|
||||
say("正在等待已暂停巡航的最后一条PTZ指令结束……")
|
||||
core.get_ptz(timeout=160)
|
||||
except BaseException:
|
||||
if should_resume:
|
||||
try:
|
||||
|
||||
+24
-3
@@ -58,7 +58,7 @@ def transient_api_failure(result: subprocess.CompletedProcess[str]) -> bool:
|
||||
if result.returncode in TRANSIENT_CURL_EXIT_CODES:
|
||||
return True
|
||||
detail = f"{result.stdout}\n{result.stderr}".lower()
|
||||
return any(marker in detail for marker in (
|
||||
return bool(re.search(r"container(?:\s+[0-9a-f]+)?\s+is not running", detail)) or any(marker in detail for marker in (
|
||||
"container is not running",
|
||||
"container is restarting",
|
||||
"no such container",
|
||||
@@ -138,6 +138,25 @@ def wait_for_scan_http(timeout: float = API_RECOVERY_TIMEOUT) -> bool:
|
||||
return False
|
||||
|
||||
|
||||
def wait_for_scan_http_stable(
|
||||
timeout: float = 180.0, stable_seconds: float = 15.0
|
||||
) -> bool:
|
||||
"""Wait until scan HTTP stays healthy across the systemd restart window."""
|
||||
deadline = time.monotonic() + timeout
|
||||
ready_since: float | None = None
|
||||
while time.monotonic() < deadline:
|
||||
now = time.monotonic()
|
||||
if scan_http_ready():
|
||||
if ready_since is None:
|
||||
ready_since = now
|
||||
elif now - ready_since >= stable_seconds:
|
||||
return True
|
||||
else:
|
||||
ready_since = None
|
||||
time.sleep(API_RECOVERY_POLL_INTERVAL)
|
||||
return False
|
||||
|
||||
|
||||
def api(method: str, path: str, body: dict | None = None, timeout: int = 60) -> dict:
|
||||
global _HELPER_READY
|
||||
command = api_command(method, path, body, timeout)
|
||||
@@ -218,8 +237,10 @@ def parse_ptz_payload(payload: dict) -> dict[str, int]:
|
||||
return result
|
||||
|
||||
|
||||
def get_ptz() -> dict[str, int]:
|
||||
payload = api("GET", "/api/v1/ptz?source=scan_calibrator", timeout=50)
|
||||
def get_ptz(timeout: int = 50) -> dict[str, int]:
|
||||
payload = api(
|
||||
"GET", "/api/v1/ptz?source=scan_calibrator", timeout=timeout
|
||||
)
|
||||
return parse_ptz_payload(payload)
|
||||
|
||||
|
||||
|
||||
@@ -51,6 +51,18 @@ class ApiRecoveryTests(unittest.TestCase):
|
||||
self.assertEqual(payload["scan"]["state"], "idle")
|
||||
wait_mock.assert_called_once_with()
|
||||
|
||||
def test_container_id_not_running_is_transient(self):
|
||||
result = completed(
|
||||
1,
|
||||
stderr=(
|
||||
"Error response from daemon: container "
|
||||
"181a3df5ec75c1ed7183528c758078fee72b5f23505e18dd25b46789a01efac2 "
|
||||
"is not running"
|
||||
),
|
||||
)
|
||||
|
||||
self.assertTrue(core.transient_api_failure(result))
|
||||
|
||||
def test_move_not_reached_error_is_explained_for_operator(self):
|
||||
result = completed(
|
||||
22,
|
||||
@@ -230,7 +242,7 @@ class InteractiveWizardTests(unittest.TestCase):
|
||||
"POST", "/api/v1/patrol/stop",
|
||||
{"source": "scan_calibrator"}, timeout=15,
|
||||
)
|
||||
get_ptz_mock.assert_called_once_with()
|
||||
get_ptz_mock.assert_called_once_with(timeout=160)
|
||||
|
||||
def test_manual_ptz_accepts_annotated_display_format(self):
|
||||
self.assertEqual(
|
||||
|
||||
Reference in new issue
Block a user