任务执行

This commit is contained in:
ywb
2026-05-28 10:05:40 +08:00
parent fcf5649d8c
commit 6ec778dc6d
12 changed files with 2681 additions and 0 deletions
Binary file not shown.
+650
View File
@@ -0,0 +1,650 @@
# -*- coding: utf-8 -*-
"""
任务执行器 V3 — M×N 网格蛇形路径拍摄
工作流:
1. 根据 grid 生成蛇形路径(奇数行左→右,偶数行右→左)
2. 逐台机器:
- 正面:导航 → 扫码(多姿态重试) → 查机型 → 按姿态拍照
- 背面:导航 → 按姿态拍照
3. 回到 (0,0)
"""
import os
import time
import json
import logging
import threading
import subprocess
import math
from typing import Optional, Dict, List
from enum import Enum
import requests
import cv2
import numpy as np
logger = logging.getLogger(__name__)
ROS2_SETUP_CMD = "source /opt/ros/humble/setup.bash && source ~/agv_pro_ros2/install/setup.bash"
from config import ARM_CAMERA_CONFIG
ARM_CAMERA_SNAPSHOT = ARM_CAMERA_CONFIG["snapshot_url"]
PHOTOS_DIR = "/home/elephant/photos"
# 二维码扫描重试参数
QR_SCAN_TIMEOUT = 5 # 单次扫描超时
QR_POSE_WAIT = 1.5 # 调整姿态后等待时间
MANUAL_QR_TIMEOUT = 300 # 5分钟超时
class MissionStatus(str, Enum):
IDLE = "idle"
RUNNING = "running"
PAUSED = "paused"
COMPLETED = "completed"
WAITING_QR = "waiting_qr"
class MissionExecutorV3:
"""任务执行器 V3 — M×N 网格蛇形路径"""
_instance = None # 单例,供外部停止用
def __init__(self, config: dict):
self.config = config
self.status = MissionStatus.IDLE
MissionExecutorV3._instance = self
# 实时状态报告
self.report = {
"status": "idle",
"step": "",
"progress": 0,
"total": 0,
"log": [],
"error": None,
}
# 线程同步
self._stop = threading.Event()
self._pause = threading.Event()
self._pause.set() # 初始不暂停
self._qr_event = threading.Event()
self._qr_value: Optional[str] = None
# 设备
from .arm_client import ArmClient
self.arm_client = ArmClient(
config["arm"]["host"],
config["arm"]["port"]
)
from .agv_controller_ros2 import AGVController
self.agv = AGVController(
device=config.get("device", "/dev/agvpro_controller"),
baudrate=config.get("baudrate", 1000000)
)
# ==================== 连接 ====================
def connect_all(self) -> Dict[str, bool]:
results = {}
results["agv"] = self.agv.connect()
results["arm"] = self.arm_client.connect()
return results
def disconnect_all(self):
if self.arm_client:
self.arm_client.close()
self.agv.disconnect()
# ==================== 主任务流程 ====================
def execute_mission(
self,
mission_config: dict,
machines: list,
qr_configs: list,
models: list,
) -> dict:
"""
执行完整拍摄任务。
Args:
mission_config: {rows, cols, grid, positions}
machines: [{id, row, col, front: {coords}, back: {coords}}]
qr_configs: [{id, name, joint_angles: [a1..a6]}]
models: [{id, name, poses: [{name, arm_angles}], poses_back: [...]}]
Returns:
执行报告 dict
"""
self.status = MissionStatus.RUNNING
self.report = {"status": "running", "step": "初始化", "progress": 0, "total": 0, "log": [], "error": None}
self._stop.clear()
self._pause.set()
start_time = time.time()
try:
rows = int(mission_config.get("rows", 1))
cols = int(mission_config.get("cols", 1))
grid = mission_config.get("grid", [])
positions = mission_config.get("positions", [])
# 1. 生成蛇形路径
path = self._build_snake_path(rows, cols, grid)
if not path:
self._log("❌ 网格中没有机器,任务终止")
self.report["error"] = "No machines in grid"
return self._finish(0)
self.report["total"] = len(path)
self._log(f"📍 蛇形路径生成: {len(path)} 台机器")
# 初始化任务列表
self.report["tasks"] = [{
"row": r, "col": c,
"machine_id": f"m_{r}_{c}",
"label": f"{r+1}-{c+1}",
"status": "pending",
"step": "等待",
"qr_value": None,
"photos_front": 0,
"photos_back": 0,
} for (r, c) in path]
# 初始化任务列表
self.report["tasks"] = [{
"row": r, "col": c,
"machine_id": f"m_{r}_{c}",
"label": f"{r+1}-{c+1}",
"status": "pending",
"step": "等待",
"qr_value": None,
"photos_front": 0,
"photos_back": 0,
} for (r, c) in path]
# 2. 逐台执行
for idx, (r, c) in enumerate(path):
if self._stop.is_set():
self._log("⏹️ 任务已停止")
break
self._wait_pause()
# 更新任务状态 → 正面开始
task = self._get_task(r, c)
if task:
task["status"] = "active"
task["step"] = "正面扫码"
machine_id = f"m_{r}_{c}"
machine = next((m for m in machines if m.get("id") == machine_id), None)
if not machine:
self._log(f"⚠️ 机器 {r+1}-{c+1} 不存在,跳过")
continue
rl, cl = r + 1, c + 1 # 显示用的 1-based
# --- 正面 ---
self._log(f"📍 机器 {rl}-{cl} 进入正面点位")
self._step(f"机器 {rl}-{cl} 正面")
# 导航到正面点位
front_pt = self._find_point(positions, r, c, "front")
if front_pt and self._has_coords(front_pt):
if not self._navigate(front_pt, "正面"):
self._log(f"⚠️ 导航失败,尝试继续")
else:
self._log(f"⚠️ 无正面点位坐标")
# 扫描二维码
qr_value = self._scan_qr_with_poses(qr_configs)
if self._stop.is_set():
break
# 查机型 + 更新任务步骤
model_name = self._lookup_model(qr_value)
self._log(f" 🏷️ 机型: {model_name}")
if task and qr_value:
task["qr_value"] = qr_value
if task:
task["step"] = "正面拍照"
# 正面拍照
model = self._find_model(models, model_name)
if model:
self._shoot(model, "front", rl, cl, qr_value or "unknown")
else:
self._log(f" ⚠️ 未找到机型 {model_name}")
self._progress(idx, 1)
# --- 背面 ---
if task:
task["step"] = "背面拍照"
self._log(f"📍 机器 {rl}-{cl} 进入背面点位")
self._step(f"机器 {rl}-{cl} 背面")
# 导航到背面点位
back_pt = self._find_point(positions, r + 1, c, "back")
if back_pt and self._has_coords(back_pt):
if not self._navigate(back_pt, "背面"):
self._log(f"⚠️ 导航失败,尝试继续")
else:
self._log(f"⚠️ 无背面点位坐标")
# 背面拍照
if model:
self._shoot(model, "back", rl, cl, qr_value or "unknown")
# 标记任务完成
if task:
task["status"] = "completed"
task["step"] = "完成"
self._progress(idx, 2)
# 3. 回到出发点
if not self._stop.is_set():
self._step("返回出发点")
self._log("→ 返回 (0, 0)")
self._nav2_go_to_point(0, 0, 0, timeout_sec=60)
elapsed = time.time() - start_time
return self._finish(elapsed)
except Exception as e:
self._log(f"❌ 任务异常: {e}")
logger.exception("execute_mission 崩溃")
self.report["error"] = str(e)
self.status = MissionStatus.IDLE
self.report["status"] = "idle"
return self.report
def _finish(self, elapsed: float) -> dict:
if self._stop.is_set():
self._step("已停止")
else:
self._step("完成")
self._log(f"✅ 任务完成 ({elapsed:.0f}s)")
self.report["progress"] = 100
self.status = MissionStatus.IDLE
self.report["status"] = "idle"
return self.report
# ==================== 蛇形路径 ====================
def _build_snake_path(self, rows: int, cols: int, grid: list) -> list:
"""奇数行(0,2,4...)左→右,偶数行(1,3,5...)右→左"""
path = []
for r in range(rows):
if r % 2 == 0:
for c in range(cols):
if self._has_machine(grid, r, c):
path.append((r, c))
else:
for c in range(cols - 1, -1, -1):
if self._has_machine(grid, r, c):
path.append((r, c))
return path
@staticmethod
def _has_machine(grid: list, r: int, c: int) -> bool:
if not grid or r >= len(grid):
return False
row = grid[r]
if isinstance(row, list):
return c < len(row) and bool(row[c])
return False
# ==================== 点位查找 ====================
@staticmethod
def _find_point(positions: list, row: int, col: int, side: str) -> Optional[dict]:
for p in positions:
if p.get("row") == row and p.get("col") == col and p.get("side") == side:
return p
return None
@staticmethod
def _has_coords(point: dict) -> bool:
coords = point.get("coords", [])
return len(coords) >= 2 and (coords[0] != 0 or coords[1] != 0)
# ==================== 导航 ====================
def _navigate(self, point: dict, label: str) -> bool:
coords = point["coords"]
x, y = float(coords[0]), float(coords[1])
yaw = float(coords[2]) if len(coords) >= 3 else 0.0
self._log(f" 🧭 导航到{label}点位 ({x:.2f}, {y:.2f}, yaw={math.degrees(yaw):.0f}°)")
return self._nav2_go_to_point(x, y, yaw)
# ==================== 二维码扫描 ====================
def _scan_qr_with_poses(self, qr_configs: list) -> Optional[str]:
"""用二维码配置中的姿态依次尝试"""
if not qr_configs:
self._log(f" ⚠️ 无二维码配置")
return self._request_manual_qr()
self._log(f" 🔍 尝试 {len(qr_configs)} 个二维码姿态...")
for i, qc in enumerate(qr_configs):
if self._stop.is_set():
return None
self._wait_pause()
angles = qc.get("joint_angles", [])
if not angles or len(angles) < 6:
continue
name = qc.get("name", f"姿态{i+1}")
self._log(f" [{i+1}/{len(qr_configs)}] {name}")
# 调整机械臂
if self.arm_client:
self.arm_client.set_angles(angles, speed=500)
time.sleep(QR_POSE_WAIT)
# 扫码
qr = self._decode_qr_from_arm()
if qr:
self._log(f" ✅ 识别成功: {qr}")
return qr
time.sleep(0.3)
self._log(f" ⚠️ 全部 {len(qr_configs)} 个姿态均未识别到二维码")
return self._request_manual_qr()
def _decode_qr_from_arm(self) -> Optional[str]:
"""从机械臂摄像头取一帧,识别二维码"""
for attempt in range(3):
try:
resp = requests.get(ARM_CAMERA_SNAPSHOT, timeout=QR_SCAN_TIMEOUT)
if resp.status_code != 200 or not resp.content:
continue
arr = np.frombuffer(resp.content, dtype=np.uint8)
frame = cv2.imdecode(arr, cv2.IMREAD_COLOR)
if frame is None:
continue
detector = cv2.QRCodeDetector()
data, bbox, _ = detector.detectAndDecode(frame)
if data and len(data) > 0:
return data
except Exception:
pass
time.sleep(0.5)
return None
def _request_manual_qr(self) -> Optional[str]:
"""暂停任务,等待手动输入"""
self.status = MissionStatus.WAITING_QR
self.report["status"] = "waiting_qr"
self.report["step"] = "等待手动输入二维码"
self._log(" ⌨️ 弹窗等待手动输入二维码...")
self._qr_event.clear()
if self._qr_event.wait(timeout=MANUAL_QR_TIMEOUT):
self.status = MissionStatus.RUNNING
self.report["status"] = "running"
self._log(f" ✏️ 手动输入: {self._qr_value}")
return self._qr_value
else:
self.status = MissionStatus.RUNNING
self.report["status"] = "running"
self._log(f" ⚠️ 等待超时({MANUAL_QR_TIMEOUT}s),跳过")
return None
def set_manual_qr(self, value: str):
self._qr_value = value.strip()
self._qr_event.set()
# ==================== 机型查询 ====================
def _lookup_model(self, qr_value: Optional[str]) -> str:
"""TODO: 后续通过 HTTP 接口查询机型"""
return "机器1"
@staticmethod
def _find_model(models: list, name: str) -> Optional[dict]:
"""在机型列表中找到匹配的机型"""
for m in models:
if m.get("name") == name or m.get("id") == name:
return m
# fallback: 第一个机型
return models[0] if models else None
# ==================== 姿态拍照 ====================
def _shoot(self, model: dict, side: str, row: int, col: int, qr_value: str):
"""按机型配置的所有姿态依次拍照"""
# 更新任务照片计数
task = self._get_task(row - 1, col - 1)
side_label = "正面" if side == "front" else "背面"
poses = model.get("poses", []) if side == "front" else model.get("poses_back", [])
if not poses:
self._log(f" ⚠️ 机型无{side_label}姿态配置")
return
self._log(f" 📷 {side_label}拍照 ({len(poses)} 个姿态)")
for pi, pose in enumerate(poses):
if self._stop.is_set():
break
self._wait_pause()
angles = pose.get("arm_angles", [])
if not angles or len(angles) < 6:
self._log(f" 跳过 {pose.get('name', f'姿态{pi+1}')}: 无效角度")
continue
name = pose.get("name", f"{side_label}-{pi+1}")
self._log(f" 🎯 {name}")
# 调整机械臂
if self.arm_client:
self.arm_client.set_angles(angles, speed=500)
time.sleep(QR_POSE_WAIT)
# 拍照
path = self._capture_arm_photo(row, col, side, pi + 1, qr_value)
if path:
self._log(f" 💾 {os.path.basename(path)}")
def _capture_arm_photo(self, row: int, col: int, side: str,
pose_idx: int, qr_value: str) -> Optional[str]:
"""从机械臂摄像头拍照存本地"""
try:
resp = requests.get(ARM_CAMERA_SNAPSHOT, timeout=10)
if resp.status_code != 200 or not resp.content:
logger.error("arm snapshot 请求失败")
return None
os.makedirs(PHOTOS_DIR, exist_ok=True)
ts = time.strftime("%Y%m%d_%H%M%S")
fname = f"{ts}_r{row}c{col}_{side}_p{pose_idx}_{qr_value[:20]}.jpg"
fpath = os.path.join(PHOTOS_DIR, fname)
with open(fpath, "wb") as f:
f.write(resp.content)
return fpath
except Exception as e:
logger.error(f"拍照异常: {e}")
return None
# ==================== 控制 ====================
def _wait_pause(self):
"""等待暂停状态解除"""
self._pause.wait()
def pause(self):
self._pause.clear()
self.status = MissionStatus.PAUSED
self.report["status"] = "paused"
self.report["step"] = "已暂停"
self._log("⏸️ 任务已暂停")
def resume(self):
self._pause.set()
self.status = MissionStatus.RUNNING
self.report["status"] = "running"
self._log("▶️ 任务已恢复")
def stop(self):
self._stop.set()
self._pause.set() # 解除暂停
self._qr_event.set() # 解除 QR 等待
if self.arm_client:
try:
self.arm_client.task_stop()
except Exception:
pass
self.agv.stop()
self.status = MissionStatus.IDLE
self.report["status"] = "idle"
def get_status(self) -> dict:
return {
"status": self.report["status"],
"step": self.report["step"],
"progress": self.report["progress"],
"total": self.report["total"],
}
def get_logs(self) -> dict:
"""返回实时日志和完整状态"""
return self.report
# ==================== 状态报告 ====================
def _log(self, msg: str):
self.report["log"].append(msg)
# Keep last 500 entries
if len(self.report["log"]) > 500:
self.report["log"] = self.report["log"][-500:]
logger.info(msg)
def _step(self, text: str):
self.report["step"] = text
def _get_task(self, row: int, col: int) -> Optional[dict]:
"""获取指定行列的任务记录"""
for t in self.report.get("tasks", []):
if t["row"] == row and t["col"] == col:
return t
return None
def _progress(self, machine_idx: int, side_code: int):
"""side_code: 1=正面完成, 2=背面完成"""
if self.report["total"]:
self.report["progress"] = min(
int((machine_idx * 2 + side_code) / (self.report["total"] * 2) * 100),
99
)
# ==================== Nav2 导航 ====================
# (保留原实现)
def _nav2_check_available(self) -> bool:
try:
rc, out, err = self._run_ros2_cmd("ros2 action list")
if rc != 0:
return False
return "/navigate_to_pose" in out
except:
return False
def _nav2_go_to_point(self, x: float, y: float, yaw: float = 0.0,
timeout_sec: float = 120.0) -> bool:
if not self._nav2_check_available():
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:
process = subprocess.Popen(
full_cmd, shell=True,
stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True
)
succeeded = False
elapsed = 0.0
while elapsed < timeout_sec:
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:
logger.error(f"Nav2 异常: {e}")
return False
def _nav2_cancel(self):
cancel_cmd = f"bash -c '{ROS2_SETUP_CMD} && ros2 action cancel /navigate_to_pose 2>/dev/null || true'"
try:
subprocess.run(cancel_cmd, shell=True, timeout=3)
except:
pass
def _run_ros2_cmd(self, cmd: str, timeout: float = 5.0) -> tuple:
full_cmd = f"bash -c '{ROS2_SETUP_CMD} && {cmd}'"
try:
result = subprocess.run(
full_cmd, shell=True,
capture_output=True, text=True, timeout=timeout
)
return result.returncode, result.stdout.strip(), result.stderr.strip()
except subprocess.TimeoutExpired:
return -1, "", "Timeout"
except Exception as e:
return -1, "", str(e)
+122
View File
@@ -0,0 +1,122 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>运行监控 - AGV 拍摄系统</title>
<link rel="stylesheet" href="/static/css/style.css">
</head>
<body>
<div id="app">
<header class="topbar">
<div class="logo">▶️ 任务运行</div>
<nav class="nav">
<a href="/" class="nav-link">🏠 首页</a>
<a href="/setting" class="nav-link">⚙️ 设置</a>
<a href="/running" class="nav-link active">▶️ 运行</a>
</nav>
</header>
<main class="container">
<!-- 状态概览 -->
<section class="card">
<div class="running-header">
<div class="running-status" :class="missionState">
<span class="pulse"></span>
[[ missionStateText ]]
</div>
<div class="running-progress" v-if="missionState === 'running' || missionState === 'waiting_qr'">
<span>进度 [[ Math.round(progress) ]]%</span>
<div class="progress-bar">
<div class="progress-fill" :style="{width: progress + '%'}"></div>
</div>
</div>
</div>
<div class="btn-row">
<button class="btn btn-success btn-large" @click="startMission" :disabled="missionState !== 'idle'">
▶️ 开始任务
</button>
<button class="btn btn-warning btn-large" @click="pauseMission" :disabled="missionState !== 'running'">
⏸️ 暂停
</button>
<button class="btn btn-primary btn-large" @click="resumeMission" :disabled="missionState !== 'paused'">
▶️ 继续
</button>
<button class="btn btn-error btn-large" @click="stopMission" :disabled="missionState === 'idle'">
⏹️ 停止
</button>
</div>
</section>
<!-- 任务清单 -->
<section class="card" v-if="tasks.length > 0">
<h2>📋 任务清单 ([[ tasks.length ]] 台机器)</h2>
<div class="task-grid">
<div v-for="task in tasks" :key="task.machine_id"
class="task-cell" :class="'task-' + task.status"
:title="task.step">
<div class="task-pos">[[ task.label ]]</div>
<div class="task-status-icon">
<span v-if="task.status === 'pending'"></span>
<span v-else-if="task.status === 'active'" class="pulse-icon">🔄</span>
<span v-else-if="task.status === 'completed'"></span>
<span v-else></span>
</div>
<div class="task-step-text">[[ task.step ]]</div>
<div class="task-info">
<div v-if="task.qr_value" class="task-qr">🏷 [[ task.qr_value.substring(0,8) ]]</div>
<div class="task-photos" v-if="task.photos_front || task.photos_back">
📷 [[ task.photos_front ]]正 [[ task.photos_back ]]背
</div>
</div>
</div>
</div>
</section>
<!-- 实时日志 -->
<section class="card" v-if="missionState === 'running' || missionState === 'waiting_qr'">
<h2>📜 实时日志</h2>
<div class="log-box" ref="logBox">
<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>
</section>
<!-- 实时预览 -->
<section class="card">
<h2>📷 摄像头预览</h2>
<div class="camera-full">
<img :src="previewUrl" @error="onPreviewError">
</div>
</section>
<!-- 任务报告 -->
<section class="card" v-if="report">
<h2>📊 任务报告</h2>
<div class="report-summary">
<div class="stat ok">✅ 完成: [[ report.completed ]]</div>
<div class="stat error">❌ 失败: [[ report.failed ]]</div>
<div class="stat">总计: [[ report.total_points ]]</div>
</div>
</section>
<!-- 手动输入二维码弹窗 -->
<div class="modal-overlay" v-if="showQrModal">
<div class="modal">
<h3>⌨️ 手动输入二维码</h3>
<p>所有姿态均未识别到二维码,请手动输入:</p>
<input type="text" v-model="qrValue" placeholder="输入二维码内容" autofocus @keyup.enter="submitQr">
<div class="modal-actions">
<button class="btn btn-primary" @click="submitQr">确认</button>
<button class="btn" @click="cancelQr">跳过</button>
</div>
</div>
</div>
</main>
</div>
<script src="/static/js/vue3.global.prod.js"></script>
<script src="/static/js/running.js"></script>
</body>
</html>
+130
View File
@@ -0,0 +1,130 @@
const { createApp } = Vue
const API = ''
createApp({
delimiters: ['[[', ']]'],
data() {
return {
missionState: 'idle',
progress: 0,
tasks: [],
report: null,
previewUrl: API + '/api/camera/preview',
polling: null,
logs: [],
showQrModal: false,
qrValue: '',
}
},
computed: {
missionStateText() {
const map = {
idle: '空闲',
running: '任务运行中',
paused: '已暂停',
completed: '已完成',
waiting_qr: '等待输入二维码'
}
return map[this.missionState] || '未知'
},
},
mounted() {
this.poll()
},
beforeUnmount() {
if (this.polling) clearInterval(this.polling)
},
methods: {
poll() {
this.refresh()
this.pollLogs()
this.polling = setInterval(() => {
this.refresh()
this.pollLogs()
}, 2000)
},
async refresh() {
try {
const res = await fetch(API + '/api/mission/state')
const data = await res.json()
this.missionState = data.status || 'idle'
this.progress = data.progress || 0
if (data.tasks) this.tasks = data.tasks
// QR 弹窗
if (this.missionState === 'waiting_qr' && !this.showQrModal) {
this.showQrModal = true
this.qrValue = ''
}
// 完成后获取报告
if (this.missionState === 'idle' && this.tasks.length > 0) {
const reportRes = await fetch(API + '/api/mission/report')
const reportData = await reportRes.json()
this.report = reportData.report
}
} catch (e) {}
},
async pollLogs() {
if (this.missionState !== 'running' && this.missionState !== 'waiting_qr') return
try {
const res = await fetch(API + '/api/mission/log')
const data = await res.json()
if (data.log) this.logs = data.log
if (data.progress != null) this.progress = data.progress
if (data.tasks) this.tasks = data.tasks
// 自动滚到底
this.$nextTick(() => {
const box = this.$refs.logBox
if (box) box.scrollTop = box.scrollHeight
})
} catch (e) {}
},
async startMission() {
if (this.missionState !== 'idle') return
this.logs = []
this.progress = 0
this.report = null
this.showQrModal = false
await fetch(API + '/api/mission/start', { method: 'POST' })
this.missionState = 'running'
},
async pauseMission() {
await fetch(API + '/api/mission/pause', { method: 'POST' })
this.missionState = 'paused'
},
async resumeMission() {
await fetch(API + '/api/mission/resume', { method: 'POST' })
this.missionState = 'running'
this.showQrModal = false
},
async stopMission() {
await fetch(API + '/api/mission/stop', { method: 'POST' })
this.missionState = 'idle'
this.showQrModal = false
},
async submitQr() {
const val = this.qrValue.trim()
await fetch(API + '/api/mission/manual-qr', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ qr: val || ' ' })
})
this.showQrModal = false
this.qrValue = ''
},
cancelQr() {
this.showQrModal = false
this.qrValue = ''
fetch(API + '/api/mission/manual-qr', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ qr: 'SKIP' })
})
},
onPreviewError(e) {
e.target.style.display = 'none'
}
}
}).mount('#app')
+67
View File
@@ -0,0 +1,67 @@
#!/usr/bin/env python3
"""修复激光雷达时间戳偏移的修正器 v5"""
import os, sys, rclpy
from rclpy.node import Node
from sensor_msgs.msg import LaserScan
from builtin_interfaces.msg import Time
LOCKFILE = "/tmp/scan_fixer.lock"
if os.path.exists(LOCKFILE):
with open(LOCKFILE) as f:
old_pid = int(f.read().strip())
try:
os.kill(old_pid, 0)
print(f"Another fixer running PID {old_pid}, exit.", file=sys.stderr)
sys.exit(1)
except (OSError, ProcessLookupError):
print(f"Stale lock removed (PID {old_pid} dead)", file=sys.stderr)
with open(LOCKFILE, "w") as f:
f.write(str(os.getpid()))
def main():
rclpy.init(args=sys.argv[1:])
node = Node('scan_timestamp_fixer')
offset = 2.0
pub = node.create_publisher(LaserScan, '/scan_corrected', 10)
count = [0]
def cb(msg: LaserScan):
count[0] += 1
s, ns = msg.header.stamp.sec, msg.header.stamp.nanosec
s2 = s - int(offset)
ns2 = ns - int((offset % 1) * 1e9)
if ns2 < 0:
ns2 += 1000000000
s2 -= 1
out = LaserScan()
out.header.frame_id = msg.header.frame_id
out.header.stamp = Time(sec=s2, nanosec=ns2)
out.angle_min = msg.angle_min
out.angle_max = msg.angle_max
out.angle_increment = msg.angle_increment
out.time_increment = msg.time_increment
out.scan_time = msg.scan_time
out.range_min = msg.range_min
out.range_max = msg.range_max
out.ranges = msg.ranges
out.intensities = msg.intensities
pub.publish(out)
if count[0] % 200 == 0:
node.get_logger().info(f'#{count[0]} /scan={s} -> /scan_corrected={s2}')
node.create_subscription(LaserScan, '/scan', cb, 10)
node.get_logger().info(f'Fixer PID={os.getpid()}, offset={offset}s')
try:
while rclpy.ok():
rclpy.spin_once(node, timeout_sec=0.5)
finally:
node.destroy_node()
rclpy.shutdown()
if os.path.exists(LOCKFILE):
os.unlink(LOCKFILE)
if __name__ == '__main__':
main()
+17
View File
@@ -0,0 +1,17 @@
#!/bin/bash
source /opt/ros/humble/setup.bash
source /home/elephant/agv_pro_ros2/install/setup.bash
export ROS_DOMAIN_ID=1
cd /home/elephant/agv_pro_ros2
nohup ros2 daemon start >/dev/null 2>&1 &
sleep 5
nohup ros2 launch agv_pro_bringup agv_pro_bringup.launch.py port_name:=/dev/agvpro_controller > /tmp/ros2_bringup.log 2>&1 &
sleep 8
nohup python3 /home/elephant/work/scan_fixer/fix_scan_timestamp.py > /tmp/scan_fixer.log 2>&1 &
sleep 5
nohup ros2 launch agv_pro_navigation2 navigation2_active.launch.py autostart:=True > /tmp/ros2_nav2.log 2>&1 &
sleep 15
cd /home/elephant/work/agv_app && nohup python3 app.py > /tmp/agv_flask.log 2>&1 &
sleep 5
echo "ALL_STARTED"
ps aux | grep -E 'lslidar|agv_pro_node|nav2_container|scan_timestamp_fixer|ros2-daemon|app.py' | grep -v grep
+17
View File
@@ -0,0 +1,17 @@
#!/bin/bash
source /opt/ros/humble/setup.bash
source /home/elephant/agv_pro_ros2/install/setup.bash
export ROS_DOMAIN_ID=1
cd /home/elephant/agv_pro_ros2
nohup ros2 daemon start >/dev/null 2>&1 &
sleep 5
nohup ros2 launch agv_pro_bringup agv_pro_bringup.launch.py port_name:=/dev/agvpro_controller > /tmp/ros2_bringup.log 2>&1 &
sleep 8
nohup python3 /home/elephant/work/scan_fixer/fix_scan_timestamp.py > /tmp/scan_fixer.log 2>&1 &
sleep 5
nohup ros2 launch agv_pro_navigation2 navigation2_active.launch.py autostart:=True > /tmp/ros2_nav2.log 2>&1 &
sleep 15
cd /home/elephant/work/agv_app && nohup python3 app.py > /tmp/agv_flask.log 2>&1 &
sleep 5
echo "ALL_STARTED"
ps aux | grep -E 'lslidar|agv_pro_node|nav2_container|scan_timestamp_fixer|ros2-daemon|app.py' | grep -v grep
+177
View File
@@ -0,0 +1,177 @@
#!/bin/bash
# ============================================================
# Robot AGV 全量启动脚本 v2.6
# 修复:
# - 清理 scan_fixer lock 文件防残留
# - Nav2 节点检测 grep -c 改为单行输出
# - nohup 启动 Nav2 用 bash -c 包裹(确保 source 环境)
# ============================================================
set -e
AGV_APP_DIR="/home/elephant/work/agv_app"
AGV_ROS2_DIR="/home/elephant/agv_pro_ros2"
ROS_DOMAIN_ID_VAL=1
echo "=========================================="
echo " Robot AGV 全量启动 v2.6"
echo "=========================================="
echo ""
# ---------- 1. 清理旧进程(不杀 ros2-daemon ----------
echo "[1/7] 清理旧进程..."
pkill -f "ros2 launch agv_pro_bringup" 2>/dev/null || true
pkill -f "ros2 launch agv_pro_navigation2" 2>/dev/null || true
pkill -f "agv_pro_node" 2>/dev/null || true
pkill -f "lslidar_driver_node" 2>/dev/null || true
pkill -f "fix_scan_timestamp" 2>/dev/null || true
pkill -f "python.*app.py" 2>/dev/null || true
sleep 4
# 清理 scan_fixer 锁文件(防残留 PID 导致启动失败)
rm -f /tmp/scan_fixer.lock
echo " 清理完成"
# ---------- 2. 重启 ros2 daemon ----------
echo "[2/7] 重启 ros2 daemon..."
pkill -f "ros2-daemon" 2>/dev/null || true
sleep 2
nohup bash -c "source /opt/ros/humble/setup.bash && ros2 daemon start" >/dev/null 2>&1 &
sleep 5
echo " ros2 daemon 已就绪"
# ---------- 3. 启动 bringup (含激光雷达) ----------
echo "[3/7] 启动 AGV Bringup..."
source /opt/ros/humble/setup.bash
cd "$AGV_ROS2_DIR"
source install/setup.bash
nohup ros2 launch agv_pro_bringup agv_pro_bringup.launch.py \
port_name:=/dev/agvpro_controller > /tmp/ros2_bringup.log 2>&1 &
BRINGUP_PID=$!
echo " bringup PID: $BRINGUP_PID"
echo " 等待 bringup 就绪..."
for i in $(seq 1 20); do
if ROS_DOMAIN_ID=$ROS_DOMAIN_ID_VAL ros2 topic list 2>/dev/null | grep -q '/odom'; then
echo " ✅ bringup 已就绪"
break
fi
sleep 2
done
# ---------- 4. 启动激光时间戳修正节点 ----------
echo "[4/7] 启动激光时间戳修正节点..."
pkill -f "fix_scan_timestamp" 2>/dev/null || true
sleep 2
# 确保 /scan 存在
for i in $(seq 1 10); do
if ROS_DOMAIN_ID=$ROS_DOMAIN_ID_VAL ros2 topic list 2>/dev/null | grep -q '/scan'; then
echo " /scan 话题已上线"
break
fi
sleep 2
done
nohup bash -c "source /opt/ros/humble/setup.bash && \
ROS_DOMAIN_ID=$ROS_DOMAIN_ID_VAL python3 /home/elephant/work/scan_fixer/fix_scan_timestamp.py" \
> /tmp/scan_fixer.log 2>&1 &
FIXER_PID=$!
echo " fix_scan_timestamp PID: $FIXER_PID"
sleep 5
# 验证 fixer 进程和 scan_corrected
FIXER_COUNT=$(ps aux | grep -c "[f]ix_scan_timestamp" 2>/dev/null || echo 0)
if [ "$FIXER_COUNT" -gt 1 ]; then
echo " ⚠️ 发现 $FIXER_COUNT 个 fixer 进程,杀掉多余的..."
pkill -f "fix_scan_timestamp" 2>/dev/null || true
sleep 2
rm -f /tmp/scan_fixer.lock
nohup bash -c "source /opt/ros/humble/setup.bash && \
ROS_DOMAIN_ID=$ROS_DOMAIN_ID_VAL python3 /home/elephant/work/scan_fixer/fix_scan_timestamp.py" \
> /tmp/scan_fixer.log 2>&1 &
FIXER_PID=$!
sleep 3
fi
if ROS_DOMAIN_ID=$ROS_DOMAIN_ID_VAL ros2 topic list 2>/dev/null | grep -q '/scan_corrected'; then
echo " ✅ /scan_corrected 已上线"
else
echo " ⚠️ /scan_corrected 未上线,检查日志:"
tail -5 /tmp/scan_fixer.log
fi
# ---------- 5. 启动 Nav2 ----------
echo "[5/7] 启动 Nav2 导航..."
source /opt/ros/humble/setup.bash
cd "$AGV_ROS2_DIR"
source install/setup.bash
# 使用 bash -c 确保 source 环境变量传递到 nohup
nohup bash -c "source /opt/ros/humble/setup.bash && \
source /home/elephant/agv_pro_ros2/install/setup.bash && \
export ROS_DOMAIN_ID=$ROS_DOMAIN_ID_VAL && \
ros2 launch agv_pro_navigation2 navigation2_active.launch.py \
autostart:=True" > /tmp/ros2_nav2.log 2>&1 &
NAV2_PID=$!
echo " Nav2 PID: $NAV2_PID"
sleep 12
echo " 等待 Nav2 节点就绪..."
for i in $(seq 1 20); do
NODES=$(ROS_DOMAIN_ID=$ROS_DOMAIN_ID_VAL ros2 node list 2>/dev/null | \
grep -cE 'lifecycle_manager_navigation|bt_navigator|controller_server' 2>/dev/null || echo 0)
# 去除可能的换行符,确保是单个数字
NODES=$(echo "$NODES" | tr -d '\n' | awk '{print $1}')
if [ "$NODES" -ge 3 ] 2>/dev/null; then
echo " ✅ Nav2 节点已就绪 ($NODES 个)"
break
fi
sleep 3
done
# ---------- 6. 设置精度参数 ----------
echo "[6/7] 设置导航精度参数 (xy_goal_tolerance=0.05m)..."
source /opt/ros/humble/setup.bash
cd "$AGV_ROS2_DIR"
source install/setup.bash
for NODE in /controller_server /bt_navigator /planner_server; do
ROS_DOMAIN_ID=$ROS_DOMAIN_ID_VAL timeout 1 ros2 param set $NODE general_goal_checker.xy_goal_tolerance 0.05 2>/dev/null || true
ROS_DOMAIN_ID=$ROS_DOMAIN_ID_VAL timeout 1 ros2 param set $NODE general_goal_checker.yaw_goal_tolerance 0.05 2>/dev/null || true
done
ROS_DOMAIN_ID=$ROS_DOMAIN_ID_VAL timeout 1 ros2 param set /controller_server FollowPath.xy_goal_tolerance 0.05 2>/dev/null || true
ROS_DOMAIN_ID=$ROS_DOMAIN_ID_VAL timeout 1 ros2 param set /controller_server general_goal_checker.stateful True 2>/dev/null || true
ROS_DOMAIN_ID=$ROS_DOMAIN_ID_VAL timeout 1 ros2 param set /controller_server FollowPath.stateful True 2>/dev/null || true
echo " 精度参数已设置"
# ---------- 7. 启动 Flask ----------
echo "[7/7] 启动 Flask API..."
cd "$AGV_APP_DIR"
nohup python3 app.py > /tmp/agv_flask.log 2>&1 &
FLASK_PID=$!
echo " Flask PID: $FLASK_PID"
sleep 4
# ---------- 完成 ----------
echo ""
echo "=========================================="
echo " ✅ 启动完成"
echo "=========================================="
echo ""
echo " 进程状态:"
for PROC in "bringup:$BRINGUP_PID" "Nav2:$NAV2_PID" "fixer:$FIXER_PID" "Flask:$FLASK_PID"; do
NAME="${PROC%%:*}"
PID="${PROC##*:}"
echo " $NAME : $(ps aux | grep -w "$PID" | grep -v grep | awk '{print $2}' || echo '已退出')"
done
echo ""
echo " 日志文件:"
echo " bringup : /tmp/ros2_bringup.log"
echo " Nav2 : /tmp/ros2_nav2.log"
echo " fixer : /tmp/scan_fixer.log"
echo " Flask : /tmp/agv_flask.log"
echo ""
echo " 关键验证命令:"
echo " curl http://localhost:5000/api/navigate/status"
echo " ROS_DOMAIN_ID=1 ros2 topic echo /scan_corrected --once"
echo " ROS_DOMAIN_ID=1 ros2 topic echo /amcl_pose --once"
+165
View File
@@ -0,0 +1,165 @@
#!/bin/bash
# ============================================================
# Robot AGV 全量启动脚本 v2.2
# 完整流程:
# 清理旧进程(不杀 daemon -> 启动 bringup ->
# 启动激光时间戳修正节点 -> 启动 Nav2 ->
# 设置导航精度参数 -> 启动 Flask
# ============================================================
set -e
AGV_APP_DIR="/home/elephant/work/agv_app"
AGV_ROS2_DIR="/home/elephant/agv_pro_ros2"
ROS_DOMAIN_ID_VAL=1
echo "=========================================="
echo " Robot AGV 全量启动 v2.2"
echo "=========================================="
echo ""
# ---------- 1. 清理旧进程(不杀 ros2-daemon ----------
echo "[1/7] 清理旧进程..."
pkill -f "ros2 launch agv_pro_bringup" 2>/dev/null || true
pkill -f "ros2 launch agv_pro_navigation2" 2>/dev/null || true
pkill -f "agv_pro_node" 2>/dev/null || true
pkill -f "lslidar_driver_node" 2>/dev/null || true
pkill -f "scan_timestamp_fixer" 2>/dev/null || true
pkill -f "python.*app.py" 2>/dev/null || true
sleep 4
echo " 清理完成"
# ---------- 2. 重启 ros2 daemon(仅杀 daemon进程本身,不杀整个环境) ----------
echo "[2/7] 重启 ros2 daemon..."
pkill -f "ros2-daemon" 2>/dev/null || true
sleep 2
nohup bash -c "source /opt/ros/humble/setup.bash && ros2 daemon start" >/dev/null 2>&1 &
sleep 5
echo " ros2 daemon 已就绪"
# ---------- 3. 启动 bringup (含激光雷达) ----------
echo "[3/7] 启动 AGV Bringup..."
source /opt/ros/humble/setup.bash
cd "$AGV_ROS2_DIR"
source install/setup.bash
nohup ros2 launch agv_pro_bringup agv_pro_bringup.launch.py \
port_name:=/dev/agvpro_controller > /tmp/ros2_bringup.log 2>&1 &
BRINGUP_PID=$!
echo " bringup PID: $BRINGUP_PID"
echo " 等待 bringup 就绪..."
for i in $(seq 1 20); do
if ROS_DOMAIN_ID=$ROS_DOMAIN_ID_VAL ros2 topic list 2>/dev/null | grep -q '/odom'; then
echo " ✅ bringup 已就绪"
break
fi
sleep 2
done
# ---------- 4. 启动激光时间戳修正节点(单例,不重复启动) ----------
echo "[4/7] 启动激光时间戳修正节点..."
# 确保只有1个 fixer 进程在运行
pkill -f "scan_timestamp_fixer" 2>/dev/null || true
sleep 2
for i in $(seq 1 10); do
if ROS_DOMAIN_ID=$ROS_DOMAIN_ID_VAL ros2 topic list 2>/dev/null | grep -q '/scan'; then
echo " /scan 话题已上线"
break
fi
sleep 2
done
nohup bash -c "source /opt/ros/humble/setup.bash && \
ROS_DOMAIN_ID=$ROS_DOMAIN_ID_VAL python3 /home/elephant/work/scan_fixer/fix_scan_timestamp.py" \
> /tmp/scan_fixer.log 2>&1 &
FIXER_PID=$!
echo " scan_timestamp_fixer PID: $FIXER_PID"
sleep 5
# 验证只有1个 fixer 进程
FIXER_COUNT=$(ps aux | grep -c "[f]ix_scan_timestamp" 2>/dev/null || echo 0)
if [ "$FIXER_COUNT" -gt 1 ]; then
echo " ⚠️ 发现 $FIXER_COUNT 个 fixer 进程,杀掉多余的..."
pkill -f "scan_timestamp_fixer" 2>/dev/null || true
sleep 2
nohup bash -c "source /opt/ros/humble/setup.bash && \
ROS_DOMAIN_ID=$ROS_DOMAIN_ID_VAL python3 /home/elephant/work/scan_fixer/fix_scan_timestamp.py" \
> /tmp/scan_fixer.log 2>&1 &
sleep 3
fi
if ROS_DOMAIN_ID=$ROS_DOMAIN_ID_VAL ros2 topic list 2>/dev/null | grep -q '/scan_corrected'; then
echo " ✅ /scan_corrected 已上线"
else
echo " ⚠️ /scan_corrected 未上线,检查日志:"
cat /tmp/scan_fixer.log
fi
# ---------- 5. 启动 Nav2 ----------
echo "[5/7] 启动 Nav2 导航..."
source /opt/ros/humble/setup.bash
cd "$AGV_ROS2_DIR"
source install/setup.bash
nohup ros2 launch agv_pro_navigation2 navigation2_active.launch.py \
autostart:=True > /tmp/ros2_nav2.log 2>&1 &
NAV2_PID=$!
echo " Nav2 PID: $NAV2_PID"
sleep 12
echo " 等待 Nav2 节点就绪..."
for i in $(seq 1 15); do
NODES=$(ROS_DOMAIN_ID=$ROS_DOMAIN_ID_VAL ros2 node list 2>/dev/null | \
grep -c "lifecycle_manager_navigation\|bt_navigator\|controller_server" 2>/dev/null || echo 0)
if [ "$NODES" -ge 3 ]; then
echo " ✅ Nav2 节点已就绪 ($NODES 个)"
break
fi
sleep 3
done
# ---------- 6. 设置精度参数 ----------
echo "[6/7] 设置导航精度参数 (xy_goal_tolerance=0.05m)..."
source /opt/ros/humble/setup.bash
cd "$AGV_ROS2_DIR"
source install/setup.bash
for NODE in /controller_server /bt_navigator /planner_server; do
ROS_DOMAIN_ID=$ROS_DOMAIN_ID_VAL ros2 param set $NODE general_goal_checker.xy_goal_tolerance 0.05 2>/dev/null || true
ROS_DOMAIN_ID=$ROS_DOMAIN_ID_VAL ros2 param set $NODE general_goal_checker.yaw_goal_tolerance 0.05 2>/dev/null || true
done
ROS_DOMAIN_ID=$ROS_DOMAIN_ID_VAL ros2 param set /controller_server FollowPath.xy_goal_tolerance 0.05 2>/dev/null || true
ROS_DOMAIN_ID=$ROS_DOMAIN_ID_VAL ros2 param set /controller_server general_goal_checker.stateful True 2>/dev/null || true
ROS_DOMAIN_ID=$ROS_DOMAIN_ID_VAL ros2 param set /controller_server FollowPath.stateful True 2>/dev/null || true
echo " 精度参数已设置"
# ---------- 7. 启动 Flask ----------
echo "[7/7] 启动 Flask API..."
cd "$AGV_APP_DIR"
nohup python3 app.py > /tmp/agv_flask.log 2>&1 &
FLASK_PID=$!
echo " Flask PID: $FLASK_PID"
sleep 4
# ---------- 完成 ----------
echo ""
echo "=========================================="
echo " ✅ 启动完成"
echo "=========================================="
echo ""
echo " 进程状态:"
for PROC in "bringup:$BRINGUP_PID" "Nav2:$NAV2_PID" "fixer:$FIXER_PID" "Flask:$FLASK_PID"; do
NAME="${PROC%%:*}"
PID="${PROC##*:}"
echo " $NAME : $(ps aux | grep -w "$PID" | grep -v grep | awk '{print $2}' || echo '已退出')"
done
echo ""
echo " 日志文件:"
echo " bringup : /tmp/ros2_bringup.log"
echo " Nav2 : /tmp/ros2_nav2.log"
echo " fixer : /tmp/scan_fixer.log"
echo " Flask : /tmp/agv_flask.log"
echo ""
echo " 关键验证命令:"
echo " curl http://localhost:5000/api/navigate/status"
echo " ROS_DOMAIN_ID=1 ros2 topic echo /scan_corrected --once"
echo " ROS_DOMAIN_ID=1 ros2 topic echo /amcl_pose --once"
+350
View File
@@ -0,0 +1,350 @@
amcl:
ros__parameters:
use_sim_time: True
alpha1: 0.2
alpha2: 0.2
alpha3: 0.2
alpha4: 0.2
alpha5: 0.2
base_frame_id: "base_footprint"
beam_skip_distance: 0.5
beam_skip_error_threshold: 0.9
beam_skip_threshold: 0.3
do_beamskip: false
global_frame_id: "map"
lambda_short: 0.1
laser_likelihood_max_dist: 2.0
laser_max_range: 100.0
laser_min_range: -1.0
laser_model_type: "likelihood_field"
max_beams: 60
max_particles: 2000
min_particles: 500
odom_frame_id: "odom"
pf_err: 0.05
pf_z: 0.99
recovery_alpha_fast: 0.0
recovery_alpha_slow: 0.0
resample_interval: 1
robot_model_type: "nav2_amcl::DifferentialMotionModel"
save_pose_rate: 0.5
sigma_hit: 0.2
tf_broadcast: true
transform_tolerance: 5.0
update_min_a: 0.2
update_min_d: 0.25
z_hit: 0.5
z_max: 0.05
z_rand: 0.5
z_short: 0.05
scan_topic: scan_corrected
bt_navigator:
ros__parameters:
use_sim_time: True
global_frame: map
robot_base_frame: base_link
odom_topic: /odom
bt_loop_duration: 10
default_server_timeout: 20
wait_for_service_timeout: 1000
# 'default_nav_through_poses_bt_xml' and 'default_nav_to_pose_bt_xml' are use defaults:
# nav2_bt_navigator/navigate_to_pose_w_replanning_and_recovery.xml
# nav2_bt_navigator/navigate_through_poses_w_replanning_and_recovery.xml
# They can be set here or via a RewrittenYaml remap from a parent launch file to Nav2.
plugin_lib_names:
- nav2_compute_path_to_pose_action_bt_node
- nav2_compute_path_through_poses_action_bt_node
- nav2_smooth_path_action_bt_node
- nav2_follow_path_action_bt_node
- nav2_spin_action_bt_node
- nav2_wait_action_bt_node
- nav2_assisted_teleop_action_bt_node
- nav2_back_up_action_bt_node
- nav2_drive_on_heading_bt_node
- nav2_clear_costmap_service_bt_node
- nav2_is_stuck_condition_bt_node
- nav2_goal_reached_condition_bt_node
- nav2_goal_updated_condition_bt_node
- nav2_globally_updated_goal_condition_bt_node
- nav2_is_path_valid_condition_bt_node
- nav2_initial_pose_received_condition_bt_node
- nav2_reinitialize_global_localization_service_bt_node
- nav2_rate_controller_bt_node
- nav2_distance_controller_bt_node
- nav2_speed_controller_bt_node
- nav2_truncate_path_action_bt_node
- nav2_truncate_path_local_action_bt_node
- nav2_goal_updater_node_bt_node
- nav2_recovery_node_bt_node
- nav2_pipeline_sequence_bt_node
- nav2_round_robin_node_bt_node
- nav2_transform_available_condition_bt_node
- nav2_time_expired_condition_bt_node
- nav2_path_expiring_timer_condition
- nav2_distance_traveled_condition_bt_node
- nav2_single_trigger_bt_node
- nav2_goal_updated_controller_bt_node
- nav2_is_battery_low_condition_bt_node
- nav2_navigate_through_poses_action_bt_node
- nav2_navigate_to_pose_action_bt_node
- nav2_remove_passed_goals_action_bt_node
- nav2_planner_selector_bt_node
- nav2_controller_selector_bt_node
- nav2_goal_checker_selector_bt_node
- nav2_controller_cancel_bt_node
- nav2_path_longer_on_approach_bt_node
- nav2_wait_cancel_bt_node
- nav2_spin_cancel_bt_node
- nav2_back_up_cancel_bt_node
- nav2_assisted_teleop_cancel_bt_node
- nav2_drive_on_heading_cancel_bt_node
- nav2_is_battery_charging_condition_bt_node
bt_navigator_navigate_through_poses_rclcpp_node:
ros__parameters:
use_sim_time: True
bt_navigator_navigate_to_pose_rclcpp_node:
ros__parameters:
use_sim_time: True
controller_server:
ros__parameters:
use_sim_time: True
controller_frequency: 20.0
min_x_velocity_threshold: 0.001
min_y_velocity_threshold: 0.5
min_theta_velocity_threshold: 0.001
failure_tolerance: 0.3
progress_checker_plugin: "progress_checker"
goal_checker_plugins: ["general_goal_checker"] # "precise_goal_checker"
controller_plugins: ["FollowPath"]
# Progress checker parameters
progress_checker:
plugin: "nav2_controller::SimpleProgressChecker"
required_movement_radius: 0.5
movement_time_allowance: 10.0
# Goal checker parameters
#precise_goal_checker:
# plugin: "nav2_controller::SimpleGoalChecker"
# xy_goal_tolerance: 0.05
# yaw_goal_tolerance: 0.05
# stateful: True
general_goal_checker:
stateful: True
plugin: "nav2_controller::SimpleGoalChecker"
xy_goal_tolerance: 0.05
yaw_goal_tolerance: 0.05
# DWB parameters
FollowPath:
plugin: "dwb_core::DWBLocalPlanner"
debug_trajectory_details: True
min_vel_x: 0.0
min_vel_y: 0.0
max_vel_x: 0.26
max_vel_y: 0.0
max_vel_theta: 1.0
min_speed_xy: 0.0
max_speed_xy: 0.26
min_speed_theta: 0.0
# Add high threshold velocity for turtlebot 3 issue.
# https://github.com/ROBOTIS-GIT/turtlebot3_simulations/issues/75
acc_lim_x: 2.5
acc_lim_y: 0.0
acc_lim_theta: 3.2
decel_lim_x: -2.5
decel_lim_y: 0.0
decel_lim_theta: -3.2
vx_samples: 20
vy_samples: 5
vtheta_samples: 20
sim_time: 1.7
linear_granularity: 0.05
angular_granularity: 0.025
transform_tolerance: 5.0
xy_goal_tolerance: 0.05
trans_stopped_velocity: 0.25
short_circuit_trajectory_evaluation: True
stateful: True
critics: ["RotateToGoal", "Oscillation", "BaseObstacle", "GoalAlign", "PathAlign", "PathDist", "GoalDist"]
BaseObstacle.scale: 0.02
PathAlign.scale: 32.0
PathAlign.forward_point_distance: 0.1
GoalAlign.scale: 24.0
GoalAlign.forward_point_distance: 0.1
PathDist.scale: 32.0
GoalDist.scale: 24.0
RotateToGoal.scale: 32.0
RotateToGoal.slowing_factor: 5.0
RotateToGoal.lookahead_time: -1.0
local_costmap:
local_costmap:
ros__parameters:
update_frequency: 5.0
publish_frequency: 2.0
global_frame: odom
robot_base_frame: base_link
use_sim_time: True
rolling_window: true
width: 3
height: 3
resolution: 0.05
robot_radius: 0.22
plugins: ["voxel_layer", "inflation_layer"]
inflation_layer:
plugin: "nav2_costmap_2d::InflationLayer"
cost_scaling_factor: 3.0
inflation_radius: 0.55
voxel_layer:
plugin: "nav2_costmap_2d::VoxelLayer"
enabled: True
publish_voxel_map: True
origin_z: 0.0
z_resolution: 0.05
z_voxels: 16
max_obstacle_height: 2.0
mark_threshold: 0
observation_sources: scan
scan:
topic: /scan_corrected
max_obstacle_height: 2.0
clearing: True
marking: True
data_type: "LaserScan"
raytrace_max_range: 3.0
raytrace_min_range: 0.0
obstacle_max_range: 2.5
obstacle_min_range: 0.0
static_layer:
plugin: "nav2_costmap_2d::StaticLayer"
map_subscribe_transient_local: True
always_send_full_costmap: True
global_costmap:
global_costmap:
ros__parameters:
update_frequency: 1.0
publish_frequency: 1.0
global_frame: map
robot_base_frame: base_link
use_sim_time: True
robot_radius: 0.22
resolution: 0.05
track_unknown_space: true
plugins: ["static_layer", "obstacle_layer", "inflation_layer"]
obstacle_layer:
plugin: "nav2_costmap_2d::ObstacleLayer"
enabled: True
observation_sources: scan
scan:
topic: /scan_corrected
max_obstacle_height: 2.0
clearing: True
marking: True
data_type: "LaserScan"
raytrace_max_range: 3.0
raytrace_min_range: 0.0
obstacle_max_range: 2.5
obstacle_min_range: 0.0
static_layer:
plugin: "nav2_costmap_2d::StaticLayer"
map_subscribe_transient_local: True
inflation_layer:
plugin: "nav2_costmap_2d::InflationLayer"
cost_scaling_factor: 3.0
inflation_radius: 0.55
always_send_full_costmap: True
map_server:
ros__parameters:
use_sim_time: True
# Overridden in launch by the "map" launch configuration or provided default value.
# To use in yaml, remove the default "map" value in the tb3_simulation_launch.py file & provide full path to map below.
yaml_filename: ""
map_saver:
ros__parameters:
use_sim_time: True
save_map_timeout: 5.0
free_thresh_default: 0.25
occupied_thresh_default: 0.65
map_subscribe_transient_local: True
planner_server:
ros__parameters:
expected_planner_frequency: 20.0
use_sim_time: True
planner_plugins: ["GridBased"]
GridBased:
plugin: "nav2_navfn_planner/NavfnPlanner"
tolerance: 0.5
use_astar: false
allow_unknown: true
smoother_server:
ros__parameters:
use_sim_time: True
smoother_plugins: ["simple_smoother"]
simple_smoother:
plugin: "nav2_smoother::SimpleSmoother"
tolerance: 1.0e-10
max_its: 1000
do_refinement: True
behavior_server:
ros__parameters:
costmap_topic: local_costmap/costmap_raw
footprint_topic: local_costmap/published_footprint
cycle_frequency: 10.0
behavior_plugins: ["spin", "backup", "drive_on_heading", "assisted_teleop", "wait"]
spin:
plugin: "nav2_behaviors/Spin"
backup:
plugin: "nav2_behaviors/BackUp"
drive_on_heading:
plugin: "nav2_behaviors/DriveOnHeading"
wait:
plugin: "nav2_behaviors/Wait"
assisted_teleop:
plugin: "nav2_behaviors/AssistedTeleop"
global_frame: odom
robot_base_frame: base_link
transform_tolerance: 5.0
use_sim_time: true
simulate_ahead_time: 2.0
max_rotational_vel: 1.0
min_rotational_vel: 0.4
rotational_acc_lim: 3.2
robot_state_publisher:
ros__parameters:
use_sim_time: True
waypoint_follower:
ros__parameters:
use_sim_time: True
loop_rate: 20
stop_on_failure: false
waypoint_task_executor_plugin: "wait_at_waypoint"
wait_at_waypoint:
plugin: "nav2_waypoint_follower::WaitAtWaypoint"
enabled: True
waypoint_pause_duration: 200
velocity_smoother:
ros__parameters:
use_sim_time: True
smoothing_frequency: 20.0
scale_velocities: False
feedback: "OPEN_LOOP"
max_velocity: [0.26, 0.0, 1.0]
min_velocity: [-0.26, 0.0, -1.0]
max_accel: [2.5, 0.0, 3.2]
max_decel: [-2.5, 0.0, -3.2]
odom_topic: "odom"
odom_duration: 0.1
deadband_velocity: [0.0, 0.0, 0.0]
velocity_timeout: 1.0
+362
View File
@@ -0,0 +1,362 @@
amcl:
ros__parameters:
use_sim_time: False
alpha1: 0.4
alpha2: 0.3
alpha3: 0.1
alpha4: 0.1
alpha5: 0.04
base_frame_id: "base_footprint"
beam_skip_distance: 0.5
beam_skip_error_threshold: 0.9
beam_skip_threshold: 0.3
do_beamskip: false
global_frame_id: "map"
lambda_short: 0.1
laser_likelihood_max_dist: 2.0
laser_max_range: 100.0
laser_min_range: -1.0
laser_model_type: "likelihood_field"
max_beams: 60
max_particles: 2000
min_particles: 500
odom_frame_id: "odom"
pf_err: 0.05
pf_z: 0.99
recovery_alpha_fast: 0.0
recovery_alpha_slow: 0.0
resample_interval: 2
robot_model_type: "nav2_amcl::OmniMotionModel"
scan_topic: scan_corrected
save_pose_rate: 0.5
sigma_hit: 0.02
tf_broadcast: true
transform_tolerance: 5.0
update_min_a: 0.06
update_min_d: 0.025
z_hit: 0.7
z_max: 0.001
z_rand: 0.059
z_short: 0.24
# Initial Pose
set_initial_pose: True
initial_pose.x: 0.0
initial_pose.y: 0.0
initial_pose.z: 0.0
initial_pose.yaw: 0.0
amcl_map_client:
ros__parameters:
use_sim_time: False
amcl_rclcpp_node:
ros__parameters:
use_sim_time: False
bt_navigator:
ros__parameters:
use_sim_time: False
global_frame: map
robot_base_frame: base_footprint
odom_topic: /odom
bt_loop_duration: 10
default_server_timeout: 20
enable_groot_monitoring: True
groot_zmq_publisher_port: 1666
groot_zmq_server_port: 1667
# 'default_nav_through_poses_bt_xml' and 'default_nav_to_pose_bt_xml' are use defaults:
# nav2_bt_navigator/navigate_to_pose_w_replanning_and_recovery.xml
# nav2_bt_navigator/navigate_through_poses_w_replanning_and_recovery.xml
# They can be set here or via a RewrittenYaml remap from a parent launch file to Nav2.
plugin_lib_names:
- nav2_compute_path_to_pose_action_bt_node
- nav2_compute_path_through_poses_action_bt_node
- nav2_follow_path_action_bt_node
- nav2_back_up_action_bt_node
- nav2_spin_action_bt_node
- nav2_wait_action_bt_node
- nav2_clear_costmap_service_bt_node
- nav2_is_stuck_condition_bt_node
- nav2_goal_reached_condition_bt_node
- nav2_goal_updated_condition_bt_node
- nav2_initial_pose_received_condition_bt_node
- nav2_reinitialize_global_localization_service_bt_node
- nav2_rate_controller_bt_node
- nav2_distance_controller_bt_node
- nav2_speed_controller_bt_node
- nav2_truncate_path_action_bt_node
- nav2_goal_updater_node_bt_node
- nav2_recovery_node_bt_node
- nav2_pipeline_sequence_bt_node
- nav2_round_robin_node_bt_node
- nav2_transform_available_condition_bt_node
- nav2_time_expired_condition_bt_node
- nav2_distance_traveled_condition_bt_node
- nav2_single_trigger_bt_node
- nav2_goal_updated_controller_bt_node
- nav2_is_battery_low_condition_bt_node
- nav2_navigate_through_poses_action_bt_node
- nav2_navigate_to_pose_action_bt_node
- nav2_remove_passed_goals_action_bt_node
- nav2_planner_selector_bt_node
- nav2_controller_selector_bt_node
- nav2_goal_checker_selector_bt_node
bt_navigator_rclcpp_node:
ros__parameters:
use_sim_time: False
controller_server:
ros__parameters:
use_sim_time: False
controller_frequency: 10.0
min_x_velocity_threshold: 0.001
min_y_velocity_threshold: 0.5
min_theta_velocity_threshold: 0.001
failure_tolerance: 3.0
progress_checker_plugin: "progress_checker"
goal_checker_plugins: ["general_goal_checker"] # "precise_goal_checker"
controller_plugins: ["FollowPath"]
# Progress checker parameters
progress_checker:
plugin: "nav2_controller::SimpleProgressChecker"
required_movement_radius: 0.15
movement_time_allowance: 8.0
# Goal checker parameters
#precise_goal_checker:
# plugin: "nav2_controller::SimpleGoalChecker"
# xy_goal_tolerance: 0.05
# yaw_goal_tolerance: 0.05
# stateful: True
general_goal_checker:
stateful: True
plugin: "nav2_controller::SimpleGoalChecker"
xy_goal_tolerance: 0.05
yaw_goal_tolerance: 0.05
# DWB parameters
FollowPath:
plugin: "dwb_core::DWBLocalPlanner"
debug_trajectory_details: True
min_vel_x: 0.0
min_vel_y: 0.0
max_vel_x: 0.26
max_vel_y: 0.0
max_vel_theta: 0.5
min_speed_xy: 0.0
max_speed_xy: 0.26
min_speed_theta: 0.0
# Add high threshold velocity for turtlebot 3 issue.
# https://github.com/ROBOTIS-GIT/turtlebot3_simulations/issues/75
acc_lim_x: 2.5
acc_lim_y: 0.0
acc_lim_theta: 0.5
decel_lim_x: -2.5
decel_lim_y: 0.0
decel_lim_theta: -0.5
vx_samples: 20
vy_samples: 5
vtheta_samples: 40
sim_time: 1.7
linear_granularity: 0.05
angular_granularity: 0.025
transform_tolerance: 5.0
xy_goal_tolerance: 0.05
trans_stopped_velocity: 0.1
short_circuit_trajectory_evaluation: True
stateful: True
critics: ["RotateToGoal", "Oscillation", "BaseObstacle", "GoalAlign", "PathAlign", "PathDist", "GoalDist"]
BaseObstacle.scale: 0.02
PathAlign.scale: 23.0
PathAlign.forward_point_distance: 0.1
GoalAlign.scale: 18.0
GoalAlign.forward_point_distance: 0.1
PathDist.scale: 32.0
GoalDist.scale: 24.0
RotateToGoal.scale: 32.0
RotateToGoal.slowing_factor: 5.0
RotateToGoal.lookahead_time: -1.0
controller_server_rclcpp_node:
ros__parameters:
use_sim_time: False
local_costmap:
local_costmap:
ros__parameters:
update_frequency: 5.0
publish_frequency: 2.0
global_frame: odom
robot_base_frame: base_footprint
use_sim_time: False
rolling_window: true
width: 3
height: 3
resolution: 0.05
footprint: "[[0.26, 0.18], [0.26, -0.18], [-0.26, -0.18], [-0.26, 0.18]]"
plugins: ["voxel_layer", "inflation_layer"]
inflation_layer:
plugin: "nav2_costmap_2d::InflationLayer"
cost_scaling_factor: 4.0
inflation_radius: 0.30
voxel_layer:
plugin: "nav2_costmap_2d::VoxelLayer"
enabled: True
publish_voxel_map: True
origin_z: 0.0
z_resolution: 0.05
z_voxels: 16
max_obstacle_height: 2.0
mark_threshold: 0
observation_sources: scan
scan:
topic: /scan_corrected_corrected
max_obstacle_height: 2.0
clearing: True
marking: True
data_type: "LaserScan"
raytrace_max_range: 3.0
raytrace_min_range: 0.0
obstacle_max_range: 2.5
obstacle_min_range: 0.0
static_layer:
map_subscribe_transient_local: True
always_send_full_costmap: True
local_costmap_client:
ros__parameters:
use_sim_time: False
local_costmap_rclcpp_node:
ros__parameters:
use_sim_time: False
global_costmap:
global_costmap:
ros__parameters:
update_frequency: 1.0
publish_frequency: 1.0
global_frame: map
robot_base_frame: base_footprint
use_sim_time: False
footprint: "[[0.26, 0.18], [0.26, -0.18], [-0.26, -0.18], [-0.26, 0.18]]"
resolution: 0.05
track_unknown_space: true
plugins: ["static_layer", "obstacle_layer", "inflation_layer"]
obstacle_layer:
plugin: "nav2_costmap_2d::ObstacleLayer"
enabled: True
observation_sources: scan
scan:
topic: /scan_corrected_corrected
max_obstacle_height: 2.0
clearing: True
marking: True
data_type: "LaserScan"
raytrace_max_range: 3.0
raytrace_min_range: 0.0
obstacle_max_range: 2.5
obstacle_min_range: 0.0
static_layer:
plugin: "nav2_costmap_2d::StaticLayer"
map_subscribe_transient_local: True
inflation_layer:
plugin: "nav2_costmap_2d::InflationLayer"
cost_scaling_factor: 4.0
inflation_radius: 0.30
always_send_full_costmap: True
global_costmap_client:
ros__parameters:
use_sim_time: False
global_costmap_rclcpp_node:
ros__parameters:
use_sim_time: False
map_server:
ros__parameters:
use_sim_time: False
# Overridden in launch by the "map" launch configuration or provided default value.
# To use in yaml, remove the default "map" value in the navigation2_active.launch.py file & provide full path to map below.
yaml_filename: ""
map_saver:
ros__parameters:
use_sim_time: False
save_map_timeout: 5.0
free_thresh_default: 0.25
occupied_thresh_default: 0.65
map_subscribe_transient_local: True
planner_server:
ros__parameters:
expected_planner_frequency: 1.0
use_sim_time: False
planner_plugins: ["GridBased"]
GridBased:
plugin: "nav2_navfn_planner/NavfnPlanner"
tolerance: 2.0
use_astar: false
allow_unknown: true
planner_server_rclcpp_node:
ros__parameters:
use_sim_time: False
smoother_server:
ros__parameters:
use_sim_time: True
smoother_plugins: ["simple_smoother"]
simple_smoother:
plugin: "nav2_smoother::SimpleSmoother"
tolerance: 1.0e-10
max_its: 1000
do_refinement: True
recoveries_server:
ros__parameters:
costmap_topic: local_costmap/costmap_raw
footprint_topic: local_costmap/published_footprint
cycle_frequency: 10.0
recovery_plugins: ["spin", "backup", "wait"]
spin:
plugin: "nav2_recoveries/Spin"
backup:
plugin: "nav2_recoveries/BackUp"
wait:
plugin: "nav2_recoveries/Wait"
global_frame: odom
robot_base_frame: base_footprint
transform_timeout: 0.1
use_sim_time: False
simulate_ahead_time: 2.0
max_rotational_vel: 1.0
min_rotational_vel: 0.4
rotational_acc_lim: 3.2
robot_state_publisher:
ros__parameters:
use_sim_time: False
waypoint_follower:
ros__parameters:
loop_rate: 20
stop_on_failure: false
waypoint_task_executor_plugin: "wait_at_waypoint"
wait_at_waypoint:
plugin: "nav2_waypoint_follower::WaitAtWaypoint"
enabled: True
waypoint_pause_duration: 200
velocity_smoother:
ros__parameters:
use_sim_time: True
smoothing_frequency: 20.0
scale_velocities: False
feedback: "OPEN_LOOP"
max_velocity: [0.26, 0.0, 0.5]
min_velocity: [-0.26, 0.0, -0.5]
max_accel: [2.5, 0.0, 0.5]
max_decel: [-2.5, 0.0, -0.5]
odom_topic: "odom"
odom_duration: 0.1
deadband_velocity: [0.0, 0.0, 0.0]
velocity_timeout: 1.0
@@ -0,0 +1,624 @@
#include "agv_pro_base/agv_pro_driver.h"
uint16_t AGV_PRO::crc16_ibm(const uint8_t* data, size_t length) {
uint16_t crc = 0xFFFF;
for (size_t i = 0; i < length; ++i) {
crc ^= static_cast<uint16_t>(data[i]);
for (int j = 0; j < 8; ++j) {
if (crc & 0x0001)
crc = (crc >> 1) ^ 0xA001;
else
crc = crc >> 1;
}
}
return crc;
}
std::vector<uint8_t> AGV_PRO::build_serial_frame(uint8_t cmd_id, const std::vector<uint8_t>& payload)
{
std::vector<uint8_t> frame(SEND_DATA_SIZE, 0x00);
frame[0] = 0xFE;
frame[1] = 0xFE;
frame[2] = 0x0B;
frame[3] = cmd_id;
for (size_t i = 0; i < payload.size() && i < 8; ++i) {
frame[4 + i] = payload[i];
}
uint16_t crc = crc16_ibm(frame.data(), 12);
frame[12] = (crc >> 8) & 0xff;
frame[13] = crc & 0xff;
return frame;
}
void AGV_PRO::print_hex(const std::string& label, const std::vector<uint8_t>& data, std::optional<size_t> override_size) {
std::stringstream ss;
for (auto b : data) {
ss << std::hex << std::uppercase << std::setfill('0') << std::setw(2)
<< static_cast<int>(b) << " ";
}
size_t len = override_size.value_or(data.size());
RCLCPP_INFO(this->get_logger(), "%s (%zu bytes): [%s]", label.c_str(), len, ss.str().c_str());
}
void AGV_PRO::send_serial_frame(const std::vector<uint8_t>& frame, bool debug)
{
try {
size_t bytes_transmit_size = boost::asio::write(*serial_port_, boost::asio::buffer(frame));
if (debug) {
print_hex("Sent", frame, bytes_transmit_size);
}
} catch (const std::exception &ex) {
RCLCPP_ERROR(this->get_logger(), "Error Transmiting from serial port: %s", ex.what());
}
}
std::vector<uint8_t> AGV_PRO::read_serial_response(
const std::vector<uint8_t>& expected_header,
size_t payload_size,
double timeout_sec)
{
std::vector<uint8_t> sliding_buf;
uint8_t byte = 0;
rclcpp::Time start_time = this->now();
rclcpp::Duration timeout = rclcpp::Duration::from_seconds(timeout_sec);
while ((this->now() - start_time) < timeout) {
boost::asio::mutable_buffers_1 buf(&byte, 1);
boost::system::error_code ec;
size_t n = serial_port_->read_some(buf, ec);
if (ec) {
RCLCPP_WARN(this->get_logger(), "Serial read error: %s", ec.message().c_str());
return {};
}
if (n == 1) {
sliding_buf.push_back(byte);
if (sliding_buf.size() > expected_header.size()) {
sliding_buf.erase(sliding_buf.begin());
}
if (sliding_buf == expected_header) {
break;
}
}
}
if (sliding_buf != expected_header) {
RCLCPP_WARN(this->get_logger(), "Timeout waiting for header");
return {};
}
size_t remain_len = payload_size + 2;
std::vector<uint8_t> remain_buf(remain_len);
size_t total_read = 0;
while (total_read < remain_len && (this->now() - start_time) < timeout) {
boost::asio::mutable_buffers_1 buf(&remain_buf[total_read], remain_len - total_read);
boost::system::error_code ec;
size_t n = serial_port_->read_some(buf, ec);
if (ec) {
RCLCPP_WARN(this->get_logger(), "Serial read error: %s", ec.message().c_str());
return {};
}
total_read += n;
}
if (total_read != remain_len) {
RCLCPP_WARN(this->get_logger(), "Timeout or incomplete data payload");
return {};
}
std::vector<uint8_t> full_buf = expected_header;
full_buf.insert(full_buf.end(), remain_buf.begin(), remain_buf.end());
return full_buf;
}
bool AGV_PRO::is_power_on(){
auto power_query_frame = build_serial_frame(GET_POWER_STATE, {});
send_serial_frame(power_query_frame,true);
const std::vector<uint8_t> expected_header = {0xFE, 0xFE, 0x0B, 0x12};
auto power_query_response = read_serial_response(expected_header, 8, 12.0);
print_hex("recv_buf", power_query_response);
if (power_query_response.size() != 14) return false;
uint16_t received_crc = (power_query_response[12] << 8) | power_query_response[13];
uint16_t computed_crc = crc16_ibm(power_query_response.data(), 12);
if (received_crc != computed_crc) {
RCLCPP_WARN(this->get_logger(), "CRC mismatch: received=0x%04X, expected=0x%04X", received_crc, computed_crc);
return false;
}
int is_poweron_status = static_cast<int8_t>(power_query_response[4]);
RCLCPP_INFO(this->get_logger(), "is_poweron_status: %d", is_poweron_status);
if (is_poweron_status == 0){
auto status_query_frame = build_serial_frame(POWER_ON, {});
send_serial_frame(status_query_frame,true);
rclcpp::sleep_for(std::chrono::milliseconds(1000));// Sleep for 1000 milliseconds to allow the device enough time to process the previous command
const std::vector<uint8_t> expected_header = {0xFE, 0xFE, 0x0B, 0x10};
auto status_query_response = read_serial_response(expected_header, 8, 5.0);// Read the serial response with the specified expected header, payload size, and timeout of 5 seconds
print_hex("recv_buf", status_query_response);
if (status_query_response.size() != 14) return false;
uint16_t received_crc = (status_query_response[12] << 8) | status_query_response[13];
uint16_t computed_crc = crc16_ibm(status_query_response.data(), 12);
if (received_crc != computed_crc) {
RCLCPP_WARN(this->get_logger(), "CRC mismatch: received=0x%04X, expected=0x%04X", received_crc, computed_crc);
return false;
}
int poweron_status = static_cast<int8_t>(status_query_response[4]);
std::string status_msg;
switch (poweron_status) {
case 1:
status_msg = "Motor is operating normally.";
RCLCPP_INFO(this->get_logger(), "power_status: %d, %s", poweron_status, status_msg.c_str());
return true;
case 2:
status_msg = "Emergency stop button is not released.";
RCLCPP_ERROR(this->get_logger(), "power_status: %d, %s", poweron_status, status_msg.c_str());
return false;
case 3:
status_msg = "Battery voltage is below 19.5V.";
RCLCPP_ERROR(this->get_logger(), "power_status: %d, %s", poweron_status, status_msg.c_str());
return false;
case 4:
status_msg = "CAN initialization error.";
RCLCPP_ERROR(this->get_logger(), "power_status: %d, %s", poweron_status, status_msg.c_str());
return false;
case 5:
status_msg = "Motor initialization error.";
RCLCPP_ERROR(this->get_logger(), "power_status: %d, %s", poweron_status, status_msg.c_str());
return false;
default:
RCLCPP_WARN(this->get_logger(), "power_status: %d, Unknown power status code", poweron_status);
return false;
}
}
else{
RCLCPP_INFO(this->get_logger(), "Motor is operating normally.");
return true;
}
}
void AGV_PRO::set_auto_report(bool enable){
auto frame = build_serial_frame(0x23, {static_cast<uint8_t>(enable)});
send_serial_frame(frame,true);
}
void AGV_PRO::clearSerialBuffer(int fd) {
if (tcflush(fd, TCIOFLUSH) < 0) {
RCLCPP_WARN(this->get_logger(), "Failed to flush serial buffer: %s", std::strerror(errno));
} else {
RCLCPP_INFO(this->get_logger(), "Serial buffer flushed.");
}
}
void AGV_PRO::disableDTR_RTS(int fd) {
int status;
if (::ioctl(fd, TIOCMGET, &status) == 0) {
status &= ~(TIOCM_DTR | TIOCM_RTS);
if (::ioctl(fd, TIOCMSET, &status) != 0) {
RCLCPP_WARN(this->get_logger(), "Failed to clear DTR and RTS: %s", std::strerror(errno));
} else {
RCLCPP_INFO(this->get_logger(), "DTR and RTS lines disabled successfully.");
}
} else {
RCLCPP_WARN(this->get_logger(), "Failed to read modem status: %s", std::strerror(errno));
}
}
void AGV_PRO::cmdCallback(const geometry_msgs::msg::Twist::SharedPtr msg)
{
linearX = std::clamp(msg->linear.x, -1.5, 1.5);
linearY = std::clamp(msg->linear.y, -1.0, 1.0);
angularZ = std::clamp(msg->angular.z, -1.0, 1.0);
int16_t x_send = static_cast<int16_t>(linearX * 100);
int16_t y_send = static_cast<int16_t>(linearY * 100);
int16_t rot_send = static_cast<int16_t>(angularZ * 100);
uint8_t buf[14] = { 0xfe,0xfe,0x0b,0x21 };
buf[4] = (x_send >> 8) & 0xff;
buf[5] = x_send & 0xff;
buf[6] = (y_send >> 8) & 0xff;
buf[7] = y_send & 0xff;
buf[8] = (rot_send >> 8) & 0xff;
buf[9] = rot_send & 0xff;
buf[10] = 0x00;
buf[11] = 0x00;
uint16_t crc = crc16_ibm(buf, 12);
buf[12] = (crc >> 8) & 0xff;
buf[13] = crc & 0xff;
std::vector<uint8_t> data_vec(buf, buf + sizeof(buf));
try
{
boost::asio::write(*serial_port_,boost::asio::buffer(data_vec));
// print_hex("Sent", data_vec);//debug
}
catch(const std::exception &ex)
{
RCLCPP_ERROR(this->get_logger(), "Error Transmiting from serial port:%s",ex.what());
}
}
void AGV_PRO::handleSetDigitalOutput(
const std::shared_ptr<agv_pro_msgs::srv::SetDigitalOutput::Request> request,
std::shared_ptr<agv_pro_msgs::srv::SetDigitalOutput::Response> response)
{
uint8_t output_number = request->pin;
uint8_t output_state = request->state;
if (output_number < 1 || output_number > 6){
RCLCPP_ERROR(this->get_logger(), "Invalid output pin number: %u", output_number);
response->success = false;
response->message = "Invalid output pin number";
return;
}
auto frame = build_serial_frame(SET_OUTPUT_IO, {output_number, output_state});
send_serial_frame(frame, true);
const std::vector<uint8_t> expected_header = {0xFE, 0xFE, 0x0B, SET_OUTPUT_IO};
auto response_frame = read_serial_response(expected_header, 8, 5.0);
// print_hex("recv_buf", response_frame); //debug
uint8_t status = response_frame[4];
if (status == 0x01) {
RCLCPP_INFO(this->get_logger(), "SetDigitalOutput succeeded");
response->success = true;
response->message = "Success";
} else {
RCLCPP_ERROR(this->get_logger(), "SetDigitalOutput failed with status: 0x%02X", status);
response->success = false;
response->message = "Failed with status code";
}
}
void AGV_PRO::handleGetDigitalInput(
const std::shared_ptr<agv_pro_msgs::srv::GetDigitalInput::Request> request,
std::shared_ptr<agv_pro_msgs::srv::GetDigitalInput::Response> response)
{
uint8_t input_number = request->pin;
if (input_number < 1 || input_number > 6){
RCLCPP_ERROR(this->get_logger(), "Invalid input pin number: %u", input_number);
response->success = false;
response->message = "Invalid input pin number";
return;
}
auto frame = build_serial_frame(GET_INPUT_IO, {input_number});
send_serial_frame(frame, true);
const std::vector<uint8_t> expected_header = {0xFE, 0xFE, 0x0B, GET_INPUT_IO};
auto response_frame = read_serial_response(expected_header, 8, 5.0);
// print_hex("recv_buf", response_frame); //debug
uint8_t status = response_frame[5];
if (status == 0xff) {
RCLCPP_ERROR(this->get_logger(), "GetDigitalInput failed with status: 0x%02X", status);
response->success = false;
} else {
RCLCPP_INFO(this->get_logger(), "GetDigitalInput succeeded, state: %u", status);
response->state = static_cast<int32_t>(status);
response->success = true;
response->message = "Success";
}
}
bool AGV_PRO::readData()
{
std::vector<uint8_t> buf_length(1);
std::vector<uint8_t> data_buf(RECEIVE_PAYLOAD_SIZE);
uint8_t byte = 0;
boost::system::error_code ec;
while (true)
{
size_t ret = boost::asio::read(*serial_port_, boost::asio::buffer(&byte, 1), ec);
if (ec) {
RCLCPP_ERROR(this->get_logger(), "Serial read error: %s", ec.message().c_str());
return false;
}
if (ret != 1 || byte != 0xfe) {
continue;
}
ret = boost::asio::read(*serial_port_, boost::asio::buffer(&byte, 1), ec);
if (ec) {
RCLCPP_ERROR(this->get_logger(), "Serial read error: %s", ec.message().c_str());
return false;
}
if (ret == 1 && byte == 0xfe) {
break;
}
}
size_t ret = boost::asio::read(*serial_port_, boost::asio::buffer(buf_length), ec);
if (ec) {
RCLCPP_ERROR(this->get_logger(), "Serial read error: %s", ec.message().c_str());
return false;
}
if (buf_length[0] != RECEIVE_PAYLOAD_SIZE) {
//RCLCPP_ERROR(this->get_logger(), "The received length is incorrect:%u", buf_length[0]);
return false;
}
ret = boost::asio::read(*serial_port_, boost::asio::buffer(data_buf), ec);
if (ec || ret != data_buf.size())
{
RCLCPP_ERROR(this->get_logger(), "Failed to receive full payload");
return false;
}
std::vector<uint8_t> recv_buf;
recv_buf.push_back(0xFE);
recv_buf.push_back(0xFE);
recv_buf.push_back(0x1C);
recv_buf.insert(recv_buf.end(), data_buf.begin(), data_buf.end());
// print_hex("recv_buf", recv_buf); //debug
if (recv_buf[3] != 0x25) {
//RCLCPP_WARN(this->get_logger(), "Command error:0x%02X", recv_buf[2]); //debug
return false;
}
uint16_t received_crc = recv_buf[RECEIVE_FRAME_SIZE-1] | (recv_buf[RECEIVE_FRAME_SIZE-2] << 8);
uint16_t computed_crc = crc16_ibm(recv_buf.data(), RECEIVE_FRAME_SIZE-2);
if (received_crc != computed_crc) {
RCLCPP_WARN(this->get_logger(), "CRC error: received 0x%04X, calculated 0x%04X", received_crc, computed_crc);
return false;
}
vx = static_cast<double>(static_cast<int8_t>(recv_buf[4])) * 0.01;
vy = static_cast<double>(static_cast<int8_t>(recv_buf[5])) * 0.01;
vtheta = static_cast<double>(static_cast<int8_t>(recv_buf[6])) * 0.01;
motor_status = recv_buf[7];
motor_error = recv_buf[8];
battery_voltage = static_cast<float>(recv_buf[9]) / 10.0f;
enable_status = recv_buf[10];
imu_data.linear_acceleration.x = static_cast<double>(static_cast<int16_t>((recv_buf[11] << 8) | recv_buf[12])) * 0.01;
imu_data.linear_acceleration.y = static_cast<double>(static_cast<int16_t>((recv_buf[13] << 8) | recv_buf[14])) * 0.01;
imu_data.linear_acceleration.z = static_cast<double>(static_cast<int16_t>((recv_buf[15] << 8) | recv_buf[16])) * 0.01;
imu_data.angular_velocity.x = static_cast<double>(static_cast<int16_t>((recv_buf[17] << 8) | recv_buf[18])) * 0.01;
imu_data.angular_velocity.y = static_cast<double>(static_cast<int16_t>((recv_buf[19] << 8) | recv_buf[20])) * 0.01;
imu_data.angular_velocity.z = static_cast<double>(static_cast<int16_t>((recv_buf[21] << 8) | recv_buf[22])) * 0.01;
roll = static_cast<double>(static_cast<int16_t>((recv_buf[23] << 8) | recv_buf[24])) * 0.01;
pitch = static_cast<double>(static_cast<int16_t>((recv_buf[25] << 8) | recv_buf[26])) * 0.01;
yaw = static_cast<double>(static_cast<int16_t>((recv_buf[27] << 8) | recv_buf[28])) * 0.01;
// RCLCPP_INFO(this->get_logger(),
// "IMU Data - Accel[x: %.2f, y: %.2f, z: %.2f], "
// "Gyro[x: %.2f, y: %.2f, z: %.2f], "
// "RPY[roll: %.2f, pitch: %.2f, yaw: %.2f]",
// imu_data.linear_acceleration.x,
// imu_data.linear_acceleration.y,
// imu_data.linear_acceleration.z,
// imu_data.angular_velocity.x,
// imu_data.angular_velocity.y,
// imu_data.angular_velocity.z,
// roll, pitch, yaw);
return true;
}
void AGV_PRO::publisherVoltage()
{
std_msgs::msg::Float32 voltage_msg,voltage_backup_msg;
voltage_msg.data = battery_voltage;
pub_voltage->publish(voltage_msg);
}
void AGV_PRO::publisherImuSensor()
{
sensor_msgs::msg::Imu ImuSensor;
ImuSensor.header.stamp = this->get_clock()->now();
ImuSensor.header.frame_id = "imu_link";
tf2::Quaternion qua;
qua.setRPY(0, 0, yaw * M_PI / 180.0);
ImuSensor.orientation.x = qua[0];
ImuSensor.orientation.y = qua[1];
ImuSensor.orientation.z = qua[2];
ImuSensor.orientation.w = qua[3];
ImuSensor.angular_velocity.x = imu_data.angular_velocity.x;
ImuSensor.angular_velocity.y = imu_data.angular_velocity.y;
ImuSensor.angular_velocity.z = imu_data.angular_velocity.z;
ImuSensor.linear_acceleration.x = imu_data.linear_acceleration.x;
ImuSensor.linear_acceleration.y = imu_data.linear_acceleration.y;
ImuSensor.linear_acceleration.z = imu_data.linear_acceleration.z;
ImuSensor.orientation_covariance[0] = 1e6;
ImuSensor.orientation_covariance[4] = 1e6;
ImuSensor.orientation_covariance[8] = 1e-6;
ImuSensor.angular_velocity_covariance[0] = 1e6;
ImuSensor.angular_velocity_covariance[4] = 1e6;
ImuSensor.angular_velocity_covariance[8] = 1e-6;
pub_imu->publish(ImuSensor);
}
void AGV_PRO::publisherOdom(double dt)
{
currentTime = this->get_clock()->now();
double delta_x = (vx * cos(theta) - vy * sin(theta)) * dt;
double delta_y = (vx * sin(theta) + vy * cos(theta)) * dt;
double delta_th = vtheta * dt;
x += delta_x;
y += delta_y;
theta += delta_th;
geometry_msgs::msg::TransformStamped odom_trans;
odom_trans.header.stamp = currentTime;
odom_trans.header.frame_id = frame_id_of_odometry_;
odom_trans.child_frame_id = child_frame_id_of_odometry_;
tf2::Quaternion quat;
quat.setRPY(0.0, 0.0, theta);
geometry_msgs::msg::Quaternion odom_quat = tf2::toMsg(quat);
odom_trans.transform.translation.x = x;
odom_trans.transform.translation.y = y;
odom_trans.transform.translation.z = 0.0;
odom_trans.transform.rotation = odom_quat;
odomBroadcaster->sendTransform(odom_trans);
nav_msgs::msg::Odometry odom;
odom.header.stamp = currentTime;
odom.header.frame_id = frame_id_of_odometry_;
odom.child_frame_id = child_frame_id_of_odometry_;
odom.pose.pose.position.x = x;
odom.pose.pose.position.y = y;
odom.pose.pose.position.z = 0.0;
odom.pose.pose.orientation = odom_quat;
odom.pose.covariance = this->odom_pose_covariance;
odom.twist.twist.linear.x = vx;
odom.twist.twist.linear.y = vy;
odom.twist.twist.angular.z = vtheta;
odom.twist.covariance = this->odom_twist_covariance;
pub_odom->publish(odom);
}
void AGV_PRO::Control()
{
if (true == readData())
{
currentTime = this->get_clock()->now();
double dt = 0.0;
if (lastTime.nanoseconds() != 0) {
dt = (currentTime - lastTime).seconds();
}
lastTime = currentTime;
publisherOdom(dt);
// RCLCPP_INFO(this->get_logger(), "dt:%f", dt);
publisherVoltage();
publisherImuSensor();
}
}
AGV_PRO::AGV_PRO(std::string node_name):rclcpp::Node(node_name)
{
this->declare_parameter<std::string>("port_name","/dev/agvpro_controller");
this->declare_parameter<std::string>("odometry.frame_id", "odom");
this->declare_parameter<std::string>("odometry.child_frame_id", "base_footprint");
this->declare_parameter<std::string>("imu.frame_id", "imu_link");
this->declare_parameter<std::string>("namespace", "");
this->get_parameter_or<std::string>("port_name",device_name_,std::string("/dev/agvpro_controller"));
this->get_parameter_or<std::string>("odometry.frame_id",frame_id_of_odometry_,std::string("odom"));
this->get_parameter_or<std::string>("odometry.child_frame_id",child_frame_id_of_odometry_,std::string("base_footprint"));
this->get_parameter_or<std::string>("imu.frame_id",frame_id_of_imu_,std::string("imu_link"));
this->get_parameter_or<std::string>("namespace",name_space_,std::string(""));
if (name_space_ != "") {
frame_id_of_odometry_ = name_space_ + "/" + frame_id_of_odometry_;
child_frame_id_of_odometry_ = name_space_ + "/" + child_frame_id_of_odometry_;
frame_id_of_imu_ = name_space_ + "/" + frame_id_of_imu_;
}
odomBroadcaster = std::make_unique<tf2_ros::TransformBroadcaster>(this);
pub_imu = this->create_publisher<sensor_msgs::msg::Imu>("imu", 20);
pub_odom = this->create_publisher<nav_msgs::msg::Odometry>("odom", rclcpp::SensorDataQoS());
pub_voltage = create_publisher<std_msgs::msg::Float32>("voltage", 10);
cmd_sub = this->create_subscription<geometry_msgs::msg::Twist>(
"/cmd_vel", 10, std::bind(&AGV_PRO::cmdCallback, this, std::placeholders::_1));
set_output_service = this->create_service<agv_pro_msgs::srv::SetDigitalOutput>(
"set_digital_output",
std::bind(&AGV_PRO::handleSetDigitalOutput, this, std::placeholders::_1, std::placeholders::_2)
);
get_input_service = this->create_service<agv_pro_msgs::srv::GetDigitalInput>(
"get_digital_input",
std::bind(&AGV_PRO::handleGetDigitalInput, this, std::placeholders::_1, std::placeholders::_2)
);
lastTime = this->get_clock()->now();
try{
serial_port_ = std::make_unique<boost::asio::serial_port>(io_);
serial_port_->open(device_name_);
serial_port_->set_option(boost::asio::serial_port_base::baud_rate(1000000));
serial_port_->set_option(boost::asio::serial_port_base::character_size(8));
serial_port_->set_option(boost::asio::serial_port_base::parity(boost::asio::serial_port_base::parity::none));
serial_port_->set_option(boost::asio::serial_port_base::stop_bits(boost::asio::serial_port_base::stop_bits::one));
serial_port_->set_option(boost::asio::serial_port_base::flow_control(boost::asio::serial_port_base::flow_control::none));
int fd = serial_port_->native_handle();
this->clearSerialBuffer(fd);
this->disableDTR_RTS(fd);
rclcpp::sleep_for(std::chrono::milliseconds(3000));//esp32 Restart time
RCLCPP_INFO(this->get_logger(), "Serial port initialized successfully");
RCLCPP_INFO(this->get_logger(), "Using device: %s", device_name_.c_str());
boost::asio::serial_port_base::baud_rate baud_option;
serial_port_->get_option(baud_option);
unsigned int current_baud = baud_option.value();
RCLCPP_INFO(this->get_logger(), "Baud_rate: %u", current_baud);
}
catch (const std::exception &ex){
RCLCPP_ERROR(this->get_logger(), "Failed to initialize serial port: %s", ex.what());
return;
}
if (this->is_power_on()) {
this->set_auto_report(1);
control_timer_ = this->create_wall_timer(
std::chrono::milliseconds(20),
std::bind(&AGV_PRO::Control, this)
);
RCLCPP_INFO(this->get_logger(), "Control timer started");
}
else {
RCLCPP_WARN(this->get_logger(), "Control timer not started.");
}
}
AGV_PRO::~AGV_PRO()
{
if (serial_port_ && serial_port_->is_open()) {
this->set_auto_report(0);
serial_port_->cancel();
serial_port_->close();
}
}