-
This commit is contained in:
@@ -21,8 +21,7 @@ createApp({
|
|||||||
errorMsg: '',
|
errorMsg: '',
|
||||||
waitingStep: false,
|
waitingStep: false,
|
||||||
stepLabel: '',
|
stepLabel: '',
|
||||||
// 任务步骤控制开关
|
// 任务步骤控制开关(机械臂初始化并入AGV移动)
|
||||||
armInitEnabled: true,
|
|
||||||
agvMoveEnabled: true,
|
agvMoveEnabled: true,
|
||||||
qrScanEnabled: true,
|
qrScanEnabled: true,
|
||||||
frontPhotoEnabled: true,
|
frontPhotoEnabled: true,
|
||||||
@@ -121,7 +120,6 @@ createApp({
|
|||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
arm_init: this.armInitEnabled,
|
|
||||||
agv_move: this.agvMoveEnabled,
|
agv_move: this.agvMoveEnabled,
|
||||||
qr_scan: this.qrScanEnabled,
|
qr_scan: this.qrScanEnabled,
|
||||||
front_photo: this.frontPhotoEnabled,
|
front_photo: this.frontPhotoEnabled,
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>运行监控 - AGV 拍摄系统</title>
|
<title>运行监控 - AGV 拍摄系统</title>
|
||||||
<link rel="stylesheet" href="/static/css/style.css?v=20260527a">
|
<link rel="stylesheet" href="/static/css/style.css?v=20260527b">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="app">
|
<div id="app">
|
||||||
@@ -53,21 +53,13 @@
|
|||||||
<h2>🎛️ 任务步骤控制</h2>
|
<h2>🎛️ 任务步骤控制</h2>
|
||||||
<p class="hint" style="margin-bottom:12px">关闭的步骤将在本次任务中跳过</p>
|
<p class="hint" style="margin-bottom:12px">关闭的步骤将在本次任务中跳过</p>
|
||||||
<div class="toggle-grid">
|
<div class="toggle-grid">
|
||||||
<div class="toggle-item">
|
|
||||||
<label class="toggle-switch">
|
|
||||||
<input type="checkbox" v-model="armInitEnabled">
|
|
||||||
<span class="toggle-slider"></span>
|
|
||||||
</label>
|
|
||||||
<span class="toggle-label">🦾 机械臂位置初始化</span>
|
|
||||||
<span class="toggle-hint">移动到每个点位前恢复初始姿态</span>
|
|
||||||
</div>
|
|
||||||
<div class="toggle-item">
|
<div class="toggle-item">
|
||||||
<label class="toggle-switch">
|
<label class="toggle-switch">
|
||||||
<input type="checkbox" v-model="agvMoveEnabled">
|
<input type="checkbox" v-model="agvMoveEnabled">
|
||||||
<span class="toggle-slider"></span>
|
<span class="toggle-slider"></span>
|
||||||
</label>
|
</label>
|
||||||
<span class="toggle-label">🚗 AGV移动</span>
|
<span class="toggle-label">🚗 AGV移动</span>
|
||||||
<span class="toggle-hint">按之字形路线移动到各点位</span>
|
<span class="toggle-hint">含机械臂初始化,按之字形路线移动到各点位</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="toggle-item">
|
<div class="toggle-item">
|
||||||
<label class="toggle-switch">
|
<label class="toggle-switch">
|
||||||
@@ -122,8 +114,8 @@
|
|||||||
</section>
|
</section>
|
||||||
|
|
||||||
<!-- 实时日志 -->
|
<!-- 实时日志 -->
|
||||||
<section class="card" v-if="missionState === 'running' || missionState === 'waiting_qr'">
|
<section class="card">
|
||||||
<h2>📜 实时日志</h2>
|
<h2>📜 任务日志</h2>
|
||||||
<div class="log-box" ref="logBox">
|
<div class="log-box" ref="logBox">
|
||||||
<div v-for="(log, i) in logs" :key="i" class="log-line">[[ log ]]</div>
|
<div v-for="(log, i) in logs" :key="i" class="log-line">[[ log ]]</div>
|
||||||
<div v-if="logs.length === 0" class="log-empty">等待任务开始...</div>
|
<div v-if="logs.length === 0" class="log-empty">等待任务开始...</div>
|
||||||
@@ -197,6 +189,6 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script src="/static/js/vue3.global.prod.js"></script>
|
<script src="/static/js/vue3.global.prod.js"></script>
|
||||||
<script src="/static/js/running.js?v=20260527a"></script>
|
<script src="/static/js/running.js?v=20260527b"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
@@ -23,6 +23,8 @@ import requests
|
|||||||
import cv2
|
import cv2
|
||||||
import numpy as np
|
import numpy as np
|
||||||
|
|
||||||
|
from utils.nav2_navigator import Nav2Navigator, Nav2Status
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
ROS2_SETUP_CMD = "source /opt/ros/humble/setup.bash && source ~/agv_pro_ros2/install/setup.bash"
|
ROS2_SETUP_CMD = "source /opt/ros/humble/setup.bash && source ~/agv_pro_ros2/install/setup.bash"
|
||||||
@@ -102,6 +104,9 @@ class MissionExecutorV3:
|
|||||||
baudrate=config.get("baudrate", 1000000)
|
baudrate=config.get("baudrate", 1000000)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Nav2 导航器(直接使用 rclpy BasicNavigator API,比 subprocess 更可靠)
|
||||||
|
self._nav = Nav2Navigator()
|
||||||
|
|
||||||
# ==================== 连接 ====================
|
# ==================== 连接 ====================
|
||||||
|
|
||||||
def connect_all(self) -> Dict[str, bool]:
|
def connect_all(self) -> Dict[str, bool]:
|
||||||
@@ -204,10 +209,13 @@ class MissionExecutorV3:
|
|||||||
options = {}
|
options = {}
|
||||||
opt_arm_init = options.get("arm_init", True)
|
opt_arm_init = options.get("arm_init", True)
|
||||||
opt_agv_move = options.get("agv_move", True)
|
opt_agv_move = options.get("agv_move", True)
|
||||||
|
# 机械臂初始化并入 AGV 移动:只要开移动,就先初始化机械臂
|
||||||
|
if opt_agv_move:
|
||||||
|
opt_arm_init = True
|
||||||
opt_qr_scan = options.get("qr_scan", True)
|
opt_qr_scan = options.get("qr_scan", True)
|
||||||
opt_front_photo = options.get("front_photo", True)
|
opt_front_photo = options.get("front_photo", True)
|
||||||
opt_back_photo = options.get("back_photo", True)
|
opt_back_photo = options.get("back_photo", True)
|
||||||
self._log(f"⚙️ 任务步骤: arm_init={opt_arm_init}, agv_move={opt_agv_move}, "
|
self._log(f"⚙️ 任务步骤: agv_move={opt_agv_move}, "
|
||||||
f"qr_scan={opt_qr_scan}, front={opt_front_photo}, back={opt_back_photo}")
|
f"qr_scan={opt_qr_scan}, front={opt_front_photo}, back={opt_back_photo}")
|
||||||
|
|
||||||
# 机械臂初始姿态(AGV 移动前恢复)
|
# 机械臂初始姿态(AGV 移动前恢复)
|
||||||
@@ -228,10 +236,11 @@ class MissionExecutorV3:
|
|||||||
# 恢复机械臂初始姿态
|
# 恢复机械臂初始姿态
|
||||||
if opt_arm_init and has_arm_pose:
|
if opt_arm_init and has_arm_pose:
|
||||||
self._log(" 🦾 恢复机械臂初始姿态")
|
self._log(" 🦾 恢复机械臂初始姿态")
|
||||||
|
try:
|
||||||
self.arm_client.set_angles(arm_initial_pose, speed=500)
|
self.arm_client.set_angles(arm_initial_pose, speed=500)
|
||||||
time.sleep(2)
|
time.sleep(2)
|
||||||
elif not opt_arm_init:
|
except Exception as e:
|
||||||
self._log(" ⏭️ 跳过机械臂位置初始化")
|
self._log(f" ⚠️ 机械臂初始化失败: {e}")
|
||||||
|
|
||||||
# 更新任务状态 → 正面开始
|
# 更新任务状态 → 正面开始
|
||||||
task = self._get_task(r, c)
|
task = self._get_task(r, c)
|
||||||
@@ -300,6 +309,14 @@ class MissionExecutorV3:
|
|||||||
self._step(f"机器 {rl}-{cl} 背面")
|
self._step(f"机器 {rl}-{cl} 背面")
|
||||||
|
|
||||||
# 导航到背面点位
|
# 导航到背面点位
|
||||||
|
# 导航到背面点位之前:恢复机械臂初始姿态
|
||||||
|
if opt_agv_move and has_arm_pose:
|
||||||
|
self._log(" 🦾 恢复机械臂初始姿态(背面移动前)")
|
||||||
|
try:
|
||||||
|
self.arm_client.set_angles(arm_initial_pose, speed=500)
|
||||||
|
time.sleep(2)
|
||||||
|
except Exception as e:
|
||||||
|
self._log(f" ⚠️ 机械臂初始化失败: {e}")
|
||||||
if opt_agv_move:
|
if opt_agv_move:
|
||||||
back_pt = self._find_point(positions, r + 1, c, "back")
|
back_pt = self._find_point(positions, r + 1, c, "back")
|
||||||
if back_pt and self._has_coords(back_pt):
|
if back_pt and self._has_coords(back_pt):
|
||||||
@@ -468,37 +485,30 @@ class MissionExecutorV3:
|
|||||||
# ==================== 二维码扫描 ====================
|
# ==================== 二维码扫描 ====================
|
||||||
|
|
||||||
def _scan_qr_with_poses(self, qr_configs: list) -> Optional[str]:
|
def _scan_qr_with_poses(self, qr_configs: list) -> Optional[str]:
|
||||||
"""用二维码配置中的姿态依次尝试"""
|
"""用二维码配置中的姿态依次尝试,逐一调整姿态+等2秒+扫码,全部失败才弹框"""
|
||||||
if not qr_configs:
|
if not qr_configs:
|
||||||
self._log(f" ⚠️ 无二维码配置")
|
self._log(f" ⚠️ 无二维码配置")
|
||||||
return self._request_manual_qr()
|
return self._request_manual_qr()
|
||||||
|
|
||||||
self._log(f" 🔍 尝试 {len(qr_configs)} 个二维码姿态...")
|
self._log(f" 🔍 尝试 {len(qr_configs)} 个二维码姿态...")
|
||||||
for i, qc in enumerate(qr_configs):
|
for i, qc in enumerate(qr_configs):
|
||||||
if self._stop.is_set():
|
if self._stop.is_set():
|
||||||
return None
|
return None
|
||||||
self._wait_pause()
|
self._wait_pause()
|
||||||
|
|
||||||
angles = qc.get("joint_angles", [])
|
angles = qc.get("joint_angles", [])
|
||||||
if not angles or len(angles) < 6:
|
if not angles or len(angles) < 6:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
name = qc.get("name", f"姿态{i+1}")
|
name = qc.get("name", f"姿态{i+1}")
|
||||||
self._log(f" [{i+1}/{len(qr_configs)}] {name}")
|
self._log(f" [{i+1}/{len(qr_configs)}] {name}")
|
||||||
|
# 调整机械臂姿态
|
||||||
# 调整机械臂
|
|
||||||
if self.arm_client:
|
if self.arm_client:
|
||||||
self.arm_client.set_angles(angles, speed=500)
|
self.arm_client.set_angles(angles, speed=500)
|
||||||
time.sleep(QR_POSE_WAIT)
|
time.sleep(2) # 等机械臂稳定 + 摄像头图像稳定
|
||||||
|
# 读取摄像头并扫码
|
||||||
# 扫码
|
|
||||||
qr = self._decode_qr_from_arm()
|
qr = self._decode_qr_from_arm()
|
||||||
if qr:
|
if qr:
|
||||||
self._log(f" ✅ 识别成功: {qr}")
|
self._log(f" ✅ 识别成功: {qr}")
|
||||||
return qr
|
return qr
|
||||||
|
self._log(f" ❌ {name} 未识别到二维码")
|
||||||
time.sleep(0.3)
|
|
||||||
|
|
||||||
self._log(f" ⚠️ 全部 {len(qr_configs)} 个姿态均未识别到二维码")
|
self._log(f" ⚠️ 全部 {len(qr_configs)} 个姿态均未识别到二维码")
|
||||||
return self._request_manual_qr()
|
return self._request_manual_qr()
|
||||||
|
|
||||||
@@ -780,72 +790,15 @@ class MissionExecutorV3:
|
|||||||
|
|
||||||
def _nav2_go_to_point(self, x: float, y: float, yaw: float = 0.0,
|
def _nav2_go_to_point(self, x: float, y: float, yaw: float = 0.0,
|
||||||
timeout_sec: float = 120.0) -> bool:
|
timeout_sec: float = 120.0) -> bool:
|
||||||
if not self._nav2_check_available():
|
"""使用 Nav2Navigator 直接发送导航目标(blocking 模式,等待完成)"""
|
||||||
logger.error("Nav2 action server 不可用")
|
|
||||||
return False
|
|
||||||
|
|
||||||
qz = math.sin(yaw / 2.0)
|
|
||||||
qw = math.cos(yaw / 2.0)
|
|
||||||
|
|
||||||
pose_yaml = (
|
|
||||||
f"pose:\n"
|
|
||||||
f" header:\n"
|
|
||||||
f" stamp:\n"
|
|
||||||
f" sec: 0\n"
|
|
||||||
f" nanosec: 0\n"
|
|
||||||
f" frame_id: 'map'\n"
|
|
||||||
f" pose:\n"
|
|
||||||
f" position:\n"
|
|
||||||
f" x: {x}\n"
|
|
||||||
f" y: {y}\n"
|
|
||||||
f" z: 0.0\n"
|
|
||||||
f" orientation:\n"
|
|
||||||
f" x: 0.0\n"
|
|
||||||
f" y: 0.0\n"
|
|
||||||
f" z: {qz}\n"
|
|
||||||
f" w: {qw}"
|
|
||||||
)
|
|
||||||
|
|
||||||
cmd = (
|
|
||||||
f"ros2 action send_goal /navigate_to_pose "
|
|
||||||
f"nav2_msgs/action/NavigateToPose "
|
|
||||||
f"'{pose_yaml}' --feedback"
|
|
||||||
)
|
|
||||||
full_cmd = f"bash -c '{ROS2_SETUP_CMD} && {cmd}'"
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
process = subprocess.Popen(
|
logger.info(f"🧭 导航到目标: ({x:.3f}, {y:.3f}), yaw={math.degrees(yaw):.1f}°")
|
||||||
full_cmd, shell=True,
|
ok = self._nav.navigate_to_pose(x, y, yaw, timeout_sec=timeout_sec, blocking=True)
|
||||||
stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True
|
if ok:
|
||||||
)
|
logger.info(f"✅ 导航成功到达 ({x:.3f}, {y:.3f})")
|
||||||
succeeded = False
|
else:
|
||||||
elapsed = 0.0
|
logger.warning(f"⚠️ 导航失败或超时 ({x:.3f}, {y:.3f})")
|
||||||
while elapsed < timeout_sec:
|
return ok
|
||||||
import select
|
|
||||||
reads = [process.stdout]
|
|
||||||
ready, _, _ = select.select(reads, [], [], 1.0)
|
|
||||||
elapsed += 1.0
|
|
||||||
if process.stdout in ready:
|
|
||||||
line = process.stdout.readline()
|
|
||||||
if not line:
|
|
||||||
break
|
|
||||||
ls = line.strip()
|
|
||||||
if "succeeded" in ls.lower():
|
|
||||||
succeeded = True
|
|
||||||
break
|
|
||||||
elif "failed" in ls.lower() or "aborted" in ls.lower():
|
|
||||||
break
|
|
||||||
elif "canceled" in ls.lower() or "cancelled" in ls.lower():
|
|
||||||
break
|
|
||||||
if process.poll() is not None:
|
|
||||||
break
|
|
||||||
if process.poll() is None:
|
|
||||||
process.terminate()
|
|
||||||
try:
|
|
||||||
process.wait(timeout=3)
|
|
||||||
except subprocess.TimeoutExpired:
|
|
||||||
process.kill()
|
|
||||||
return succeeded
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Nav2 异常: {e}")
|
logger.error(f"Nav2 异常: {e}")
|
||||||
return False
|
return False
|
||||||
|
|||||||
Reference in New Issue
Block a user