完善....
This commit is contained in:
@@ -652,28 +652,11 @@
|
|||||||
},
|
},
|
||||||
"chassis_calibration": {
|
"chassis_calibration": {
|
||||||
"chassis_type": "ackermann",
|
"chassis_type": "ackermann",
|
||||||
"straight_track_length_m": 5.0,
|
|
||||||
"straight_track_width_m": 0.7,
|
|
||||||
"arc_track_radius_m": 1.6,
|
|
||||||
"wheel_radius_m": 0.1,
|
"wheel_radius_m": 0.1,
|
||||||
"wheel_track_m": 0.52,
|
"wheel_track_m": 0.52,
|
||||||
"wheel_base_m": 0.8
|
"wheel_base_m": 0.8
|
||||||
},
|
},
|
||||||
"control_calibration": {
|
"control_calibration": {
|
||||||
"reference_path": [
|
|
||||||
{
|
|
||||||
"x_m": -2.5,
|
|
||||||
"y_m": 0.0,
|
|
||||||
"yaw_rad": 0.0,
|
|
||||||
"target_speed_ms": 0.3
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"x_m": 2.5,
|
|
||||||
"y_m": 0.0,
|
|
||||||
"yaw_rad": 0.0,
|
|
||||||
"target_speed_ms": 0.3
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"parameter_version": "isaac_control_baseline_v1"
|
"parameter_version": "isaac_control_baseline_v1"
|
||||||
},
|
},
|
||||||
"external_truth": {
|
"external_truth": {
|
||||||
@@ -682,7 +665,36 @@
|
|||||||
"workcell_zone_id": "isaac_workcell_zone_a",
|
"workcell_zone_id": "isaac_workcell_zone_a",
|
||||||
"expected_position_stddev_m": 0.01,
|
"expected_position_stddev_m": 0.01,
|
||||||
"expected_yaw_stddev_rad": 0.01,
|
"expected_yaw_stddev_rad": 0.01,
|
||||||
"expected_time_sync_offset_ms": 2.0
|
"expected_time_sync_offset_ms": 2.0,
|
||||||
|
"visualization_enabled": false,
|
||||||
|
"visualization_root_prim": "/World/ExternalTruthVisualization",
|
||||||
|
"target_ball_radius_m": 0.075,
|
||||||
|
"target_balls": [
|
||||||
|
{
|
||||||
|
"ball_id": "front_left",
|
||||||
|
"center_in_base_link": {
|
||||||
|
"x_m": 0.58,
|
||||||
|
"y_m": 0.32,
|
||||||
|
"z_m": 1.28
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ball_id": "front_right",
|
||||||
|
"center_in_base_link": {
|
||||||
|
"x_m": 0.58,
|
||||||
|
"y_m": -0.32,
|
||||||
|
"z_m": 1.28
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ball_id": "rear_center",
|
||||||
|
"center_in_base_link": {
|
||||||
|
"x_m": -0.5,
|
||||||
|
"y_m": 0.0,
|
||||||
|
"z_m": 1.22
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
},
|
},
|
||||||
"sensors": [
|
"sensors": [
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -61,28 +61,11 @@
|
|||||||
},
|
},
|
||||||
"chassis_calibration": {
|
"chassis_calibration": {
|
||||||
"chassis_type": "ackermann",
|
"chassis_type": "ackermann",
|
||||||
"straight_track_length_m": 5.0,
|
|
||||||
"straight_track_width_m": 0.7,
|
|
||||||
"arc_track_radius_m": 1.6,
|
|
||||||
"wheel_radius_m": 0.1,
|
"wheel_radius_m": 0.1,
|
||||||
"wheel_track_m": 0.52,
|
"wheel_track_m": 0.52,
|
||||||
"wheel_base_m": 0.8
|
"wheel_base_m": 0.8
|
||||||
},
|
},
|
||||||
"control_calibration": {
|
"control_calibration": {
|
||||||
"reference_path": [
|
|
||||||
{
|
|
||||||
"x_m": -2.5,
|
|
||||||
"y_m": 0.0,
|
|
||||||
"yaw_rad": 0.0,
|
|
||||||
"target_speed_ms": 0.3
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"x_m": 2.5,
|
|
||||||
"y_m": 0.0,
|
|
||||||
"yaw_rad": 0.0,
|
|
||||||
"target_speed_ms": 0.3
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"parameter_version": "isaac_control_baseline_v1"
|
"parameter_version": "isaac_control_baseline_v1"
|
||||||
},
|
},
|
||||||
"external_truth": {
|
"external_truth": {
|
||||||
@@ -91,7 +74,8 @@
|
|||||||
"workcell_zone_id": "isaac_workcell_zone_a",
|
"workcell_zone_id": "isaac_workcell_zone_a",
|
||||||
"expected_position_stddev_m": 0.01,
|
"expected_position_stddev_m": 0.01,
|
||||||
"expected_yaw_stddev_rad": 0.01,
|
"expected_yaw_stddev_rad": 0.01,
|
||||||
"expected_time_sync_offset_ms": 2.0
|
"expected_time_sync_offset_ms": 2.0,
|
||||||
|
"visualization_enabled": false
|
||||||
},
|
},
|
||||||
"sensors": [
|
"sensors": [
|
||||||
{
|
{
|
||||||
|
|||||||
+2
-18
@@ -61,28 +61,11 @@
|
|||||||
},
|
},
|
||||||
"chassis_calibration": {
|
"chassis_calibration": {
|
||||||
"chassis_type": "ackermann",
|
"chassis_type": "ackermann",
|
||||||
"straight_track_length_m": 5.0,
|
|
||||||
"straight_track_width_m": 0.7,
|
|
||||||
"arc_track_radius_m": 1.6,
|
|
||||||
"wheel_radius_m": 0.1,
|
"wheel_radius_m": 0.1,
|
||||||
"wheel_track_m": 0.52,
|
"wheel_track_m": 0.52,
|
||||||
"wheel_base_m": 0.8
|
"wheel_base_m": 0.8
|
||||||
},
|
},
|
||||||
"control_calibration": {
|
"control_calibration": {
|
||||||
"reference_path": [
|
|
||||||
{
|
|
||||||
"x_m": -2.5,
|
|
||||||
"y_m": 0.0,
|
|
||||||
"yaw_rad": 0.0,
|
|
||||||
"target_speed_ms": 0.3
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"x_m": 2.5,
|
|
||||||
"y_m": 0.0,
|
|
||||||
"yaw_rad": 0.0,
|
|
||||||
"target_speed_ms": 0.3
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"parameter_version": "isaac_control_baseline_v1"
|
"parameter_version": "isaac_control_baseline_v1"
|
||||||
},
|
},
|
||||||
"external_truth": {
|
"external_truth": {
|
||||||
@@ -91,7 +74,8 @@
|
|||||||
"workcell_zone_id": "isaac_workcell_zone_a",
|
"workcell_zone_id": "isaac_workcell_zone_a",
|
||||||
"expected_position_stddev_m": 0.01,
|
"expected_position_stddev_m": 0.01,
|
||||||
"expected_yaw_stddev_rad": 0.01,
|
"expected_yaw_stddev_rad": 0.01,
|
||||||
"expected_time_sync_offset_ms": 2.0
|
"expected_time_sync_offset_ms": 2.0,
|
||||||
|
"visualization_enabled": false
|
||||||
},
|
},
|
||||||
"sensors": [
|
"sensors": [
|
||||||
{
|
{
|
||||||
|
|||||||
+2
-18
@@ -61,28 +61,11 @@
|
|||||||
},
|
},
|
||||||
"chassis_calibration": {
|
"chassis_calibration": {
|
||||||
"chassis_type": "ackermann",
|
"chassis_type": "ackermann",
|
||||||
"straight_track_length_m": 5.0,
|
|
||||||
"straight_track_width_m": 0.7,
|
|
||||||
"arc_track_radius_m": 1.6,
|
|
||||||
"wheel_radius_m": 0.1,
|
"wheel_radius_m": 0.1,
|
||||||
"wheel_track_m": 0.52,
|
"wheel_track_m": 0.52,
|
||||||
"wheel_base_m": 0.8
|
"wheel_base_m": 0.8
|
||||||
},
|
},
|
||||||
"control_calibration": {
|
"control_calibration": {
|
||||||
"reference_path": [
|
|
||||||
{
|
|
||||||
"x_m": -2.5,
|
|
||||||
"y_m": 0.0,
|
|
||||||
"yaw_rad": 0.0,
|
|
||||||
"target_speed_ms": 0.3
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"x_m": 2.5,
|
|
||||||
"y_m": 0.0,
|
|
||||||
"yaw_rad": 0.0,
|
|
||||||
"target_speed_ms": 0.3
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"parameter_version": "isaac_control_baseline_v1"
|
"parameter_version": "isaac_control_baseline_v1"
|
||||||
},
|
},
|
||||||
"external_truth": {
|
"external_truth": {
|
||||||
@@ -91,7 +74,8 @@
|
|||||||
"workcell_zone_id": "isaac_workcell_zone_a",
|
"workcell_zone_id": "isaac_workcell_zone_a",
|
||||||
"expected_position_stddev_m": 0.01,
|
"expected_position_stddev_m": 0.01,
|
||||||
"expected_yaw_stddev_rad": 0.01,
|
"expected_yaw_stddev_rad": 0.01,
|
||||||
"expected_time_sync_offset_ms": 2.0
|
"expected_time_sync_offset_ms": 2.0,
|
||||||
|
"visualization_enabled": false
|
||||||
},
|
},
|
||||||
"sensors": [
|
"sensors": [
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -61,28 +61,11 @@
|
|||||||
},
|
},
|
||||||
"chassis_calibration": {
|
"chassis_calibration": {
|
||||||
"chassis_type": "ackermann",
|
"chassis_type": "ackermann",
|
||||||
"straight_track_length_m": 5.0,
|
|
||||||
"straight_track_width_m": 0.7,
|
|
||||||
"arc_track_radius_m": 1.6,
|
|
||||||
"wheel_radius_m": 0.1,
|
"wheel_radius_m": 0.1,
|
||||||
"wheel_track_m": 0.52,
|
"wheel_track_m": 0.52,
|
||||||
"wheel_base_m": 0.8
|
"wheel_base_m": 0.8
|
||||||
},
|
},
|
||||||
"control_calibration": {
|
"control_calibration": {
|
||||||
"reference_path": [
|
|
||||||
{
|
|
||||||
"x_m": -2.5,
|
|
||||||
"y_m": 0.0,
|
|
||||||
"yaw_rad": 0.0,
|
|
||||||
"target_speed_ms": 0.3
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"x_m": 2.5,
|
|
||||||
"y_m": 0.0,
|
|
||||||
"yaw_rad": 0.0,
|
|
||||||
"target_speed_ms": 0.3
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"parameter_version": "isaac_control_baseline_v1"
|
"parameter_version": "isaac_control_baseline_v1"
|
||||||
},
|
},
|
||||||
"external_truth": {
|
"external_truth": {
|
||||||
@@ -91,7 +74,8 @@
|
|||||||
"workcell_zone_id": "isaac_workcell_zone_a",
|
"workcell_zone_id": "isaac_workcell_zone_a",
|
||||||
"expected_position_stddev_m": 0.01,
|
"expected_position_stddev_m": 0.01,
|
||||||
"expected_yaw_stddev_rad": 0.01,
|
"expected_yaw_stddev_rad": 0.01,
|
||||||
"expected_time_sync_offset_ms": 2.0
|
"expected_time_sync_offset_ms": 2.0,
|
||||||
|
"visualization_enabled": false
|
||||||
},
|
},
|
||||||
"sensors": [
|
"sensors": [
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -61,28 +61,11 @@
|
|||||||
},
|
},
|
||||||
"chassis_calibration": {
|
"chassis_calibration": {
|
||||||
"chassis_type": "ackermann",
|
"chassis_type": "ackermann",
|
||||||
"straight_track_length_m": 5.0,
|
|
||||||
"straight_track_width_m": 0.7,
|
|
||||||
"arc_track_radius_m": 1.6,
|
|
||||||
"wheel_radius_m": 0.1,
|
"wheel_radius_m": 0.1,
|
||||||
"wheel_track_m": 0.52,
|
"wheel_track_m": 0.52,
|
||||||
"wheel_base_m": 0.8
|
"wheel_base_m": 0.8
|
||||||
},
|
},
|
||||||
"control_calibration": {
|
"control_calibration": {
|
||||||
"reference_path": [
|
|
||||||
{
|
|
||||||
"x_m": -2.5,
|
|
||||||
"y_m": 0.0,
|
|
||||||
"yaw_rad": 0.0,
|
|
||||||
"target_speed_ms": 0.3
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"x_m": 2.5,
|
|
||||||
"y_m": 0.0,
|
|
||||||
"yaw_rad": 0.0,
|
|
||||||
"target_speed_ms": 0.3
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"parameter_version": "isaac_control_baseline_v1"
|
"parameter_version": "isaac_control_baseline_v1"
|
||||||
},
|
},
|
||||||
"external_truth": {
|
"external_truth": {
|
||||||
@@ -91,7 +74,8 @@
|
|||||||
"workcell_zone_id": "isaac_workcell_zone_a",
|
"workcell_zone_id": "isaac_workcell_zone_a",
|
||||||
"expected_position_stddev_m": 0.01,
|
"expected_position_stddev_m": 0.01,
|
||||||
"expected_yaw_stddev_rad": 0.01,
|
"expected_yaw_stddev_rad": 0.01,
|
||||||
"expected_time_sync_offset_ms": 2.0
|
"expected_time_sync_offset_ms": 2.0,
|
||||||
|
"visualization_enabled": false
|
||||||
},
|
},
|
||||||
"sensors": [
|
"sensors": [
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ CONDA_ENV_NAME="${CONDA_ENV_NAME:-AutoCalib_Workshop}"
|
|||||||
ROS_LOG_DIR="${ROS_LOG_DIR:-/tmp/roslog}"
|
ROS_LOG_DIR="${ROS_LOG_DIR:-/tmp/roslog}"
|
||||||
RUN_LOG_ROOT="${RUN_LOG_ROOT:-${WORKSPACE_DIR}/log/isaac_real_sim_$(date +%Y%m%d_%H%M%S)}"
|
RUN_LOG_ROOT="${RUN_LOG_ROOT:-${WORKSPACE_DIR}/log/isaac_real_sim_$(date +%Y%m%d_%H%M%S)}"
|
||||||
|
|
||||||
HEADLESS=1
|
HEADLESS=0
|
||||||
DO_BUILD=0
|
DO_BUILD=0
|
||||||
RUN_SMOKE=1
|
RUN_SMOKE=1
|
||||||
KEEP_RUNNING=0
|
KEEP_RUNNING=0
|
||||||
@@ -499,6 +499,7 @@ log "工作空间: ${WORKSPACE_DIR}"
|
|||||||
log "ROS_LOG_DIR: ${ROS_LOG_DIR}"
|
log "ROS_LOG_DIR: ${ROS_LOG_DIR}"
|
||||||
log "进程日志目录: ${RUN_LOG_ROOT}"
|
log "进程日志目录: ${RUN_LOG_ROOT}"
|
||||||
log "Isaac conda 环境: ${CONDA_ENV_NAME}"
|
log "Isaac conda 环境: ${CONDA_ENV_NAME}"
|
||||||
|
log "Isaac headless: ${HEADLESS}"
|
||||||
log "总控底盘/运控模式 use_gateway=${WORKSHOP_USE_GATEWAY_VALUE}"
|
log "总控底盘/运控模式 use_gateway=${WORKSHOP_USE_GATEWAY_VALUE}"
|
||||||
if [[ -n "${DATA_INPUT_PARAMS_FILE}" ]]; then
|
if [[ -n "${DATA_INPUT_PARAMS_FILE}" ]]; then
|
||||||
log "数据输入参数文件: ${DATA_INPUT_PARAMS_FILE}"
|
log "数据输入参数文件: ${DATA_INPUT_PARAMS_FILE}"
|
||||||
@@ -589,13 +590,19 @@ start_bg "workshop-demo" bash -lc "
|
|||||||
source '${WORKSPACE_DIR}/install/setup.bash'
|
source '${WORKSPACE_DIR}/install/setup.bash'
|
||||||
set -u
|
set -u
|
||||||
export ROS_LOG_DIR='${ROS_LOG_DIR}'
|
export ROS_LOG_DIR='${ROS_LOG_DIR}'
|
||||||
exec ros2 launch win_ubuntu_bridge minimal_workshop_demo.launch.py \
|
launch_args=(
|
||||||
use_gateway:=${WORKSHOP_USE_GATEWAY_VALUE} \
|
'use_gateway:=${WORKSHOP_USE_GATEWAY_VALUE}'
|
||||||
chassis_host:=127.0.0.1 \
|
'chassis_host:=127.0.0.1'
|
||||||
control_host:=127.0.0.1 \
|
'control_host:=127.0.0.1'
|
||||||
sensor_registry:=demo_front_camera,demo_down_camera,demo_lidar_3d,demo_lidar_2d,demo_imu \
|
'sensor_registry:=demo_front_camera,demo_down_camera,demo_lidar_3d,demo_lidar_2d,demo_imu'
|
||||||
data_input_params_file:='${DATA_INPUT_PARAMS_FILE}' \
|
)
|
||||||
dataset_index_file:='${DATASET_INDEX_FILE}'
|
if [[ -n '${DATA_INPUT_PARAMS_FILE}' ]]; then
|
||||||
|
launch_args+=('data_input_params_file:=${DATA_INPUT_PARAMS_FILE}')
|
||||||
|
fi
|
||||||
|
if [[ -n '${DATASET_INDEX_FILE}' ]]; then
|
||||||
|
launch_args+=('dataset_index_file:=${DATASET_INDEX_FILE}')
|
||||||
|
fi
|
||||||
|
exec ros2 launch win_ubuntu_bridge minimal_workshop_demo.launch.py \"\${launch_args[@]}\"
|
||||||
"
|
"
|
||||||
|
|
||||||
wait_for_ros_service "/workshop_v2/create_session" 60 || exit 1
|
wait_for_ros_service "/workshop_v2/create_session" 60 || exit 1
|
||||||
|
|||||||
@@ -19,6 +19,12 @@ source install/setup.bash
|
|||||||
python3 src/apps/operator_ui/main.py
|
python3 src/apps/operator_ui/main.py
|
||||||
```
|
```
|
||||||
|
|
||||||
|
仿真联调时可以让界面直接加载仿真 profile:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
AGV_OPERATOR_SITE_PROFILE=src/deployment/profiles/sim_workshop.yaml python3 src/apps/operator_ui/main.py
|
||||||
|
```
|
||||||
|
|
||||||
如果环境里还没有界面依赖:
|
如果环境里还没有界面依赖:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
@@ -50,6 +56,29 @@ pip install -r src/apps/operator_ui/requirements.txt
|
|||||||
8. 点击“开始执行本轮标定”,在“报告”页查看最终结果。
|
8. 点击“开始执行本轮标定”,在“报告”页查看最终结果。
|
||||||
9. 验证结束后点击“停止现场服务”。
|
9. 验证结束后点击“停止现场服务”。
|
||||||
|
|
||||||
|
## Isaac 中测试底盘选路采集
|
||||||
|
|
||||||
|
如果只是验证“底盘标定流程里能不能按选定路径让车动起来、采集底盘遥测和外部真值”,先启动 Isaac 仿真链路:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./run_isaac_real_sim_test.sh --no-smoke --keep-running
|
||||||
|
```
|
||||||
|
|
||||||
|
再启动 UI:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
AGV_OPERATOR_SITE_PROFILE=src/deployment/profiles/sim_workshop.yaml python3 src/apps/operator_ui/main.py
|
||||||
|
```
|
||||||
|
|
||||||
|
在“底盘标定”页选择底盘类型和“参考路径”,然后点击“开始底盘路径采集测试”。该按钮会调用
|
||||||
|
`run_chassis_profile_capture.py`,自动传入当前选中的 `reference_path_id`,并把采集结果写到 `/tmp/agv_calib_chassis_sim/ui_*`。
|
||||||
|
|
||||||
|
## 在 UI 中录制参考路径
|
||||||
|
|
||||||
|
底盘标定、运控参数和传感器标定页的“参考路径”下拉框旁都有“录制”和“停止录制”按钮。点击“录制”后填写中文名称、路径 ID、录制来源、话题、路径类型和目标速度;录制来源可选外部真值定位或底盘遥测。录制时长填 `0` 表示一直录,点击“停止录制”后会收尾并把单条路径 YAML 写入对应模块的 `reference_paths/` 目录。
|
||||||
|
|
||||||
|
底盘路径会按当前底盘类型写入 `allowed_chassis_types`,因此录制完成后只会出现在对应底盘类型下。需要注意的是,“开始底盘路径采集测试”仍然走 `chassis_action_profile.yaml` 的动作原语执行链路;如果一条新录制底盘路径还没有绑定到动作 profile,UI 会阻止直接执行,只把它作为参考路径文件用于路径管理和任务预览。
|
||||||
|
|
||||||
## 注意事项
|
## 注意事项
|
||||||
|
|
||||||
- “跳过网络质量检查(仅调试)”只适合本地联调,真实部署默认不要勾选。
|
- “跳过网络质量检查(仅调试)”只适合本地联调,真实部署默认不要勾选。
|
||||||
@@ -65,6 +94,7 @@ pip install -r src/apps/operator_ui/requirements.txt
|
|||||||
- “车间定位”窗口只显示 3D 位置、本轮计划轨迹和定位数据,不允许修改固定 topic 或定位系统名称。
|
- “车间定位”窗口只显示 3D 位置、本轮计划轨迹和定位数据,不允许修改固定 topic 或定位系统名称。
|
||||||
- 3D 车间图支持鼠标左键拖动旋转视角,左键双击恢复默认视角。
|
- 3D 车间图支持鼠标左键拖动旋转视角,左键双击恢复默认视角。
|
||||||
- 本轮计划轨迹用绿色点显示,来自当前勾选的底盘动作和运控轨迹参数,不要求操作员额外输入。
|
- 本轮计划轨迹用绿色点显示,来自当前勾选的底盘动作和运控轨迹参数,不要求操作员额外输入。
|
||||||
|
- 底盘标定、运控参数标定和传感器标定页都有“参考路径”下拉框,选项来自对应模块的 `reference_paths/*.yaml`;运控轨迹跟踪会把选中的路径展开为实际下发的轨迹点。
|
||||||
- 标定流程运行时,界面订阅 `/workshop_v2/events`,上一项任务完成后会自动切到下一项需要动车采集的轨迹。
|
- 标定流程运行时,界面订阅 `/workshop_v2/events`,上一项任务完成后会自动切到下一项需要动车采集的轨迹。
|
||||||
- 界面会把明细选择写入本轮任务文件,并让标定流程读取这些任务。
|
- 界面会把明细选择写入本轮任务文件,并让标定流程读取这些任务。
|
||||||
- 界面通过现有 CLI 和 ROS launch 运行流程,没有绕过总控。
|
- 界面通过现有 CLI 和 ROS launch 运行流程,没有绕过总控。
|
||||||
|
|||||||
@@ -2,13 +2,26 @@
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import os
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
REPO_ROOT = Path(__file__).resolve().parents[3]
|
REPO_ROOT = Path(__file__).resolve().parents[3]
|
||||||
DEFAULT_SITE_PROFILE = REPO_ROOT / "src/deployment/profiles/site_template.yaml"
|
_DEFAULT_SITE_PROFILE_RAW = Path(
|
||||||
|
os.environ.get("AGV_OPERATOR_SITE_PROFILE", "src/deployment/profiles/site_template.yaml")
|
||||||
|
).expanduser()
|
||||||
|
DEFAULT_SITE_PROFILE = (
|
||||||
|
_DEFAULT_SITE_PROFILE_RAW.resolve(strict=False)
|
||||||
|
if _DEFAULT_SITE_PROFILE_RAW.is_absolute()
|
||||||
|
else (REPO_ROOT / _DEFAULT_SITE_PROFILE_RAW).resolve(strict=False)
|
||||||
|
)
|
||||||
DEFAULT_VEHICLE_PROFILE_DIR = REPO_ROOT / "src/deployment/profiles/vehicle_profiles"
|
DEFAULT_VEHICLE_PROFILE_DIR = REPO_ROOT / "src/deployment/profiles/vehicle_profiles"
|
||||||
DEFAULT_VEHICLE_PROFILE = DEFAULT_VEHICLE_PROFILE_DIR / "ackermann_default.yaml"
|
DEFAULT_VEHICLE_PROFILE = DEFAULT_VEHICLE_PROFILE_DIR / "ackermann_default.yaml"
|
||||||
|
DEFAULT_REFERENCE_PATH_DIRS = {
|
||||||
|
"chassis": REPO_ROOT / "src/site_deployment/workshop_chassis_calibration_real/reference_paths",
|
||||||
|
"control": REPO_ROOT / "src/site_deployment/workshop_control_calibration_real/reference_paths",
|
||||||
|
"sensor": REPO_ROOT / "src/site_deployment/workshop_sensor_calibration_real/reference_paths",
|
||||||
|
}
|
||||||
VEHICLE_PROFILE_SAVE_EXTENSION = ".ymal"
|
VEHICLE_PROFILE_SAVE_EXTENSION = ".ymal"
|
||||||
DEFAULT_SESSION_CONFIG = Path("/tmp/agv_calib_operator_ui/workshop_session_config.yaml")
|
DEFAULT_SESSION_CONFIG = Path("/tmp/agv_calib_operator_ui/workshop_session_config.yaml")
|
||||||
PLACEHOLDER_MARKERS = ("replace_with", "measured_on_site", "session_xxx")
|
PLACEHOLDER_MARKERS = ("replace_with", "measured_on_site", "session_xxx")
|
||||||
@@ -373,5 +386,3 @@ for sensor_key, sensor_label, subtype, _label, stage_type in SENSOR_TASK_OPTIONS
|
|||||||
SENSOR_STAGE_DISPLAY_NAMES[task_code] = f"{sensor_label}外参标定"
|
SENSOR_STAGE_DISPLAY_NAMES[task_code] = f"{sensor_label}外参标定"
|
||||||
elif stage_type == STAGE_HAND_EYE:
|
elif stage_type == STAGE_HAND_EYE:
|
||||||
SENSOR_STAGE_DISPLAY_NAMES[task_code] = f"{sensor_label}手眼标定"
|
SENSOR_STAGE_DISPLAY_NAMES[task_code] = f"{sensor_label}手眼标定"
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -80,6 +80,9 @@ class OperatorMainWindow(
|
|||||||
self.generated_session_config: dict[str, Any] = {}
|
self.generated_session_config: dict[str, Any] = {}
|
||||||
self.launch_process = QProcess(self)
|
self.launch_process = QProcess(self)
|
||||||
self.smoke_process = QProcess(self)
|
self.smoke_process = QProcess(self)
|
||||||
|
self.chassis_capture_process = QProcess(self)
|
||||||
|
self.reference_path_record_process = QProcess(self)
|
||||||
|
self.reference_path_record_module = ""
|
||||||
self.report_metadata: dict[str, str] = {}
|
self.report_metadata: dict[str, str] = {}
|
||||||
self.report_stages: list[dict[str, str]] = []
|
self.report_stages: list[dict[str, str]] = []
|
||||||
self.last_unresolved_signature = ""
|
self.last_unresolved_signature = ""
|
||||||
@@ -97,6 +100,8 @@ class OperatorMainWindow(
|
|||||||
|
|
||||||
self._setup_process(self.launch_process, "现场服务")
|
self._setup_process(self.launch_process, "现场服务")
|
||||||
self._setup_process(self.smoke_process, "标定流程")
|
self._setup_process(self.smoke_process, "标定流程")
|
||||||
|
self._setup_process(self.chassis_capture_process, "底盘路径采集")
|
||||||
|
self._setup_process(self.reference_path_record_process, "参考路径录制")
|
||||||
self._build_ui()
|
self._build_ui()
|
||||||
self._connect_live_refresh()
|
self._connect_live_refresh()
|
||||||
self.load_profile()
|
self.load_profile()
|
||||||
@@ -302,16 +307,24 @@ class OperatorMainWindow(
|
|||||||
self.stop_launch_btn = QPushButton("停止现场服务")
|
self.stop_launch_btn = QPushButton("停止现场服务")
|
||||||
self.smoke_btn = QPushButton("开始执行本轮标定")
|
self.smoke_btn = QPushButton("开始执行本轮标定")
|
||||||
self.stop_smoke_btn = QPushButton("停止本轮标定")
|
self.stop_smoke_btn = QPushButton("停止本轮标定")
|
||||||
|
self.chassis_capture_btn = QPushButton("开始底盘路径采集测试")
|
||||||
|
self.stop_chassis_capture_btn = QPushButton("停止底盘路径采集")
|
||||||
self.build_config_btn.clicked.connect(self.build_session_config)
|
self.build_config_btn.clicked.connect(self.build_session_config)
|
||||||
self.launch_btn.clicked.connect(self.start_launch_stack)
|
self.launch_btn.clicked.connect(self.start_launch_stack)
|
||||||
self.stop_launch_btn.clicked.connect(lambda: self.stop_process(self.launch_process, "现场服务"))
|
self.stop_launch_btn.clicked.connect(lambda: self.stop_process(self.launch_process, "现场服务"))
|
||||||
self.smoke_btn.clicked.connect(self.start_smoke_test)
|
self.smoke_btn.clicked.connect(self.start_smoke_test)
|
||||||
self.stop_smoke_btn.clicked.connect(lambda: self.stop_process(self.smoke_process, "标定流程"))
|
self.stop_smoke_btn.clicked.connect(lambda: self.stop_process(self.smoke_process, "标定流程"))
|
||||||
|
self.chassis_capture_btn.clicked.connect(self.start_chassis_path_capture)
|
||||||
|
self.stop_chassis_capture_btn.clicked.connect(
|
||||||
|
lambda: self.stop_process(self.chassis_capture_process, "底盘路径采集")
|
||||||
|
)
|
||||||
action_layout.addWidget(self.build_config_btn, 0, 0)
|
action_layout.addWidget(self.build_config_btn, 0, 0)
|
||||||
action_layout.addWidget(self.launch_btn, 0, 1)
|
action_layout.addWidget(self.launch_btn, 0, 1)
|
||||||
action_layout.addWidget(self.stop_launch_btn, 1, 1)
|
action_layout.addWidget(self.stop_launch_btn, 1, 1)
|
||||||
action_layout.addWidget(self.smoke_btn, 1, 0)
|
action_layout.addWidget(self.smoke_btn, 1, 0)
|
||||||
action_layout.addWidget(self.stop_smoke_btn, 2, 0, 1, 2)
|
action_layout.addWidget(self.stop_smoke_btn, 2, 0, 1, 2)
|
||||||
|
action_layout.addWidget(self.chassis_capture_btn, 3, 0)
|
||||||
|
action_layout.addWidget(self.stop_chassis_capture_btn, 3, 1)
|
||||||
layout.addWidget(action_box)
|
layout.addWidget(action_box)
|
||||||
layout.addStretch(1)
|
layout.addStretch(1)
|
||||||
return panel
|
return panel
|
||||||
@@ -325,10 +338,26 @@ class OperatorMainWindow(
|
|||||||
for value, label in CHASSIS_TYPES:
|
for value, label in CHASSIS_TYPES:
|
||||||
self.chassis_type_combo.addItem(label, value)
|
self.chassis_type_combo.addItem(label, value)
|
||||||
self.chassis_type_combo.currentIndexChanged.connect(self.rebuild_chassis_parameter_checks)
|
self.chassis_type_combo.currentIndexChanged.connect(self.rebuild_chassis_parameter_checks)
|
||||||
|
self.chassis_type_combo.currentIndexChanged.connect(self.refresh_reference_path_combos)
|
||||||
type_row.addWidget(self.chassis_type_combo)
|
type_row.addWidget(self.chassis_type_combo)
|
||||||
type_row.addStretch(1)
|
type_row.addStretch(1)
|
||||||
layout.addLayout(type_row)
|
layout.addLayout(type_row)
|
||||||
|
|
||||||
|
path_row = QHBoxLayout()
|
||||||
|
path_row.addWidget(QLabel("参考路径"))
|
||||||
|
self.chassis_reference_path_combo = QComboBox()
|
||||||
|
self.chassis_reference_path_combo.currentIndexChanged.connect(self.refresh_command_preview)
|
||||||
|
path_row.addWidget(self.chassis_reference_path_combo, 1)
|
||||||
|
self.record_chassis_path_btn = QPushButton("录制")
|
||||||
|
self.stop_chassis_path_record_btn = QPushButton("停止录制")
|
||||||
|
self.record_chassis_path_btn.clicked.connect(lambda: self.start_reference_path_recording("chassis"))
|
||||||
|
self.stop_chassis_path_record_btn.clicked.connect(
|
||||||
|
lambda: self.stop_process(self.reference_path_record_process, "参考路径录制")
|
||||||
|
)
|
||||||
|
path_row.addWidget(self.record_chassis_path_btn)
|
||||||
|
path_row.addWidget(self.stop_chassis_path_record_btn)
|
||||||
|
layout.addLayout(path_row)
|
||||||
|
|
||||||
self.chassis_param_area = QWidget()
|
self.chassis_param_area = QWidget()
|
||||||
self.chassis_param_layout = QVBoxLayout(self.chassis_param_area)
|
self.chassis_param_layout = QVBoxLayout(self.chassis_param_area)
|
||||||
self.chassis_param_layout.setContentsMargins(0, 0, 0, 0)
|
self.chassis_param_layout.setContentsMargins(0, 0, 0, 0)
|
||||||
@@ -341,6 +370,21 @@ class OperatorMainWindow(
|
|||||||
def _build_control_task_tab(self) -> QWidget:
|
def _build_control_task_tab(self) -> QWidget:
|
||||||
tab = QWidget()
|
tab = QWidget()
|
||||||
layout = QVBoxLayout(tab)
|
layout = QVBoxLayout(tab)
|
||||||
|
path_row = QHBoxLayout()
|
||||||
|
path_row.addWidget(QLabel("参考路径"))
|
||||||
|
self.control_reference_path_combo = QComboBox()
|
||||||
|
self.control_reference_path_combo.currentIndexChanged.connect(self.refresh_command_preview)
|
||||||
|
path_row.addWidget(self.control_reference_path_combo, 1)
|
||||||
|
self.record_control_path_btn = QPushButton("录制")
|
||||||
|
self.stop_control_path_record_btn = QPushButton("停止录制")
|
||||||
|
self.record_control_path_btn.clicked.connect(lambda: self.start_reference_path_recording("control"))
|
||||||
|
self.stop_control_path_record_btn.clicked.connect(
|
||||||
|
lambda: self.stop_process(self.reference_path_record_process, "参考路径录制")
|
||||||
|
)
|
||||||
|
path_row.addWidget(self.record_control_path_btn)
|
||||||
|
path_row.addWidget(self.stop_control_path_record_btn)
|
||||||
|
layout.addLayout(path_row)
|
||||||
|
|
||||||
self.control_param_checks: dict[str, QCheckBox] = {}
|
self.control_param_checks: dict[str, QCheckBox] = {}
|
||||||
for option in CONTROL_PARAMETER_OPTIONS:
|
for option in CONTROL_PARAMETER_OPTIONS:
|
||||||
check = QCheckBox(f"{option['label']} · {option['axis']} · {option['algorithm']}")
|
check = QCheckBox(f"{option['label']} · {option['axis']} · {option['algorithm']}")
|
||||||
@@ -354,6 +398,21 @@ class OperatorMainWindow(
|
|||||||
def _build_sensor_task_tab(self) -> QWidget:
|
def _build_sensor_task_tab(self) -> QWidget:
|
||||||
tab = QWidget()
|
tab = QWidget()
|
||||||
layout = QVBoxLayout(tab)
|
layout = QVBoxLayout(tab)
|
||||||
|
path_row = QHBoxLayout()
|
||||||
|
path_row.addWidget(QLabel("参考路径"))
|
||||||
|
self.sensor_reference_path_combo = QComboBox()
|
||||||
|
self.sensor_reference_path_combo.currentIndexChanged.connect(self.refresh_command_preview)
|
||||||
|
path_row.addWidget(self.sensor_reference_path_combo, 1)
|
||||||
|
self.record_sensor_path_btn = QPushButton("录制")
|
||||||
|
self.stop_sensor_path_record_btn = QPushButton("停止录制")
|
||||||
|
self.record_sensor_path_btn.clicked.connect(lambda: self.start_reference_path_recording("sensor"))
|
||||||
|
self.stop_sensor_path_record_btn.clicked.connect(
|
||||||
|
lambda: self.stop_process(self.reference_path_record_process, "参考路径录制")
|
||||||
|
)
|
||||||
|
path_row.addWidget(self.record_sensor_path_btn)
|
||||||
|
path_row.addWidget(self.stop_sensor_path_record_btn)
|
||||||
|
layout.addLayout(path_row)
|
||||||
|
|
||||||
self.sensor_id_edits: dict[str, QLineEdit] = {
|
self.sensor_id_edits: dict[str, QLineEdit] = {
|
||||||
"front_camera": QLineEdit("demo_front_camera"),
|
"front_camera": QLineEdit("demo_front_camera"),
|
||||||
"down_camera": QLineEdit("demo_down_camera"),
|
"down_camera": QLineEdit("demo_down_camera"),
|
||||||
@@ -391,7 +450,7 @@ class OperatorMainWindow(
|
|||||||
chassis_type = self.chassis_type_combo.currentData() or "ackermann"
|
chassis_type = self.chassis_type_combo.currentData() or "ackermann"
|
||||||
for option in CHASSIS_PARAMETER_OPTIONS[str(chassis_type)]:
|
for option in CHASSIS_PARAMETER_OPTIONS[str(chassis_type)]:
|
||||||
check = QCheckBox(f"{option['label']} · {option['target']}")
|
check = QCheckBox(f"{option['label']} · {option['target']}")
|
||||||
check.setChecked(True)
|
check.setChecked(False)
|
||||||
check.stateChanged.connect(self.refresh_command_preview)
|
check.stateChanged.connect(self.refresh_command_preview)
|
||||||
self.chassis_param_checks[str(option["code"])] = check
|
self.chassis_param_checks[str(option["code"])] = check
|
||||||
self.chassis_param_layout.addWidget(check)
|
self.chassis_param_layout.addWidget(check)
|
||||||
@@ -476,6 +535,8 @@ class OperatorMainWindow(
|
|||||||
return panel
|
return panel
|
||||||
|
|
||||||
def closeEvent(self, event) -> None:
|
def closeEvent(self, event) -> None:
|
||||||
|
self.stop_process(self.reference_path_record_process, "参考路径录制")
|
||||||
|
self.stop_process(self.chassis_capture_process, "底盘路径采集")
|
||||||
self.stop_process(self.smoke_process, "标定流程")
|
self.stop_process(self.smoke_process, "标定流程")
|
||||||
self.stop_process(self.launch_process, "现场服务")
|
self.stop_process(self.launch_process, "现场服务")
|
||||||
if self.localization_spin_timer.isActive():
|
if self.localization_spin_timer.isActive():
|
||||||
|
|||||||
@@ -4,19 +4,328 @@ from __future__ import annotations
|
|||||||
|
|
||||||
import re
|
import re
|
||||||
import sys
|
import sys
|
||||||
|
from datetime import datetime
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
from PySide6.QtCore import QProcess
|
from PySide6.QtCore import QProcess
|
||||||
from PySide6.QtWidgets import QMessageBox
|
from PySide6.QtWidgets import (
|
||||||
|
QCheckBox,
|
||||||
|
QComboBox,
|
||||||
|
QDialog,
|
||||||
|
QDialogButtonBox,
|
||||||
|
QFormLayout,
|
||||||
|
QLineEdit,
|
||||||
|
QMessageBox,
|
||||||
|
QVBoxLayout,
|
||||||
|
)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from .profile_io import launch_arg, shell_join
|
from .constants import REPO_ROOT
|
||||||
|
from .profile_io import launch_arg, read_nested, shell_join
|
||||||
from .ui_helpers import table_item
|
from .ui_helpers import table_item
|
||||||
except ImportError:
|
except ImportError:
|
||||||
from profile_io import launch_arg, shell_join
|
from constants import REPO_ROOT
|
||||||
|
from profile_io import launch_arg, read_nested, shell_join
|
||||||
from ui_helpers import table_item
|
from ui_helpers import table_item
|
||||||
|
|
||||||
|
|
||||||
|
REFERENCE_PATH_RECORDER = "src/site_deployment/workshop_reference_paths/record_calibration_reference_path.py"
|
||||||
|
RECORD_MODULE_LABELS = {
|
||||||
|
"chassis": "底盘标定",
|
||||||
|
"control": "运控参数",
|
||||||
|
"sensor": "传感器标定",
|
||||||
|
}
|
||||||
|
RECORD_DEFAULT_PATH_TYPES = {
|
||||||
|
"chassis": "recorded",
|
||||||
|
"control": "recorded",
|
||||||
|
"sensor": "recorded",
|
||||||
|
}
|
||||||
|
RECORD_DEFAULT_RECOMMENDED_TASKS = {
|
||||||
|
"chassis": "recorded",
|
||||||
|
"control": "trajectory_tracking",
|
||||||
|
"sensor": "sensor_extrinsic",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class ReferencePathRecordDialog(QDialog):
|
||||||
|
def __init__(self, module: str, defaults: dict[str, str], parent=None) -> None:
|
||||||
|
super().__init__(parent)
|
||||||
|
self.setWindowTitle(f"录制{RECORD_MODULE_LABELS.get(module, module)}参考路径")
|
||||||
|
self.module = module
|
||||||
|
self.default_topics = {
|
||||||
|
"external_pose": defaults.get("external_pose_topic", ""),
|
||||||
|
"chassis_telemetry": defaults.get("chassis_telemetry_topic", ""),
|
||||||
|
}
|
||||||
|
|
||||||
|
layout = QVBoxLayout(self)
|
||||||
|
form = QFormLayout()
|
||||||
|
layout.addLayout(form)
|
||||||
|
|
||||||
|
self.display_name_edit = QLineEdit(defaults.get("display_name", ""))
|
||||||
|
self.path_id_edit = QLineEdit(defaults.get("path_id", ""))
|
||||||
|
self.source_combo = QComboBox()
|
||||||
|
self.source_combo.addItem("外部真值定位", "external_pose")
|
||||||
|
self.source_combo.addItem("底盘遥测里程计", "chassis_telemetry")
|
||||||
|
self.topic_edit = QLineEdit(defaults.get("topic", ""))
|
||||||
|
self.path_type_combo = QComboBox()
|
||||||
|
for item in defaults.get("path_type_options", "").split(","):
|
||||||
|
item = item.strip()
|
||||||
|
if item:
|
||||||
|
self.path_type_combo.addItem(item)
|
||||||
|
self.path_type_combo.setEditable(True)
|
||||||
|
current_path_type = defaults.get("path_type", "")
|
||||||
|
if current_path_type:
|
||||||
|
index = self.path_type_combo.findText(current_path_type)
|
||||||
|
if index >= 0:
|
||||||
|
self.path_type_combo.setCurrentIndex(index)
|
||||||
|
else:
|
||||||
|
self.path_type_combo.setEditText(current_path_type)
|
||||||
|
self.recommended_task_types_edit = QLineEdit(defaults.get("recommended_task_types", ""))
|
||||||
|
self.target_speed_edit = QLineEdit(defaults.get("target_speed_ms", "0.05"))
|
||||||
|
self.duration_edit = QLineEdit(defaults.get("duration_sec", "0"))
|
||||||
|
self.min_distance_step_edit = QLineEdit(defaults.get("min_distance_step_m", "0.03"))
|
||||||
|
self.min_yaw_step_edit = QLineEdit(defaults.get("min_yaw_step_rad", "0.03"))
|
||||||
|
self.description_edit = QLineEdit(defaults.get("description", ""))
|
||||||
|
self.overwrite_check = QCheckBox("允许覆盖同名路径文件")
|
||||||
|
|
||||||
|
source = defaults.get("source", "external_pose")
|
||||||
|
source_index = self.source_combo.findData(source)
|
||||||
|
if source_index >= 0:
|
||||||
|
self.source_combo.setCurrentIndex(source_index)
|
||||||
|
self.source_combo.currentIndexChanged.connect(self._apply_default_topic_for_source)
|
||||||
|
|
||||||
|
form.addRow("中文名称", self.display_name_edit)
|
||||||
|
form.addRow("路径 ID", self.path_id_edit)
|
||||||
|
form.addRow("录制来源", self.source_combo)
|
||||||
|
form.addRow("话题", self.topic_edit)
|
||||||
|
form.addRow("路径类型", self.path_type_combo)
|
||||||
|
form.addRow("推荐任务类型", self.recommended_task_types_edit)
|
||||||
|
form.addRow("目标速度 m/s", self.target_speed_edit)
|
||||||
|
form.addRow("录制时长 s", self.duration_edit)
|
||||||
|
form.addRow("最小点间距 m", self.min_distance_step_edit)
|
||||||
|
form.addRow("最小 yaw 间隔 rad", self.min_yaw_step_edit)
|
||||||
|
form.addRow("说明", self.description_edit)
|
||||||
|
form.addRow("", self.overwrite_check)
|
||||||
|
|
||||||
|
buttons = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel)
|
||||||
|
buttons.accepted.connect(self.accept)
|
||||||
|
buttons.rejected.connect(self.reject)
|
||||||
|
layout.addWidget(buttons)
|
||||||
|
|
||||||
|
def _apply_default_topic_for_source(self, *_args) -> None:
|
||||||
|
source = str(self.source_combo.currentData() or "external_pose")
|
||||||
|
self.topic_edit.setText(self.default_topics.get(source, ""))
|
||||||
|
|
||||||
|
def values(self) -> dict[str, object]:
|
||||||
|
return {
|
||||||
|
"module": self.module,
|
||||||
|
"display_name": self.display_name_edit.text().strip(),
|
||||||
|
"path_id": self.path_id_edit.text().strip(),
|
||||||
|
"source": str(self.source_combo.currentData() or "external_pose"),
|
||||||
|
"topic": self.topic_edit.text().strip(),
|
||||||
|
"path_type": self.path_type_combo.currentText().strip() or "recorded",
|
||||||
|
"recommended_task_types": [
|
||||||
|
item.strip()
|
||||||
|
for item in re.split(r"[,;,;\s]+", self.recommended_task_types_edit.text().strip())
|
||||||
|
if item.strip()
|
||||||
|
],
|
||||||
|
"target_speed_ms": self.target_speed_edit.text().strip() or "0.0",
|
||||||
|
"duration_sec": self.duration_edit.text().strip() or "0",
|
||||||
|
"min_distance_step_m": self.min_distance_step_edit.text().strip() or "0.03",
|
||||||
|
"min_yaw_step_rad": self.min_yaw_step_edit.text().strip() or "0.03",
|
||||||
|
"description": self.description_edit.text().strip(),
|
||||||
|
"overwrite": self.overwrite_check.isChecked(),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
class ProcessReportMixin:
|
class ProcessReportMixin:
|
||||||
|
def _resolved_repo_path(self, raw_path: str) -> str:
|
||||||
|
path = Path(raw_path).expanduser()
|
||||||
|
if path.is_absolute():
|
||||||
|
return str(path.resolve(strict=False))
|
||||||
|
return str((REPO_ROOT / path).resolve(strict=False))
|
||||||
|
|
||||||
|
def _resolved_bridge_script(self, raw_path: str) -> str:
|
||||||
|
return self._resolved_repo_path(raw_path)
|
||||||
|
|
||||||
|
def _configured_path_or_default(self, keys: tuple[str, ...], default_path: str) -> str:
|
||||||
|
raw_path = read_nested(getattr(self, "site_profile", {}), keys, "")
|
||||||
|
return self._resolved_repo_path(raw_path or default_path)
|
||||||
|
|
||||||
|
def default_record_topic(self, source: str) -> str:
|
||||||
|
if source == "chassis_telemetry":
|
||||||
|
return (
|
||||||
|
read_nested(self.site_profile, ("vehicle_agent", "chassis_telemetry_topic"))
|
||||||
|
or read_nested(self.site_profile, ("chassis_telemetry_bridge", "output_topic"))
|
||||||
|
or "/chassis/telemetry"
|
||||||
|
)
|
||||||
|
return self.external_topic_edit.text().strip() or "/workshop/external_localization/vehicle/pose"
|
||||||
|
|
||||||
|
def reference_path_record_defaults(self, module: str) -> dict[str, str]:
|
||||||
|
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
||||||
|
selected = self.selected_reference_path(module) if hasattr(self, "selected_reference_path") else None
|
||||||
|
recommended = selected.get("recommended_task_types", []) if isinstance(selected, dict) else []
|
||||||
|
recommended_text = ",".join(str(item) for item in recommended) if recommended else RECORD_DEFAULT_RECOMMENDED_TASKS[module]
|
||||||
|
path_type = str(selected.get("path_type", "")) if isinstance(selected, dict) else ""
|
||||||
|
path_type_options = {
|
||||||
|
"chassis": "recorded,straight_line,arc,s_curve,in_place_rotation,lateral_translation,diagonal_motion",
|
||||||
|
"control": "recorded,straight_line,arc,s_curve,lateral_offset,stop_accuracy",
|
||||||
|
"sensor": "recorded,static_station,sampling_line,imu_motion,pose_sweep",
|
||||||
|
}[module]
|
||||||
|
return {
|
||||||
|
"display_name": f"{RECORD_MODULE_LABELS[module]}录制路径 {timestamp}",
|
||||||
|
"path_id": f"{module}_recorded_{timestamp}",
|
||||||
|
"source": "external_pose",
|
||||||
|
"topic": self.default_record_topic("external_pose"),
|
||||||
|
"external_pose_topic": self.default_record_topic("external_pose"),
|
||||||
|
"chassis_telemetry_topic": self.default_record_topic("chassis_telemetry"),
|
||||||
|
"path_type": path_type or RECORD_DEFAULT_PATH_TYPES[module],
|
||||||
|
"path_type_options": path_type_options,
|
||||||
|
"recommended_task_types": recommended_text,
|
||||||
|
"target_speed_ms": "0.05" if module != "chassis" else "0.08",
|
||||||
|
"duration_sec": "0",
|
||||||
|
"min_distance_step_m": "0.03",
|
||||||
|
"min_yaw_step_rad": "0.03",
|
||||||
|
"description": "操作台录制参考路径",
|
||||||
|
}
|
||||||
|
|
||||||
|
def validate_reference_path_record_values(self, values: dict[str, object]) -> bool:
|
||||||
|
path_id = str(values.get("path_id", "")).strip()
|
||||||
|
display_name = str(values.get("display_name", "")).strip()
|
||||||
|
if not display_name:
|
||||||
|
QMessageBox.warning(self, "路径名称缺失", "请填写中文名称。")
|
||||||
|
return False
|
||||||
|
if not re.fullmatch(r"[A-Za-z0-9][A-Za-z0-9_-]*", path_id):
|
||||||
|
QMessageBox.warning(self, "路径 ID 不合法", "路径 ID 只能使用英文、数字、下划线和短横线,且不能以符号开头。")
|
||||||
|
return False
|
||||||
|
for key, label in [
|
||||||
|
("target_speed_ms", "目标速度"),
|
||||||
|
("duration_sec", "录制时长"),
|
||||||
|
("min_distance_step_m", "最小点间距"),
|
||||||
|
("min_yaw_step_rad", "最小 yaw 间隔"),
|
||||||
|
]:
|
||||||
|
try:
|
||||||
|
value = float(str(values.get(key, "0")))
|
||||||
|
except ValueError:
|
||||||
|
QMessageBox.warning(self, "数值格式错误", f"{label} 必须是数字。")
|
||||||
|
return False
|
||||||
|
if key != "target_speed_ms" and value < 0.0:
|
||||||
|
QMessageBox.warning(self, "数值格式错误", f"{label} 不能小于 0。")
|
||||||
|
return False
|
||||||
|
if key == "target_speed_ms" and value < 0.0:
|
||||||
|
QMessageBox.warning(self, "数值格式错误", f"{label} 不能小于 0。")
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
def build_reference_path_record_command(self, values: dict[str, object]) -> str:
|
||||||
|
module = str(values["module"])
|
||||||
|
recorder = self._configured_path_or_default(
|
||||||
|
("reference_paths", "recorder"),
|
||||||
|
REFERENCE_PATH_RECORDER,
|
||||||
|
)
|
||||||
|
args = [
|
||||||
|
sys.executable,
|
||||||
|
recorder,
|
||||||
|
"--output-dir",
|
||||||
|
str(self.reference_path_dir(module)),
|
||||||
|
"--path-id",
|
||||||
|
str(values["path_id"]),
|
||||||
|
"--display-name",
|
||||||
|
str(values["display_name"]),
|
||||||
|
"--module-type",
|
||||||
|
module,
|
||||||
|
"--path-type",
|
||||||
|
str(values["path_type"]),
|
||||||
|
"--frame-id",
|
||||||
|
"workshop",
|
||||||
|
"--target-speed-ms",
|
||||||
|
str(values["target_speed_ms"]),
|
||||||
|
"--duration-sec",
|
||||||
|
str(values["duration_sec"]),
|
||||||
|
"--min-distance-step-m",
|
||||||
|
str(values["min_distance_step_m"]),
|
||||||
|
"--min-yaw-step-rad",
|
||||||
|
str(values["min_yaw_step_rad"]),
|
||||||
|
"--source",
|
||||||
|
str(values["source"]),
|
||||||
|
]
|
||||||
|
topic = str(values.get("topic", "")).strip()
|
||||||
|
if topic:
|
||||||
|
args.extend(["--topic", topic])
|
||||||
|
description = str(values.get("description", "")).strip()
|
||||||
|
if description:
|
||||||
|
args.extend(["--description", description])
|
||||||
|
for task_type in values.get("recommended_task_types", []) or []:
|
||||||
|
args.extend(["--recommended-task-type", str(task_type)])
|
||||||
|
if module == "chassis":
|
||||||
|
args.extend(["--allowed-chassis-type", str(self.chassis_type_combo.currentData() or "ackermann")])
|
||||||
|
if bool(values.get("overwrite", False)):
|
||||||
|
args.append("--overwrite")
|
||||||
|
return "source install/setup.bash && exec " + shell_join(args)
|
||||||
|
|
||||||
|
def build_chassis_path_capture_command(self) -> str:
|
||||||
|
reference_path = self.selected_reference_path("chassis")
|
||||||
|
reference_path_id = str(reference_path.get("path_id", "")).strip() if reference_path else ""
|
||||||
|
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
||||||
|
session_dir = Path("/tmp/agv_calib_chassis_sim") / f"ui_{timestamp}"
|
||||||
|
vehicle_id = self.vehicle_id_edit.text().strip() or "demo_agv_001"
|
||||||
|
chassis_type = str(self.chassis_type_combo.currentData() or "ackermann")
|
||||||
|
chassis_topic = (
|
||||||
|
read_nested(self.site_profile, ("vehicle_agent", "chassis_telemetry_topic"))
|
||||||
|
or read_nested(self.site_profile, ("chassis_telemetry_bridge", "output_topic"))
|
||||||
|
or "/chassis/telemetry"
|
||||||
|
)
|
||||||
|
ackermann_topic = read_nested(
|
||||||
|
self.site_profile,
|
||||||
|
("vehicle_agent", "internal_ackermann_command_topic"),
|
||||||
|
f"/vehicle/{vehicle_id}/internal/ackermann_cmd",
|
||||||
|
)
|
||||||
|
runner = self._configured_path_or_default(
|
||||||
|
("chassis_calibration", "action_profile_capture_runner"),
|
||||||
|
"src/site_deployment/workshop_chassis_calibration_real/run_chassis_profile_capture.py",
|
||||||
|
)
|
||||||
|
config_file = self._configured_path_or_default(
|
||||||
|
("chassis_calibration", "data_config_file"),
|
||||||
|
"src/site_deployment/workshop_chassis_calibration_real/config/chassis_data_sim.yaml",
|
||||||
|
)
|
||||||
|
action_profile = self._configured_path_or_default(
|
||||||
|
("chassis_calibration", "action_profile_file"),
|
||||||
|
"src/site_deployment/workshop_chassis_calibration_real/config/chassis_action_profile.yaml",
|
||||||
|
)
|
||||||
|
args = [
|
||||||
|
sys.executable,
|
||||||
|
runner,
|
||||||
|
"--config",
|
||||||
|
config_file,
|
||||||
|
"--action-profile",
|
||||||
|
action_profile,
|
||||||
|
"--chassis-type",
|
||||||
|
chassis_type,
|
||||||
|
"--session-id",
|
||||||
|
f"ui_chassis_path_{timestamp}",
|
||||||
|
"--site-id",
|
||||||
|
self.workcell_zone_edit.text().strip() or "isaac_workcell_zone_a",
|
||||||
|
"--vehicle-id",
|
||||||
|
vehicle_id,
|
||||||
|
"--session-dir",
|
||||||
|
str(session_dir),
|
||||||
|
"--dataset-index-path",
|
||||||
|
str(session_dir / "dataset_index.yaml"),
|
||||||
|
"--chassis-telemetry-topic",
|
||||||
|
chassis_topic,
|
||||||
|
"--external-pose-topic",
|
||||||
|
self.external_topic_edit.text().strip() or "/isaac/external_localization/vehicle/pose",
|
||||||
|
"--ackermann-command-topic",
|
||||||
|
ackermann_topic,
|
||||||
|
"--command-source",
|
||||||
|
"auto",
|
||||||
|
"--action-server",
|
||||||
|
"/chassis/execute_motion_primitive",
|
||||||
|
]
|
||||||
|
if reference_path_id:
|
||||||
|
args.extend(["--reference-path-id", reference_path_id])
|
||||||
|
return "source install/setup.bash && " + shell_join(args)
|
||||||
|
|
||||||
def build_launch_command(self) -> str:
|
def build_launch_command(self) -> str:
|
||||||
args = [
|
args = [
|
||||||
"ros2",
|
"ros2",
|
||||||
@@ -36,6 +345,45 @@ class ProcessReportMixin:
|
|||||||
args.append(launch_arg("data_input_params_file", self.data_input_edit.text().strip()))
|
args.append(launch_arg("data_input_params_file", self.data_input_edit.text().strip()))
|
||||||
if self.dataset_index_edit.text().strip():
|
if self.dataset_index_edit.text().strip():
|
||||||
args.append(launch_arg("dataset_index_file", self.dataset_index_edit.text().strip()))
|
args.append(launch_arg("dataset_index_file", self.dataset_index_edit.text().strip()))
|
||||||
|
bridge_config = getattr(self, "site_profile", {}).get("chassis_telemetry_bridge", {})
|
||||||
|
if isinstance(bridge_config, dict) and bridge_config:
|
||||||
|
bridge_script = read_nested(
|
||||||
|
self.site_profile,
|
||||||
|
("chassis_telemetry_bridge", "bridge_tool"),
|
||||||
|
"src/site_deployment/workshop_chassis_calibration_real/workshop_chassis_telemetry_bridge.py",
|
||||||
|
)
|
||||||
|
args.extend([
|
||||||
|
launch_arg("enable_chassis_telemetry_bridge", "true"),
|
||||||
|
launch_arg("chassis_telemetry_bridge_script", self._resolved_bridge_script(bridge_script)),
|
||||||
|
launch_arg(
|
||||||
|
"chassis_telemetry_bind_host",
|
||||||
|
read_nested(self.site_profile, ("chassis_telemetry_bridge", "bind_host"), "0.0.0.0"),
|
||||||
|
),
|
||||||
|
launch_arg(
|
||||||
|
"chassis_telemetry_bind_port",
|
||||||
|
read_nested(self.site_profile, ("chassis_telemetry_bridge", "bind_port"), "9010"),
|
||||||
|
),
|
||||||
|
launch_arg(
|
||||||
|
"chassis_telemetry_protocol",
|
||||||
|
read_nested(self.site_profile, ("chassis_telemetry_bridge", "protocol"), "frame"),
|
||||||
|
),
|
||||||
|
launch_arg(
|
||||||
|
"chassis_telemetry_expected_msg_type",
|
||||||
|
read_nested(self.site_profile, ("chassis_telemetry_bridge", "expected_msg_type"), "0"),
|
||||||
|
),
|
||||||
|
launch_arg(
|
||||||
|
"chassis_telemetry_topic",
|
||||||
|
read_nested(self.site_profile, ("chassis_telemetry_bridge", "output_topic"), "/chassis/telemetry"),
|
||||||
|
),
|
||||||
|
launch_arg(
|
||||||
|
"chassis_telemetry_default_chassis_type",
|
||||||
|
read_nested(self.site_profile, ("chassis_telemetry_bridge", "default_chassis_type"), "ackermann"),
|
||||||
|
),
|
||||||
|
launch_arg(
|
||||||
|
"chassis_telemetry_max_payload_bytes",
|
||||||
|
read_nested(self.site_profile, ("chassis_telemetry_bridge", "max_payload_bytes"), "262144"),
|
||||||
|
),
|
||||||
|
])
|
||||||
return "source install/setup.bash && " + shell_join(args)
|
return "source install/setup.bash && " + shell_join(args)
|
||||||
|
|
||||||
def build_smoke_command(self) -> str:
|
def build_smoke_command(self) -> str:
|
||||||
@@ -61,6 +409,7 @@ class ProcessReportMixin:
|
|||||||
"240",
|
"240",
|
||||||
"--service-timeout-sec",
|
"--service-timeout-sec",
|
||||||
"10",
|
"10",
|
||||||
|
"--verbose-feedback",
|
||||||
]
|
]
|
||||||
if self.vehicle_profile_path_edit.text().strip():
|
if self.vehicle_profile_path_edit.text().strip():
|
||||||
args.extend(["--vehicle-profile-file", self.vehicle_profile_path_edit.text().strip()])
|
args.extend(["--vehicle-profile-file", self.vehicle_profile_path_edit.text().strip()])
|
||||||
@@ -115,6 +464,61 @@ class ProcessReportMixin:
|
|||||||
self.smoke_state.setText("标定流程:运行中")
|
self.smoke_state.setText("标定流程:运行中")
|
||||||
self.smoke_process.start("bash", ["-lc", command])
|
self.smoke_process.start("bash", ["-lc", command])
|
||||||
|
|
||||||
|
def start_chassis_path_capture(self) -> None:
|
||||||
|
if self.chassis_capture_process.state() != QProcess.NotRunning:
|
||||||
|
QMessageBox.warning(self, "底盘路径采集已运行", "底盘路径采集进程已经在运行。")
|
||||||
|
return
|
||||||
|
if not self.selected_reference_path("chassis"):
|
||||||
|
QMessageBox.warning(self, "未选择路径", "请先在底盘标定页选择参考路径。")
|
||||||
|
return
|
||||||
|
reference_path = self.selected_reference_path("chassis")
|
||||||
|
reference_path_id = str(reference_path.get("path_id", "")).strip() if reference_path else ""
|
||||||
|
chassis_type = str(self.chassis_type_combo.currentData() or "ackermann")
|
||||||
|
if reference_path_id not in self.allowed_chassis_reference_path_ids(chassis_type):
|
||||||
|
QMessageBox.warning(
|
||||||
|
self,
|
||||||
|
"路径未绑定动作",
|
||||||
|
"这条路径是录制参考路径,但还没有绑定到底盘动作 profile,不能直接用于“底盘路径采集测试”。"
|
||||||
|
"请先在 chassis_action_profile.yaml 中为当前底盘类型添加对应动作,或仅用于任务预览/路径管理。",
|
||||||
|
)
|
||||||
|
return
|
||||||
|
if self.launch_process.state() == QProcess.NotRunning:
|
||||||
|
reply = QMessageBox.question(
|
||||||
|
self,
|
||||||
|
"现场服务未运行",
|
||||||
|
"当前界面没有检测到由本界面启动的现场服务。若你已经用脚本启动 Isaac 仿真链路,可以继续执行底盘路径采集测试。",
|
||||||
|
)
|
||||||
|
if reply != QMessageBox.Yes:
|
||||||
|
return
|
||||||
|
command = self.build_chassis_path_capture_command()
|
||||||
|
self.append_log("[底盘路径采集] 启动:" + command)
|
||||||
|
self.smoke_state.setText("底盘路径采集:运行中")
|
||||||
|
self.chassis_capture_process.start("bash", ["-lc", command])
|
||||||
|
|
||||||
|
def start_reference_path_recording(self, module: str) -> None:
|
||||||
|
if self.reference_path_record_process.state() != QProcess.NotRunning:
|
||||||
|
QMessageBox.warning(self, "参考路径录制已运行", "参考路径录制进程已经在运行。")
|
||||||
|
return
|
||||||
|
if self.launch_process.state() == QProcess.NotRunning:
|
||||||
|
reply = QMessageBox.question(
|
||||||
|
self,
|
||||||
|
"现场服务未运行",
|
||||||
|
"当前界面没有检测到由本界面启动的现场服务。若你已经用脚本启动 Isaac 或 ROS 现场链路,可以继续录制参考路径。",
|
||||||
|
)
|
||||||
|
if reply != QMessageBox.Yes:
|
||||||
|
return
|
||||||
|
dialog = ReferencePathRecordDialog(module, self.reference_path_record_defaults(module), self)
|
||||||
|
if dialog.exec() != QDialog.Accepted:
|
||||||
|
return
|
||||||
|
values = dialog.values()
|
||||||
|
if not self.validate_reference_path_record_values(values):
|
||||||
|
return
|
||||||
|
command = self.build_reference_path_record_command(values)
|
||||||
|
self.reference_path_record_module = module
|
||||||
|
self.append_log("[参考路径录制] 启动:" + command)
|
||||||
|
self.smoke_state.setText("参考路径录制:运行中")
|
||||||
|
self.reference_path_record_process.start("bash", ["-lc", command])
|
||||||
|
|
||||||
def stop_process(self, process: QProcess, name: str) -> None:
|
def stop_process(self, process: QProcess, name: str) -> None:
|
||||||
if process.state() == QProcess.NotRunning:
|
if process.state() == QProcess.NotRunning:
|
||||||
self.append_log(f"[{name}] 当前没有运行中的进程。")
|
self.append_log(f"[{name}] 当前没有运行中的进程。")
|
||||||
@@ -142,11 +546,16 @@ class ProcessReportMixin:
|
|||||||
self.append_log(f"[{name}] {status_text}")
|
self.append_log(f"[{name}] {status_text}")
|
||||||
if name == "现场服务":
|
if name == "现场服务":
|
||||||
self.stack_state.setText("现场服务:未启动")
|
self.stack_state.setText("现场服务:未启动")
|
||||||
else:
|
elif name == "标定流程":
|
||||||
self.trajectory_runtime_active = False
|
self.trajectory_runtime_active = False
|
||||||
self.trajectory_last_completed_stage_id = ""
|
self.trajectory_last_completed_stage_id = ""
|
||||||
self.refresh_planned_trajectory()
|
self.refresh_planned_trajectory()
|
||||||
self.smoke_state.setText("标定流程:空闲")
|
self.smoke_state.setText("标定流程:空闲")
|
||||||
|
elif name == "底盘路径采集":
|
||||||
|
self.smoke_state.setText("底盘路径采集:空闲")
|
||||||
|
elif name == "参考路径录制":
|
||||||
|
self.smoke_state.setText("参考路径录制:空闲")
|
||||||
|
self.refresh_reference_path_combos()
|
||||||
|
|
||||||
def append_log(self, text: str) -> None:
|
def append_log(self, text: str) -> None:
|
||||||
self.log_view.appendPlainText(text)
|
self.log_view.appendPlainText(text)
|
||||||
|
|||||||
@@ -380,3 +380,5 @@ class ProfileHandlersMixin:
|
|||||||
self.vehicle_profile_path_edit.setText(str(resolve_vehicle_profile_path(vehicle_profile_file)))
|
self.vehicle_profile_path_edit.setText(str(resolve_vehicle_profile_path(vehicle_profile_file)))
|
||||||
if Path(self.vehicle_profile_path_edit.text()).exists():
|
if Path(self.vehicle_profile_path_edit.text()).exists():
|
||||||
self.load_vehicle_profile_from_current_path()
|
self.load_vehicle_profile_from_current_path()
|
||||||
|
if hasattr(self, "refresh_reference_path_combos"):
|
||||||
|
self.refresh_reference_path_combos()
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ try:
|
|||||||
from .constants import (
|
from .constants import (
|
||||||
CHASSIS_PARAMETER_OPTIONS,
|
CHASSIS_PARAMETER_OPTIONS,
|
||||||
CONTROL_PARAMETER_OPTIONS,
|
CONTROL_PARAMETER_OPTIONS,
|
||||||
|
DEFAULT_REFERENCE_PATH_DIRS,
|
||||||
DEFAULT_VEHICLE_PROFILE,
|
DEFAULT_VEHICLE_PROFILE,
|
||||||
POLICY_DISPLAY_NAMES,
|
POLICY_DISPLAY_NAMES,
|
||||||
REPO_ROOT,
|
REPO_ROOT,
|
||||||
@@ -41,6 +42,7 @@ except ImportError:
|
|||||||
from constants import (
|
from constants import (
|
||||||
CHASSIS_PARAMETER_OPTIONS,
|
CHASSIS_PARAMETER_OPTIONS,
|
||||||
CONTROL_PARAMETER_OPTIONS,
|
CONTROL_PARAMETER_OPTIONS,
|
||||||
|
DEFAULT_REFERENCE_PATH_DIRS,
|
||||||
DEFAULT_VEHICLE_PROFILE,
|
DEFAULT_VEHICLE_PROFILE,
|
||||||
POLICY_DISPLAY_NAMES,
|
POLICY_DISPLAY_NAMES,
|
||||||
REPO_ROOT,
|
REPO_ROOT,
|
||||||
@@ -89,6 +91,219 @@ class SessionBuilderMixin:
|
|||||||
tasks = self.selected_tasks()
|
tasks = self.selected_tasks()
|
||||||
return ",".join(tasks) if tasks else "external"
|
return ",".join(tasks) if tasks else "external"
|
||||||
|
|
||||||
|
def reference_path_dir(self, module: str) -> Path:
|
||||||
|
config_keys = {
|
||||||
|
"chassis": ("chassis_calibration", "reference_path_dir"),
|
||||||
|
"control": ("control_calibration", "reference_path_dir"),
|
||||||
|
"sensor": ("sensor_calibration", "reference_path_dir"),
|
||||||
|
}
|
||||||
|
raw_path = read_nested(self.site_profile, config_keys[module])
|
||||||
|
if raw_path:
|
||||||
|
path = Path(raw_path).expanduser()
|
||||||
|
if path.is_absolute():
|
||||||
|
return path.resolve(strict=False)
|
||||||
|
profile_path = resolve_profile_path(self.profile_path_edit.text().strip())
|
||||||
|
return (profile_path.parent / path).resolve(strict=False)
|
||||||
|
return DEFAULT_REFERENCE_PATH_DIRS[module]
|
||||||
|
|
||||||
|
def resolve_site_or_repo_path(self, raw_path: str) -> Path:
|
||||||
|
path = Path(raw_path).expanduser()
|
||||||
|
if path.is_absolute():
|
||||||
|
return path.resolve(strict=False)
|
||||||
|
profile_path = resolve_profile_path(self.profile_path_edit.text().strip())
|
||||||
|
profile_relative = (profile_path.parent / path).resolve(strict=False)
|
||||||
|
if profile_relative.exists():
|
||||||
|
return profile_relative
|
||||||
|
return (REPO_ROOT / path).resolve(strict=False)
|
||||||
|
|
||||||
|
def chassis_action_profile_path(self) -> Path:
|
||||||
|
raw_path = read_nested(
|
||||||
|
self.site_profile,
|
||||||
|
("chassis_calibration", "action_profile_file"),
|
||||||
|
"src/site_deployment/workshop_chassis_calibration_real/config/chassis_action_profile.yaml",
|
||||||
|
)
|
||||||
|
return self.resolve_site_or_repo_path(raw_path)
|
||||||
|
|
||||||
|
def allowed_chassis_reference_path_ids(self, chassis_type: str) -> set[str]:
|
||||||
|
profile_path = self.chassis_action_profile_path()
|
||||||
|
if not profile_path.exists():
|
||||||
|
return set()
|
||||||
|
try:
|
||||||
|
profile = load_yaml(profile_path)
|
||||||
|
except Exception as exc:
|
||||||
|
self.append_log(f"[参考路径] 底盘动作 profile 加载失败 {profile_path}: {exc}")
|
||||||
|
return set()
|
||||||
|
section = profile.get("chassis_profiles", {}).get(chassis_type, {})
|
||||||
|
actions = section.get("actions", []) if isinstance(section, dict) else []
|
||||||
|
if not isinstance(actions, list):
|
||||||
|
return set()
|
||||||
|
return {
|
||||||
|
str(action.get("reference_path_id", "")).strip()
|
||||||
|
for action in actions
|
||||||
|
if isinstance(action, dict) and str(action.get("reference_path_id", "")).strip()
|
||||||
|
}
|
||||||
|
|
||||||
|
def load_reference_path_options(self, module: str) -> tuple[dict[str, Any], list[dict[str, Any]]]:
|
||||||
|
path_dir = self.reference_path_dir(module)
|
||||||
|
index_path = path_dir / "index.yaml"
|
||||||
|
index = load_yaml(index_path) if index_path.exists() else {}
|
||||||
|
paths: list[dict[str, Any]] = []
|
||||||
|
if not path_dir.exists():
|
||||||
|
return index, paths
|
||||||
|
for path_file in sorted(path_dir.glob("*.yaml")):
|
||||||
|
if path_file.name in {"index.yaml", "_index.yaml"}:
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
payload = load_yaml(path_file)
|
||||||
|
except Exception as exc:
|
||||||
|
self.append_log(f"[参考路径] 跳过 {path_file}: {exc}")
|
||||||
|
continue
|
||||||
|
path_id = str(payload.get("path_id", "")).strip()
|
||||||
|
display_name = str(payload.get("display_name", "")).strip()
|
||||||
|
points = payload.get("points", [])
|
||||||
|
if not path_id or not display_name or not isinstance(points, list) or not points:
|
||||||
|
self.append_log(f"[参考路径] 跳过无效路径文件: {path_file}")
|
||||||
|
continue
|
||||||
|
payload["_source_file"] = str(path_file.resolve(strict=False))
|
||||||
|
payload["_path_dir"] = str(path_dir.resolve(strict=False))
|
||||||
|
paths.append(payload)
|
||||||
|
if module == "chassis":
|
||||||
|
chassis_type = (
|
||||||
|
str(self.chassis_type_combo.currentData() or "ackermann")
|
||||||
|
if hasattr(self, "chassis_type_combo")
|
||||||
|
else "ackermann"
|
||||||
|
)
|
||||||
|
allowed_ids = self.allowed_chassis_reference_path_ids(chassis_type)
|
||||||
|
if allowed_ids:
|
||||||
|
def recorded_path_chassis_types(path: dict[str, Any]) -> set[str]:
|
||||||
|
raw_types = path.get("allowed_chassis_types", []) or []
|
||||||
|
if isinstance(raw_types, str):
|
||||||
|
return {raw_types}
|
||||||
|
if isinstance(raw_types, list):
|
||||||
|
return {str(item) for item in raw_types}
|
||||||
|
return set()
|
||||||
|
|
||||||
|
paths = [
|
||||||
|
path
|
||||||
|
for path in paths
|
||||||
|
if str(path.get("path_id", "")).strip() in allowed_ids
|
||||||
|
or chassis_type in recorded_path_chassis_types(path)
|
||||||
|
]
|
||||||
|
return index, paths
|
||||||
|
|
||||||
|
def default_reference_path_id(self, module: str, index: dict[str, Any], paths: list[dict[str, Any]]) -> str:
|
||||||
|
chassis_type = str(self.chassis_type_combo.currentData() or "ackermann") if hasattr(self, "chassis_type_combo") else "ackermann"
|
||||||
|
by_chassis = index.get("default_path_id_by_chassis", {}) if isinstance(index, dict) else {}
|
||||||
|
by_task = index.get("default_path_id_by_task_type", {}) if isinstance(index, dict) else {}
|
||||||
|
if module in {"chassis", "control"} and isinstance(by_chassis, dict):
|
||||||
|
default_id = str(by_chassis.get(chassis_type, "")).strip()
|
||||||
|
if default_id:
|
||||||
|
return default_id
|
||||||
|
if isinstance(by_task, dict):
|
||||||
|
task_key = {
|
||||||
|
"chassis": "straight_line",
|
||||||
|
"control": "trajectory_tracking",
|
||||||
|
"sensor": "camera_intrinsic",
|
||||||
|
}[module]
|
||||||
|
default_id = str(by_task.get(task_key, "")).strip()
|
||||||
|
if default_id:
|
||||||
|
return default_id
|
||||||
|
return str(paths[0].get("path_id", "")) if paths else ""
|
||||||
|
|
||||||
|
def refresh_reference_path_combos(self) -> None:
|
||||||
|
combo_specs = {
|
||||||
|
"chassis": "chassis_reference_path_combo",
|
||||||
|
"control": "control_reference_path_combo",
|
||||||
|
"sensor": "sensor_reference_path_combo",
|
||||||
|
}
|
||||||
|
for module, attr_name in combo_specs.items():
|
||||||
|
if not hasattr(self, attr_name):
|
||||||
|
continue
|
||||||
|
combo = getattr(self, attr_name)
|
||||||
|
previous = combo.currentData()
|
||||||
|
previous_id = str(previous.get("path_id", "")) if isinstance(previous, dict) else ""
|
||||||
|
index, paths = self.load_reference_path_options(module)
|
||||||
|
ids = {str(path.get("path_id", "")) for path in paths}
|
||||||
|
selected_id = previous_id if previous_id in ids else self.default_reference_path_id(module, index, paths)
|
||||||
|
if selected_id not in ids and paths:
|
||||||
|
selected_id = str(paths[0].get("path_id", ""))
|
||||||
|
combo.blockSignals(True)
|
||||||
|
combo.clear()
|
||||||
|
if not paths:
|
||||||
|
combo.addItem("未找到参考路径", None)
|
||||||
|
combo.setEnabled(False)
|
||||||
|
else:
|
||||||
|
combo.setEnabled(True)
|
||||||
|
for path in paths:
|
||||||
|
label = f"{path.get('display_name', path.get('path_id'))} ({path.get('path_id')})"
|
||||||
|
combo.addItem(label, path)
|
||||||
|
if str(path.get("path_id", "")) == selected_id:
|
||||||
|
combo.setCurrentIndex(combo.count() - 1)
|
||||||
|
combo.blockSignals(False)
|
||||||
|
if hasattr(self, "launch_command_preview"):
|
||||||
|
self.refresh_command_preview()
|
||||||
|
|
||||||
|
def selected_reference_path(self, module: str) -> dict[str, Any] | None:
|
||||||
|
combo = getattr(self, f"{module}_reference_path_combo", None)
|
||||||
|
if combo is None:
|
||||||
|
return None
|
||||||
|
path = combo.currentData()
|
||||||
|
return path if isinstance(path, dict) else None
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def stringify_reference_value(value: Any) -> str:
|
||||||
|
if isinstance(value, bool):
|
||||||
|
return "true" if value else "false"
|
||||||
|
if isinstance(value, float):
|
||||||
|
return f"{value:.9g}"
|
||||||
|
return str(value)
|
||||||
|
|
||||||
|
def add_reference_path_metadata(self, params: dict[str, Any], path: dict[str, Any] | None) -> None:
|
||||||
|
if not path:
|
||||||
|
return
|
||||||
|
points = path.get("points", [])
|
||||||
|
if not isinstance(points, list):
|
||||||
|
return
|
||||||
|
params["reference_path.id"] = path.get("path_id", "")
|
||||||
|
params["reference_path.display_name"] = path.get("display_name", "")
|
||||||
|
params["reference_path.frame_id"] = path.get("frame_id", "workshop")
|
||||||
|
params["reference_path.path_type"] = path.get("path_type", "polyline")
|
||||||
|
params["reference_path.point_count"] = len(points)
|
||||||
|
if path.get("_path_dir"):
|
||||||
|
params["reference_path.path_dir"] = path["_path_dir"]
|
||||||
|
if path.get("_source_file"):
|
||||||
|
params["reference_path.file"] = path["_source_file"]
|
||||||
|
if path.get("module_type"):
|
||||||
|
params["reference_path.module_type"] = path["module_type"]
|
||||||
|
if path.get("description"):
|
||||||
|
params["reference_path.description"] = path["description"]
|
||||||
|
for index, point in enumerate(points):
|
||||||
|
if not isinstance(point, dict):
|
||||||
|
continue
|
||||||
|
prefix = f"reference_path.pt_{index}"
|
||||||
|
params[f"{prefix}_x_m"] = self.stringify_reference_value(point.get("x_m", 0.0))
|
||||||
|
params[f"{prefix}_y_m"] = self.stringify_reference_value(point.get("y_m", 0.0))
|
||||||
|
params[f"{prefix}_z_m"] = self.stringify_reference_value(point.get("z_m", 0.0))
|
||||||
|
params[f"{prefix}_yaw_rad"] = self.stringify_reference_value(point.get("yaw_rad", 0.0))
|
||||||
|
params[f"{prefix}_speed_ms"] = self.stringify_reference_value(point.get("target_speed_ms", 0.0))
|
||||||
|
|
||||||
|
def apply_reference_path_as_trajectory(self, params: dict[str, Any], path: dict[str, Any] | None) -> None:
|
||||||
|
if not path:
|
||||||
|
return
|
||||||
|
points = path.get("points", [])
|
||||||
|
if not isinstance(points, list) or len(points) < 2:
|
||||||
|
return
|
||||||
|
for key in list(params):
|
||||||
|
if key.startswith("traj_pt_"):
|
||||||
|
del params[key]
|
||||||
|
for index, point in enumerate(points):
|
||||||
|
if not isinstance(point, dict):
|
||||||
|
continue
|
||||||
|
params[f"traj_pt_{index}_x_m"] = self.stringify_reference_value(point.get("x_m", 0.0))
|
||||||
|
params[f"traj_pt_{index}_y_m"] = self.stringify_reference_value(point.get("y_m", 0.0))
|
||||||
|
params[f"traj_pt_{index}_yaw_rad"] = self.stringify_reference_value(point.get("yaw_rad", 0.0))
|
||||||
|
params[f"traj_pt_{index}_speed_ms"] = self.stringify_reference_value(point.get("target_speed_ms", 0.0))
|
||||||
|
|
||||||
def refresh_all(self) -> None:
|
def refresh_all(self) -> None:
|
||||||
self.refresh_command_preview()
|
self.refresh_command_preview()
|
||||||
self.refresh_task_preview_from_config()
|
self.refresh_task_preview_from_config()
|
||||||
@@ -158,6 +373,10 @@ class SessionBuilderMixin:
|
|||||||
|
|
||||||
def trajectory_segments_from_task_params(self, params: dict[str, str]) -> list[list[tuple[float, float, float]]]:
|
def trajectory_segments_from_task_params(self, params: dict[str, str]) -> list[list[tuple[float, float, float]]]:
|
||||||
segments: list[list[tuple[float, float, float]]] = []
|
segments: list[list[tuple[float, float, float]]] = []
|
||||||
|
segment = self.trajectory_points_from_reference_path(params)
|
||||||
|
if segment:
|
||||||
|
segments.append(segment)
|
||||||
|
return segments
|
||||||
segment = self.trajectory_points_from_explicit_params(params)
|
segment = self.trajectory_points_from_explicit_params(params)
|
||||||
if segment:
|
if segment:
|
||||||
segments.append(segment)
|
segments.append(segment)
|
||||||
@@ -212,6 +431,21 @@ class SessionBuilderMixin:
|
|||||||
indexed_points[index] = (x, y)
|
indexed_points[index] = (x, y)
|
||||||
return [(x, y, 0.04) for _, (x, y) in sorted(indexed_points.items())]
|
return [(x, y, 0.04) for _, (x, y) in sorted(indexed_points.items())]
|
||||||
|
|
||||||
|
def trajectory_points_from_reference_path(self, params: dict[str, str]) -> list[tuple[float, float, float]]:
|
||||||
|
indexes = sorted({
|
||||||
|
int(match.group(1))
|
||||||
|
for key in params
|
||||||
|
if (match := re.fullmatch(r"reference_path\.pt_(\d+)_x_m", key))
|
||||||
|
})
|
||||||
|
points: list[tuple[float, float, float]] = []
|
||||||
|
for index in indexes:
|
||||||
|
x = float_or_none(params.get(f"reference_path.pt_{index}_x_m"))
|
||||||
|
y = float_or_none(params.get(f"reference_path.pt_{index}_y_m"))
|
||||||
|
z = float_or_none(params.get(f"reference_path.pt_{index}_z_m"))
|
||||||
|
if x is not None and y is not None:
|
||||||
|
points.append((x, y, z if z is not None else 0.04))
|
||||||
|
return points
|
||||||
|
|
||||||
def trajectory_points_from_motion_primitive(self, params: dict[str, str]) -> list[tuple[float, float, float]]:
|
def trajectory_points_from_motion_primitive(self, params: dict[str, str]) -> list[tuple[float, float, float]]:
|
||||||
primitive_type = params.get("primitive_type", "")
|
primitive_type = params.get("primitive_type", "")
|
||||||
if primitive_type == "straight_line":
|
if primitive_type == "straight_line":
|
||||||
@@ -268,6 +502,7 @@ class SessionBuilderMixin:
|
|||||||
if not hasattr(self, "chassis_type_combo"):
|
if not hasattr(self, "chassis_type_combo"):
|
||||||
return []
|
return []
|
||||||
chassis_type = str(self.chassis_type_combo.currentData() or "ackermann")
|
chassis_type = str(self.chassis_type_combo.currentData() or "ackermann")
|
||||||
|
reference_path = self.selected_reference_path("chassis")
|
||||||
tasks: list[dict[str, Any]] = []
|
tasks: list[dict[str, Any]] = []
|
||||||
for option in CHASSIS_PARAMETER_OPTIONS[chassis_type]:
|
for option in CHASSIS_PARAMETER_OPTIONS[chassis_type]:
|
||||||
check = self.chassis_param_checks.get(str(option["code"]))
|
check = self.chassis_param_checks.get(str(option["code"]))
|
||||||
@@ -280,6 +515,7 @@ class SessionBuilderMixin:
|
|||||||
}
|
}
|
||||||
params.setdefault("brake_when_finished", "true")
|
params.setdefault("brake_when_finished", "true")
|
||||||
params.setdefault("timeout_sec", "20.0")
|
params.setdefault("timeout_sec", "20.0")
|
||||||
|
self.add_reference_path_metadata(params, reference_path)
|
||||||
tasks.append(self.make_task(
|
tasks.append(self.make_task(
|
||||||
STAGE_CHASSIS,
|
STAGE_CHASSIS,
|
||||||
str(option["code"]),
|
str(option["code"]),
|
||||||
@@ -292,6 +528,7 @@ class SessionBuilderMixin:
|
|||||||
def selected_control_tasks(self) -> list[dict[str, Any]]:
|
def selected_control_tasks(self) -> list[dict[str, Any]]:
|
||||||
if not hasattr(self, "control_param_checks"):
|
if not hasattr(self, "control_param_checks"):
|
||||||
return []
|
return []
|
||||||
|
reference_path = self.selected_reference_path("control")
|
||||||
tasks: list[dict[str, Any]] = []
|
tasks: list[dict[str, Any]] = []
|
||||||
for option in CONTROL_PARAMETER_OPTIONS:
|
for option in CONTROL_PARAMETER_OPTIONS:
|
||||||
check = self.control_param_checks.get(str(option["code"]))
|
check = self.control_param_checks.get(str(option["code"]))
|
||||||
@@ -308,6 +545,8 @@ class SessionBuilderMixin:
|
|||||||
"trajectory_tracking.required_external_pose_source_id",
|
"trajectory_tracking.required_external_pose_source_id",
|
||||||
self.reference_source_edit.text().strip(),
|
self.reference_source_edit.text().strip(),
|
||||||
)
|
)
|
||||||
|
self.apply_reference_path_as_trajectory(params, reference_path)
|
||||||
|
self.add_reference_path_metadata(params, reference_path)
|
||||||
tasks.append(self.make_task(
|
tasks.append(self.make_task(
|
||||||
STAGE_CONTROL,
|
STAGE_CONTROL,
|
||||||
str(option["code"]),
|
str(option["code"]),
|
||||||
@@ -320,6 +559,7 @@ class SessionBuilderMixin:
|
|||||||
def selected_sensor_tasks(self) -> list[dict[str, Any]]:
|
def selected_sensor_tasks(self) -> list[dict[str, Any]]:
|
||||||
if not hasattr(self, "sensor_task_checks"):
|
if not hasattr(self, "sensor_task_checks"):
|
||||||
return []
|
return []
|
||||||
|
reference_path = self.selected_reference_path("sensor")
|
||||||
tasks: list[dict[str, Any]] = []
|
tasks: list[dict[str, Any]] = []
|
||||||
for sensor_key, sensor_label, subtype, label, stage_type in SENSOR_TASK_OPTIONS:
|
for sensor_key, sensor_label, subtype, label, stage_type in SENSOR_TASK_OPTIONS:
|
||||||
check = self.sensor_task_checks.get((sensor_key, subtype))
|
check = self.sensor_task_checks.get((sensor_key, subtype))
|
||||||
@@ -358,6 +598,7 @@ class SessionBuilderMixin:
|
|||||||
"hand_eye.required_pose_count": "1",
|
"hand_eye.required_pose_count": "1",
|
||||||
"hand_eye.timeout_sec": "5.0",
|
"hand_eye.timeout_sec": "5.0",
|
||||||
})
|
})
|
||||||
|
self.add_reference_path_metadata(params, reference_path)
|
||||||
tasks.append(self.make_task(
|
tasks.append(self.make_task(
|
||||||
stage_type,
|
stage_type,
|
||||||
f"sensor.{sensor_key}.{subtype}",
|
f"sensor.{sensor_key}.{subtype}",
|
||||||
|
|||||||
+132
-4
@@ -1,5 +1,7 @@
|
|||||||
from launch import LaunchDescription
|
from launch import LaunchDescription
|
||||||
from launch.actions import DeclareLaunchArgument, OpaqueFunction
|
import sys
|
||||||
|
|
||||||
|
from launch.actions import DeclareLaunchArgument, ExecuteProcess, OpaqueFunction
|
||||||
from launch.substitutions import LaunchConfiguration
|
from launch.substitutions import LaunchConfiguration
|
||||||
from launch_ros.actions import Node
|
from launch_ros.actions import Node
|
||||||
|
|
||||||
@@ -13,6 +15,8 @@ from launch_ros.actions import Node
|
|||||||
# 传感器 readiness 的最小真实配置入口。
|
# 传感器 readiness 的最小真实配置入口。
|
||||||
# external_telemetry_topic / expected_reference_source_name / expected_workcell_zone_id:
|
# external_telemetry_topic / expected_reference_source_name / expected_workcell_zone_id:
|
||||||
# 外部真值定位源的现场配置入口。
|
# 外部真值定位源的现场配置入口。
|
||||||
|
# enable_chassis_telemetry_bridge / chassis_telemetry_*:
|
||||||
|
# 车间电脑侧 WiFi/TCP 底盘遥测接收桥,发布 /chassis/telemetry。
|
||||||
# data_input_params_file:可选 ROS 参数文件,用于把真实采集/ingest 产物路径注入算法输入。
|
# data_input_params_file:可选 ROS 参数文件,用于把真实采集/ingest 产物路径注入算法输入。
|
||||||
# dataset_index_file:可选数据集索引文件;为空时主控会按 data_input_params_file 同目录自动查找。
|
# dataset_index_file:可选数据集索引文件;为空时主控会按 data_input_params_file 同目录自动查找。
|
||||||
def generate_launch_description():
|
def generate_launch_description():
|
||||||
@@ -29,6 +33,16 @@ def generate_launch_description():
|
|||||||
"control_host", default_value="192.168.1.100",
|
"control_host", default_value="192.168.1.100",
|
||||||
description="Windows 车端 IP(use_gateway=true 时生效)",
|
description="Windows 车端 IP(use_gateway=true 时生效)",
|
||||||
)
|
)
|
||||||
|
chassis_timeout_ms_arg = DeclareLaunchArgument(
|
||||||
|
"chassis_timeout_ms",
|
||||||
|
default_value="120000",
|
||||||
|
description="底盘网关 TCP 请求超时,动作原语需要覆盖完整运动时长",
|
||||||
|
)
|
||||||
|
control_timeout_ms_arg = DeclareLaunchArgument(
|
||||||
|
"control_timeout_ms",
|
||||||
|
default_value="120000",
|
||||||
|
description="运控网关 TCP 请求超时,轨迹评估需要覆盖完整运动时长",
|
||||||
|
)
|
||||||
profile_storage_path_arg = DeclareLaunchArgument(
|
profile_storage_path_arg = DeclareLaunchArgument(
|
||||||
"profile_storage_path",
|
"profile_storage_path",
|
||||||
default_value="/tmp/agv_calib_vehicle_profiles.db",
|
default_value="/tmp/agv_calib_vehicle_profiles.db",
|
||||||
@@ -89,11 +103,63 @@ def generate_launch_description():
|
|||||||
default_value="",
|
default_value="",
|
||||||
description="external_localization_service 期望的工位区域 ID,留空表示不校验",
|
description="external_localization_service 期望的工位区域 ID,留空表示不校验",
|
||||||
)
|
)
|
||||||
|
enable_chassis_telemetry_bridge_arg = DeclareLaunchArgument(
|
||||||
|
"enable_chassis_telemetry_bridge",
|
||||||
|
default_value="false",
|
||||||
|
description="true: 启动车间电脑侧 WiFi/TCP 底盘遥测 bridge,发布 chassis_telemetry_topic",
|
||||||
|
)
|
||||||
|
chassis_telemetry_bridge_script_arg = DeclareLaunchArgument(
|
||||||
|
"chassis_telemetry_bridge_script",
|
||||||
|
default_value="src/site_deployment/workshop_chassis_calibration_real/workshop_chassis_telemetry_bridge.py",
|
||||||
|
description="车间电脑侧底盘遥测 bridge Python 脚本路径",
|
||||||
|
)
|
||||||
|
chassis_telemetry_bind_host_arg = DeclareLaunchArgument(
|
||||||
|
"chassis_telemetry_bind_host",
|
||||||
|
default_value="0.0.0.0",
|
||||||
|
description="底盘遥测 TCP 监听地址",
|
||||||
|
)
|
||||||
|
chassis_telemetry_bind_port_arg = DeclareLaunchArgument(
|
||||||
|
"chassis_telemetry_bind_port",
|
||||||
|
default_value="9010",
|
||||||
|
description="底盘遥测 TCP 监听端口",
|
||||||
|
)
|
||||||
|
chassis_telemetry_protocol_arg = DeclareLaunchArgument(
|
||||||
|
"chassis_telemetry_protocol",
|
||||||
|
default_value="frame",
|
||||||
|
description="底盘遥测包协议:frame/json_lines/raw_json/auto",
|
||||||
|
)
|
||||||
|
chassis_telemetry_expected_msg_type_arg = DeclareLaunchArgument(
|
||||||
|
"chassis_telemetry_expected_msg_type",
|
||||||
|
default_value="0",
|
||||||
|
description="frame 协议期望 msg_type;0 表示接受任意类型",
|
||||||
|
)
|
||||||
|
chassis_telemetry_topic_arg = DeclareLaunchArgument(
|
||||||
|
"chassis_telemetry_topic",
|
||||||
|
default_value="/chassis/telemetry",
|
||||||
|
description="底盘遥测 bridge 输出 topic",
|
||||||
|
)
|
||||||
|
chassis_telemetry_default_chassis_type_arg = DeclareLaunchArgument(
|
||||||
|
"chassis_telemetry_default_chassis_type",
|
||||||
|
default_value="ackermann",
|
||||||
|
description="底盘遥测包未携带 chassis_type 时使用的默认车型",
|
||||||
|
)
|
||||||
|
chassis_telemetry_max_payload_bytes_arg = DeclareLaunchArgument(
|
||||||
|
"chassis_telemetry_max_payload_bytes",
|
||||||
|
default_value="262144",
|
||||||
|
description="底盘遥测单包最大 payload 字节数",
|
||||||
|
)
|
||||||
|
chassis_telemetry_send_ack_arg = DeclareLaunchArgument(
|
||||||
|
"chassis_telemetry_send_ack",
|
||||||
|
default_value="true",
|
||||||
|
description="true: bridge 收到遥测后向 TCP 连接回 ACK",
|
||||||
|
)
|
||||||
|
|
||||||
def make_nodes(context):
|
def make_nodes(context):
|
||||||
use_gateway = LaunchConfiguration("use_gateway").perform(context).lower() == "true"
|
use_gateway = LaunchConfiguration("use_gateway").perform(context).lower() == "true"
|
||||||
chassis_host = LaunchConfiguration("chassis_host").perform(context)
|
chassis_host = LaunchConfiguration("chassis_host").perform(context)
|
||||||
control_host = LaunchConfiguration("control_host").perform(context)
|
control_host = LaunchConfiguration("control_host").perform(context)
|
||||||
|
chassis_timeout_ms = int(LaunchConfiguration("chassis_timeout_ms").perform(context))
|
||||||
|
control_timeout_ms = int(LaunchConfiguration("control_timeout_ms").perform(context))
|
||||||
profile_storage_path = LaunchConfiguration("profile_storage_path").perform(context)
|
profile_storage_path = LaunchConfiguration("profile_storage_path").perform(context)
|
||||||
sensor_storage_root = LaunchConfiguration("sensor_storage_root").perform(context)
|
sensor_storage_root = LaunchConfiguration("sensor_storage_root").perform(context)
|
||||||
sensor_registry = LaunchConfiguration("sensor_registry").perform(context)
|
sensor_registry = LaunchConfiguration("sensor_registry").perform(context)
|
||||||
@@ -106,6 +172,26 @@ def generate_launch_description():
|
|||||||
external_telemetry_topic = LaunchConfiguration("external_telemetry_topic").perform(context).strip()
|
external_telemetry_topic = LaunchConfiguration("external_telemetry_topic").perform(context).strip()
|
||||||
expected_reference_source_name = LaunchConfiguration("expected_reference_source_name").perform(context).strip()
|
expected_reference_source_name = LaunchConfiguration("expected_reference_source_name").perform(context).strip()
|
||||||
expected_workcell_zone_id = LaunchConfiguration("expected_workcell_zone_id").perform(context).strip()
|
expected_workcell_zone_id = LaunchConfiguration("expected_workcell_zone_id").perform(context).strip()
|
||||||
|
enable_chassis_telemetry_bridge = (
|
||||||
|
LaunchConfiguration("enable_chassis_telemetry_bridge").perform(context).lower() == "true"
|
||||||
|
)
|
||||||
|
chassis_telemetry_bridge_script = LaunchConfiguration("chassis_telemetry_bridge_script").perform(context).strip()
|
||||||
|
chassis_telemetry_bind_host = LaunchConfiguration("chassis_telemetry_bind_host").perform(context).strip()
|
||||||
|
chassis_telemetry_bind_port = LaunchConfiguration("chassis_telemetry_bind_port").perform(context).strip()
|
||||||
|
chassis_telemetry_protocol = LaunchConfiguration("chassis_telemetry_protocol").perform(context).strip()
|
||||||
|
chassis_telemetry_expected_msg_type = (
|
||||||
|
LaunchConfiguration("chassis_telemetry_expected_msg_type").perform(context).strip()
|
||||||
|
)
|
||||||
|
chassis_telemetry_topic = LaunchConfiguration("chassis_telemetry_topic").perform(context).strip()
|
||||||
|
chassis_telemetry_default_chassis_type = (
|
||||||
|
LaunchConfiguration("chassis_telemetry_default_chassis_type").perform(context).strip()
|
||||||
|
)
|
||||||
|
chassis_telemetry_max_payload_bytes = (
|
||||||
|
LaunchConfiguration("chassis_telemetry_max_payload_bytes").perform(context).strip()
|
||||||
|
)
|
||||||
|
chassis_telemetry_send_ack = (
|
||||||
|
LaunchConfiguration("chassis_telemetry_send_ack").perform(context).lower() == "true"
|
||||||
|
)
|
||||||
|
|
||||||
def with_data_input_params(params=None):
|
def with_data_input_params(params=None):
|
||||||
merged = []
|
merged = []
|
||||||
@@ -172,10 +258,10 @@ def generate_launch_description():
|
|||||||
parameters=[{
|
parameters=[{
|
||||||
"chassis_host": chassis_host,
|
"chassis_host": chassis_host,
|
||||||
"chassis_port": 9000,
|
"chassis_port": 9000,
|
||||||
"chassis_timeout_ms": 5000,
|
"chassis_timeout_ms": chassis_timeout_ms,
|
||||||
"control_host": control_host,
|
"control_host": control_host,
|
||||||
"control_port": 9000,
|
"control_port": 9000,
|
||||||
"control_timeout_ms": 5000,
|
"control_timeout_ms": control_timeout_ms,
|
||||||
}],
|
}],
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
@@ -198,12 +284,44 @@ def generate_launch_description():
|
|||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|
||||||
return common_nodes + chassis_control_nodes
|
telemetry_bridge_nodes = []
|
||||||
|
if enable_chassis_telemetry_bridge:
|
||||||
|
ack_arg = "--send-ack" if chassis_telemetry_send_ack else "--no-send-ack"
|
||||||
|
telemetry_bridge_nodes.append(
|
||||||
|
ExecuteProcess(
|
||||||
|
cmd=[
|
||||||
|
sys.executable,
|
||||||
|
chassis_telemetry_bridge_script,
|
||||||
|
"--bind-host",
|
||||||
|
chassis_telemetry_bind_host,
|
||||||
|
"--bind-port",
|
||||||
|
chassis_telemetry_bind_port,
|
||||||
|
"--protocol",
|
||||||
|
chassis_telemetry_protocol,
|
||||||
|
"--expected-msg-type",
|
||||||
|
chassis_telemetry_expected_msg_type,
|
||||||
|
"--output-topic",
|
||||||
|
chassis_telemetry_topic,
|
||||||
|
"--default-chassis-type",
|
||||||
|
chassis_telemetry_default_chassis_type,
|
||||||
|
"--max-payload-bytes",
|
||||||
|
chassis_telemetry_max_payload_bytes,
|
||||||
|
ack_arg,
|
||||||
|
],
|
||||||
|
name="workshop_chassis_telemetry_bridge",
|
||||||
|
output="screen",
|
||||||
|
emulate_tty=True,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
return common_nodes + chassis_control_nodes + telemetry_bridge_nodes
|
||||||
|
|
||||||
return LaunchDescription([
|
return LaunchDescription([
|
||||||
use_gateway_arg,
|
use_gateway_arg,
|
||||||
chassis_host_arg,
|
chassis_host_arg,
|
||||||
control_host_arg,
|
control_host_arg,
|
||||||
|
chassis_timeout_ms_arg,
|
||||||
|
control_timeout_ms_arg,
|
||||||
profile_storage_path_arg,
|
profile_storage_path_arg,
|
||||||
sensor_storage_root_arg,
|
sensor_storage_root_arg,
|
||||||
sensor_registry_arg,
|
sensor_registry_arg,
|
||||||
@@ -216,5 +334,15 @@ def generate_launch_description():
|
|||||||
external_telemetry_topic_arg,
|
external_telemetry_topic_arg,
|
||||||
expected_reference_source_name_arg,
|
expected_reference_source_name_arg,
|
||||||
expected_workcell_zone_id_arg,
|
expected_workcell_zone_id_arg,
|
||||||
|
enable_chassis_telemetry_bridge_arg,
|
||||||
|
chassis_telemetry_bridge_script_arg,
|
||||||
|
chassis_telemetry_bind_host_arg,
|
||||||
|
chassis_telemetry_bind_port_arg,
|
||||||
|
chassis_telemetry_protocol_arg,
|
||||||
|
chassis_telemetry_expected_msg_type_arg,
|
||||||
|
chassis_telemetry_topic_arg,
|
||||||
|
chassis_telemetry_default_chassis_type_arg,
|
||||||
|
chassis_telemetry_max_payload_bytes_arg,
|
||||||
|
chassis_telemetry_send_ack_arg,
|
||||||
OpaqueFunction(function=make_nodes),
|
OpaqueFunction(function=make_nodes),
|
||||||
])
|
])
|
||||||
|
|||||||
@@ -50,9 +50,14 @@ vehicle_sensor_agent:
|
|||||||
external_pose_bridge:
|
external_pose_bridge:
|
||||||
type: external_pose_wifi6_bridge
|
type: external_pose_wifi6_bridge
|
||||||
source_topic: /isaac/external_localization/vehicle/pose
|
source_topic: /isaac/external_localization/vehicle/pose
|
||||||
reference_source_name: isaac_external_truth
|
reference_source_name: isaac_sim_truth_source
|
||||||
max_hz: 30.0
|
max_hz: 30.0
|
||||||
|
|
||||||
|
external_localization:
|
||||||
|
output_topic: /isaac/external_localization/vehicle/pose
|
||||||
|
reference_source_name: isaac_sim_truth_source
|
||||||
|
workcell_zone_id: isaac_workcell_zone_a
|
||||||
|
|
||||||
workshop_sensor_ingest:
|
workshop_sensor_ingest:
|
||||||
type: workshop_sensor_ingest_sim
|
type: workshop_sensor_ingest_sim
|
||||||
publish_prefix: /workshop/vehicle_sensor
|
publish_prefix: /workshop/vehicle_sensor
|
||||||
|
|||||||
@@ -29,6 +29,17 @@ vehicle_agent:
|
|||||||
wheel_base_m: measured_on_site
|
wheel_base_m: measured_on_site
|
||||||
max_steering_angle_rad: measured_on_site
|
max_steering_angle_rad: measured_on_site
|
||||||
|
|
||||||
|
chassis_telemetry_bridge:
|
||||||
|
type: wifi6_tcp_chassis_telemetry_bridge
|
||||||
|
bridge_tool: src/site_deployment/workshop_chassis_calibration_real/workshop_chassis_telemetry_bridge.py
|
||||||
|
bind_host: 0.0.0.0
|
||||||
|
bind_port: 9010
|
||||||
|
protocol: frame
|
||||||
|
expected_msg_type: 0
|
||||||
|
output_topic: /chassis/telemetry
|
||||||
|
default_chassis_type: ackermann
|
||||||
|
max_payload_bytes: 262144
|
||||||
|
|
||||||
vehicle_sensor_agent:
|
vehicle_sensor_agent:
|
||||||
type: real_vehicle_sensor_agent
|
type: real_vehicle_sensor_agent
|
||||||
front_camera_sensor_id: replace_with_front_camera_id
|
front_camera_sensor_id: replace_with_front_camera_id
|
||||||
@@ -64,6 +75,7 @@ chassis_calibration:
|
|||||||
chassis_type: ackermann
|
chassis_type: ackermann
|
||||||
data_config_file: /data/agv_calib/replace_with_site_or_line_id/chassis_data.yaml
|
data_config_file: /data/agv_calib/replace_with_site_or_line_id/chassis_data.yaml
|
||||||
action_profile_file: /data/agv_calib/replace_with_site_or_line_id/chassis_action_profile.yaml
|
action_profile_file: /data/agv_calib/replace_with_site_or_line_id/chassis_action_profile.yaml
|
||||||
|
reference_path_dir: /data/agv_calib/replace_with_site_or_line_id/chassis_reference_paths
|
||||||
action_profile_tool: src/site_deployment/workshop_chassis_calibration_real/chassis_action_profile.py
|
action_profile_tool: src/site_deployment/workshop_chassis_calibration_real/chassis_action_profile.py
|
||||||
action_profile_capture_runner: src/site_deployment/workshop_chassis_calibration_real/run_chassis_profile_capture.py
|
action_profile_capture_runner: src/site_deployment/workshop_chassis_calibration_real/run_chassis_profile_capture.py
|
||||||
capture_tool: src/site_deployment/workshop_chassis_calibration_real/capture_chassis_session.py
|
capture_tool: src/site_deployment/workshop_chassis_calibration_real/capture_chassis_session.py
|
||||||
@@ -89,6 +101,7 @@ control_calibration:
|
|||||||
data_config_file: /data/agv_calib/replace_with_site_or_line_id/control_data.yaml
|
data_config_file: /data/agv_calib/replace_with_site_or_line_id/control_data.yaml
|
||||||
capture_tool: src/site_deployment/workshop_control_calibration_real/capture_control_session.py
|
capture_tool: src/site_deployment/workshop_control_calibration_real/capture_control_session.py
|
||||||
evaluation_profile_file: src/site_deployment/workshop_control_calibration_real/config/control_evaluation_profile.yaml
|
evaluation_profile_file: src/site_deployment/workshop_control_calibration_real/config/control_evaluation_profile.yaml
|
||||||
|
reference_path_dir: /data/agv_calib/replace_with_site_or_line_id/control_reference_paths
|
||||||
evaluation_profile_tool: src/site_deployment/workshop_control_calibration_real/control_evaluation_profile.py
|
evaluation_profile_tool: src/site_deployment/workshop_control_calibration_real/control_evaluation_profile.py
|
||||||
evaluation_profile_capture_runner: src/site_deployment/workshop_control_calibration_real/run_control_profile_capture.py
|
evaluation_profile_capture_runner: src/site_deployment/workshop_control_calibration_real/run_control_profile_capture.py
|
||||||
evaluation_profile_capture_smoke_tool: src/site_deployment/workshop_control_calibration_real/smoke_test_control_profile_capture.py
|
evaluation_profile_capture_smoke_tool: src/site_deployment/workshop_control_calibration_real/smoke_test_control_profile_capture.py
|
||||||
@@ -115,6 +128,7 @@ sensor_calibration:
|
|||||||
# 这里只定义传感器标定算法的数据输入、任务 profile 和参数交接产物。
|
# 这里只定义传感器标定算法的数据输入、任务 profile 和参数交接产物。
|
||||||
type: workshop_sensor_data
|
type: workshop_sensor_data
|
||||||
profile_file: src/site_deployment/workshop_sensor_calibration_real/config/sensor_calibration_profile.yaml
|
profile_file: src/site_deployment/workshop_sensor_calibration_real/config/sensor_calibration_profile.yaml
|
||||||
|
reference_path_dir: /data/agv_calib/replace_with_site_or_line_id/sensor_reference_paths
|
||||||
profile_tool: src/site_deployment/workshop_sensor_calibration_real/sensor_calibration_profile.py
|
profile_tool: src/site_deployment/workshop_sensor_calibration_real/sensor_calibration_profile.py
|
||||||
dataset_manifest_tool: src/site_deployment/workshop_sensor_calibration_real/sensor_dataset_manifest.py
|
dataset_manifest_tool: src/site_deployment/workshop_sensor_calibration_real/sensor_dataset_manifest.py
|
||||||
preprocess_tool: src/site_deployment/workshop_sensor_calibration_real/sensor_preprocess_pipeline.py
|
preprocess_tool: src/site_deployment/workshop_sensor_calibration_real/sensor_preprocess_pipeline.py
|
||||||
|
|||||||
@@ -194,27 +194,45 @@ def normalize_profile_task(raw_task: dict[str, Any]) -> dict[str, Any]:
|
|||||||
return task
|
return task
|
||||||
|
|
||||||
|
|
||||||
def export_chassis_profile_tasks(profile_path: Path, chassis_type: str) -> list[dict[str, Any]]:
|
def export_chassis_profile_tasks(
|
||||||
|
profile_path: Path,
|
||||||
|
chassis_type: str,
|
||||||
|
reference_path_dir: Path | None = None,
|
||||||
|
) -> list[dict[str, Any]]:
|
||||||
tool_path = REPO_ROOT / "src/site_deployment/workshop_chassis_calibration_real/chassis_action_profile.py"
|
tool_path = REPO_ROOT / "src/site_deployment/workshop_chassis_calibration_real/chassis_action_profile.py"
|
||||||
tool = load_module("chassis_action_profile_tool", tool_path)
|
tool = load_module("chassis_action_profile_tool", tool_path)
|
||||||
profile = tool.validate_profile(tool.load_yaml(profile_path))
|
profile = tool.validate_profile(tool.load_yaml(profile_path))
|
||||||
exported = tool.export_requested_tasks(profile, chassis_type)
|
if reference_path_dir is not None:
|
||||||
|
profile["reference_path_dir"] = str(reference_path_dir)
|
||||||
|
exported = tool.export_requested_tasks(profile, chassis_type, profile_path)
|
||||||
return [normalize_profile_task(raw_task) for raw_task in exported["requested_tasks"]]
|
return [normalize_profile_task(raw_task) for raw_task in exported["requested_tasks"]]
|
||||||
|
|
||||||
|
|
||||||
def export_control_profile_tasks(profile_path: Path, chassis_type: str) -> list[dict[str, Any]]:
|
def export_control_profile_tasks(
|
||||||
|
profile_path: Path,
|
||||||
|
chassis_type: str,
|
||||||
|
reference_path_dir: Path | None = None,
|
||||||
|
) -> list[dict[str, Any]]:
|
||||||
tool_path = REPO_ROOT / "src/site_deployment/workshop_control_calibration_real/control_evaluation_profile.py"
|
tool_path = REPO_ROOT / "src/site_deployment/workshop_control_calibration_real/control_evaluation_profile.py"
|
||||||
tool = load_module("control_evaluation_profile_tool", tool_path)
|
tool = load_module("control_evaluation_profile_tool", tool_path)
|
||||||
profile = tool.validate_profile(tool.load_yaml(profile_path))
|
profile = tool.validate_profile(tool.load_yaml(profile_path))
|
||||||
exported = tool.export_requested_tasks(profile, chassis_type)
|
if reference_path_dir is not None:
|
||||||
|
profile["reference_path_dir"] = str(reference_path_dir)
|
||||||
|
exported = tool.export_requested_tasks(profile, chassis_type, profile_path)
|
||||||
return [normalize_profile_task(raw_task) for raw_task in exported["requested_tasks"]]
|
return [normalize_profile_task(raw_task) for raw_task in exported["requested_tasks"]]
|
||||||
|
|
||||||
|
|
||||||
def export_sensor_profile_tasks(profile_path: Path, task_name: str) -> list[dict[str, Any]]:
|
def export_sensor_profile_tasks(
|
||||||
|
profile_path: Path,
|
||||||
|
task_name: str,
|
||||||
|
reference_path_dir: Path | None = None,
|
||||||
|
) -> list[dict[str, Any]]:
|
||||||
tool_path = REPO_ROOT / "src/site_deployment/workshop_sensor_calibration_real/sensor_calibration_profile.py"
|
tool_path = REPO_ROOT / "src/site_deployment/workshop_sensor_calibration_real/sensor_calibration_profile.py"
|
||||||
tool = load_module("sensor_calibration_profile_tool", tool_path)
|
tool = load_module("sensor_calibration_profile_tool", tool_path)
|
||||||
profile = tool.validate_profile(tool.load_yaml(profile_path))
|
profile = tool.validate_profile(tool.load_yaml(profile_path))
|
||||||
exported = tool.export_requested_tasks(profile, {task_name})
|
if reference_path_dir is not None:
|
||||||
|
profile["reference_path_dir"] = str(reference_path_dir)
|
||||||
|
exported = tool.export_requested_tasks(profile, {task_name}, profile_path)
|
||||||
return [normalize_profile_task(raw_task) for raw_task in exported["requested_tasks"]]
|
return [normalize_profile_task(raw_task) for raw_task in exported["requested_tasks"]]
|
||||||
|
|
||||||
|
|
||||||
@@ -327,32 +345,62 @@ def build_requested_tasks(
|
|||||||
site_profile_path,
|
site_profile_path,
|
||||||
("chassis_calibration", "action_profile_file"),
|
("chassis_calibration", "action_profile_file"),
|
||||||
)
|
)
|
||||||
|
chassis_reference_path_dir = resolve_configured_path(
|
||||||
|
"",
|
||||||
|
site_profile,
|
||||||
|
site_profile_path,
|
||||||
|
("chassis_calibration", "reference_path_dir"),
|
||||||
|
)
|
||||||
control_profile_path = resolve_configured_path(
|
control_profile_path = resolve_configured_path(
|
||||||
args.control_evaluation_profile,
|
args.control_evaluation_profile,
|
||||||
site_profile,
|
site_profile,
|
||||||
site_profile_path,
|
site_profile_path,
|
||||||
("control_calibration", "evaluation_profile_file"),
|
("control_calibration", "evaluation_profile_file"),
|
||||||
)
|
)
|
||||||
|
control_reference_path_dir = resolve_configured_path(
|
||||||
|
"",
|
||||||
|
site_profile,
|
||||||
|
site_profile_path,
|
||||||
|
("control_calibration", "reference_path_dir"),
|
||||||
|
)
|
||||||
sensor_profile_path = resolve_configured_path(
|
sensor_profile_path = resolve_configured_path(
|
||||||
args.sensor_calibration_profile,
|
args.sensor_calibration_profile,
|
||||||
site_profile,
|
site_profile,
|
||||||
site_profile_path,
|
site_profile_path,
|
||||||
("sensor_calibration", "profile_file"),
|
("sensor_calibration", "profile_file"),
|
||||||
)
|
)
|
||||||
|
sensor_reference_path_dir = resolve_configured_path(
|
||||||
|
"",
|
||||||
|
site_profile,
|
||||||
|
site_profile_path,
|
||||||
|
("sensor_calibration", "reference_path_dir"),
|
||||||
|
)
|
||||||
|
|
||||||
requested_tasks: list[dict[str, Any]] = []
|
requested_tasks: list[dict[str, Any]] = []
|
||||||
for task_name in tasks:
|
for task_name in tasks:
|
||||||
if task_name == "chassis" and profile_tasks_available(chassis_profile_path, args.profile_mode, "底盘动作"):
|
if task_name == "chassis" and profile_tasks_available(chassis_profile_path, args.profile_mode, "底盘动作"):
|
||||||
requested_tasks.extend(export_chassis_profile_tasks(chassis_profile_path, chassis_type))
|
requested_tasks.extend(export_chassis_profile_tasks(
|
||||||
|
chassis_profile_path,
|
||||||
|
chassis_type,
|
||||||
|
chassis_reference_path_dir,
|
||||||
|
))
|
||||||
continue
|
continue
|
||||||
if task_name == "control" and profile_tasks_available(control_profile_path, args.profile_mode, "运控评估"):
|
if task_name == "control" and profile_tasks_available(control_profile_path, args.profile_mode, "运控评估"):
|
||||||
requested_tasks.extend(export_control_profile_tasks(control_profile_path, chassis_type))
|
requested_tasks.extend(export_control_profile_tasks(
|
||||||
|
control_profile_path,
|
||||||
|
chassis_type,
|
||||||
|
control_reference_path_dir,
|
||||||
|
))
|
||||||
continue
|
continue
|
||||||
if (
|
if (
|
||||||
task_name in {"sensor_intrinsic", "sensor_extrinsic", "hand_eye"} and
|
task_name in {"sensor_intrinsic", "sensor_extrinsic", "hand_eye"} and
|
||||||
profile_tasks_available(sensor_profile_path, args.profile_mode, "传感器标定")
|
profile_tasks_available(sensor_profile_path, args.profile_mode, "传感器标定")
|
||||||
):
|
):
|
||||||
requested_tasks.extend(export_sensor_profile_tasks(sensor_profile_path, task_name))
|
requested_tasks.extend(export_sensor_profile_tasks(
|
||||||
|
sensor_profile_path,
|
||||||
|
task_name,
|
||||||
|
sensor_reference_path_dir,
|
||||||
|
))
|
||||||
continue
|
continue
|
||||||
requested_tasks.append(minimal_task(task_name, args, site_profile, chassis_type))
|
requested_tasks.append(minimal_task(task_name, args, site_profile, chassis_type))
|
||||||
return requested_tasks
|
return requested_tasks
|
||||||
|
|||||||
@@ -10,7 +10,8 @@
|
|||||||
- 墙面棋盘格阵列。
|
- 墙面棋盘格阵列。
|
||||||
- 2D LiDAR 外参标定角落。
|
- 2D LiDAR 外参标定角落。
|
||||||
- 下视相机 3D ChArUco 内参标定台。
|
- 下视相机 3D ChArUco 内参标定台。
|
||||||
- 底盘标定路线和停车标记。
|
- 标定路径不在 Isaac 场景中固化,由各标定模块的 `reference_paths/*.yaml` 选择和录制。
|
||||||
|
- 外部真值遥测发布;Isaac 默认不显示黄球/观测连线这类定位调试可视化。
|
||||||
- 场景 manifest 生成。
|
- 场景 manifest 生成。
|
||||||
|
|
||||||
默认启动:
|
默认启动:
|
||||||
@@ -34,6 +35,13 @@ headless 模式:
|
|||||||
python3 src/simulation/tools/launch_sim_stack.py --component isaac --headless
|
python3 src/simulation/tools/launch_sim_stack.py --component isaac --headless
|
||||||
```
|
```
|
||||||
|
|
||||||
|
外部真值定位调试可视化默认关闭。需要临时查看四角 LiDAR、车端标定球和观测连线时:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python3 src/simulation/tools/launch_sim_stack.py --component isaac \
|
||||||
|
--isaac-arg=--enable-external-truth-visualization
|
||||||
|
```
|
||||||
|
|
||||||
边界规则:
|
边界规则:
|
||||||
|
|
||||||
- Isaac API 只能留在这里。
|
- Isaac API 只能留在这里。
|
||||||
|
|||||||
+389
-57
@@ -48,6 +48,11 @@ def parse_args():
|
|||||||
parser.add_argument("--camera-focal-length", type=float, default=5.0, help="相机焦距")
|
parser.add_argument("--camera-focal-length", type=float, default=5.0, help="相机焦距")
|
||||||
parser.add_argument("--ros-topic-prefix", type=str, default="/AutoCalib_Workshop", help="相机与激光雷达话题前缀")
|
parser.add_argument("--ros-topic-prefix", type=str, default="/AutoCalib_Workshop", help="相机与激光雷达话题前缀")
|
||||||
parser.add_argument("--cmd-vel-topic", type=str, default="/cmd_vel", help="车辆速度控制话题")
|
parser.add_argument("--cmd-vel-topic", type=str, default="/cmd_vel", help="车辆速度控制话题")
|
||||||
|
parser.add_argument(
|
||||||
|
"--disable-kinematic-command-follow",
|
||||||
|
action="store_true",
|
||||||
|
help="禁用仿真车辆按 cmd_vel 直接积分更新位姿;默认启用,便于在 Isaac 中可视化路径执行。",
|
||||||
|
)
|
||||||
parser.add_argument("--lidar-config", type=str, default="Example_Rotary", help="Isaac LiDAR 配置名")
|
parser.add_argument("--lidar-config", type=str, default="Example_Rotary", help="Isaac LiDAR 配置名")
|
||||||
parser.add_argument("--lidar-3d-config", type=str, default="Hesai_XT32_SD10", help="车载3D LiDAR 配置名(默认32线)")
|
parser.add_argument("--lidar-3d-config", type=str, default="Hesai_XT32_SD10", help="车载3D LiDAR 配置名(默认32线)")
|
||||||
parser.add_argument("--disable-camera", action="store_true", help="不创建顶置相机")
|
parser.add_argument("--disable-camera", action="store_true", help="不创建顶置相机")
|
||||||
@@ -105,6 +110,9 @@ def parse_args():
|
|||||||
parser.add_argument("--external-quality-score", type=float, default=0.98, help="上报的 external 质量分数")
|
parser.add_argument("--external-quality-score", type=float, default=0.98, help="上报的 external 质量分数")
|
||||||
parser.add_argument("--external-observed-target-count", type=int, default=4, help="上报的目标观测数量")
|
parser.add_argument("--external-observed-target-count", type=int, default=4, help="上报的目标观测数量")
|
||||||
parser.add_argument("--external-pose-drop-rate", type=float, default=0.0, help="位姿失效率,取值 [0,1]")
|
parser.add_argument("--external-pose-drop-rate", type=float, default=0.0, help="位姿失效率,取值 [0,1]")
|
||||||
|
parser.add_argument("--enable-external-truth-visualization", action="store_true", help="在 Isaac 场景中显示外部真值定位调试可视化")
|
||||||
|
parser.add_argument("--disable-external-truth-visualization", action="store_true", help=argparse.SUPPRESS)
|
||||||
|
parser.add_argument("--external-target-ball-radius", type=float, default=0.075, help="车端外部定位标定球可视化半径(米)")
|
||||||
parser.add_argument("--disable-chassis-telemetry", action="store_true", help="不发布底盘标定遥测")
|
parser.add_argument("--disable-chassis-telemetry", action="store_true", help="不发布底盘标定遥测")
|
||||||
parser.add_argument("--chassis-telemetry-topic", type=str, default="/chassis/telemetry", help="底盘标定遥测话题")
|
parser.add_argument("--chassis-telemetry-topic", type=str, default="/chassis/telemetry", help="底盘标定遥测话题")
|
||||||
parser.add_argument("--chassis-telemetry-hz", type=float, default=20.0, help="底盘标定遥测发布频率(Hz)")
|
parser.add_argument("--chassis-telemetry-hz", type=float, default=20.0, help="底盘标定遥测发布频率(Hz)")
|
||||||
@@ -122,8 +130,8 @@ def parse_args():
|
|||||||
parser.add_argument("--disable-control-telemetry", action="store_true", help="不发布运控评估遥测")
|
parser.add_argument("--disable-control-telemetry", action="store_true", help="不发布运控评估遥测")
|
||||||
parser.add_argument("--control-telemetry-topic", type=str, default="/control/telemetry", help="运控评估遥测话题")
|
parser.add_argument("--control-telemetry-topic", type=str, default="/control/telemetry", help="运控评估遥测话题")
|
||||||
parser.add_argument("--control-telemetry-hz", type=float, default=20.0, help="运控评估遥测发布频率(Hz)")
|
parser.add_argument("--control-telemetry-hz", type=float, default=20.0, help="运控评估遥测发布频率(Hz)")
|
||||||
parser.add_argument("--control-reference-speed-ms", type=float, default=0.3, help="运控遥测参考速度(m/s)")
|
parser.add_argument("--control-reference-speed-ms", type=float, default=0.3, help="兼容旧启动参数;Isaac 不再生成运控参考速度")
|
||||||
parser.add_argument("--control-reference-y-m", type=float, default=0.0, help="运控直线参考轨迹的 Y 坐标(米)")
|
parser.add_argument("--control-reference-y-m", type=float, default=0.0, help="兼容旧启动参数;Isaac 不再生成运控参考轨迹")
|
||||||
parser.add_argument("--control-parameter-version", type=str, default="isaac_control_baseline_v1", help="当前控制参数版本")
|
parser.add_argument("--control-parameter-version", type=str, default="isaac_control_baseline_v1", help="当前控制参数版本")
|
||||||
parser.add_argument("--disable-vehicle-tf", action="store_true", help="不发布车辆 TF 变换")
|
parser.add_argument("--disable-vehicle-tf", action="store_true", help="不发布车辆 TF 变换")
|
||||||
parser.add_argument("--vehicle-tf-publish-hz", type=float, default=50.0, help="车辆 TF 发布频率(Hz)")
|
parser.add_argument("--vehicle-tf-publish-hz", type=float, default=50.0, help="车辆 TF 发布频率(Hz)")
|
||||||
@@ -135,10 +143,10 @@ def parse_args():
|
|||||||
parser.add_argument("--sensor-quality-score", type=float, default=0.95, help="传感器观测质量评分,取值 [0,1]")
|
parser.add_argument("--sensor-quality-score", type=float, default=0.95, help="传感器观测质量评分,取值 [0,1]")
|
||||||
parser.add_argument("--sensor-target-missing", action="store_true", help="发布 target_detected=false 的传感器遥测")
|
parser.add_argument("--sensor-target-missing", action="store_true", help="发布 target_detected=false 的传感器遥测")
|
||||||
parser.add_argument("--disable-calibration-boards", action="store_true", help="不创建多姿态棋盘格标定目标阵列")
|
parser.add_argument("--disable-calibration-boards", action="store_true", help="不创建多姿态棋盘格标定目标阵列")
|
||||||
parser.add_argument("--disable-calibration-fixtures", action="store_true", help="不创建地面标定路线和停车目标标识")
|
parser.add_argument("--disable-calibration-fixtures", action="store_true", help="兼容旧启动参数;Isaac 不再创建地面标定路线")
|
||||||
parser.add_argument("--straight-track-length", type=float, default=5.0, help="底盘直线标定路线长度(米)")
|
parser.add_argument("--straight-track-length", type=float, default=5.0, help="兼容旧启动参数;标定路径由 reference_paths/*.yaml 提供")
|
||||||
parser.add_argument("--straight-track-width", type=float, default=0.7, help="底盘直线标定路线宽度(米)")
|
parser.add_argument("--straight-track-width", type=float, default=0.7, help="兼容旧启动参数;标定路径由 reference_paths/*.yaml 提供")
|
||||||
parser.add_argument("--arc-track-radius", type=float, default=1.6, help="圆弧/转向标定路线半径(米)")
|
parser.add_argument("--arc-track-radius", type=float, default=1.6, help="兼容旧启动参数;标定路径由 reference_paths/*.yaml 提供")
|
||||||
parser.add_argument("--random-seed", type=int, default=42, help="随机种子,保证实验可重复")
|
parser.add_argument("--random-seed", type=int, default=42, help="随机种子,保证实验可重复")
|
||||||
parser.add_argument("--vehicle-id", type=str, default="demo_agv_001", help="车辆 ID,用于 manifest 和后端会话配置对齐")
|
parser.add_argument("--vehicle-id", type=str, default="demo_agv_001", help="车辆 ID,用于 manifest 和后端会话配置对齐")
|
||||||
parser.add_argument("--vehicle-initial-x", type=float, default=0.0, help="车辆初始 X 坐标")
|
parser.add_argument("--vehicle-initial-x", type=float, default=0.0, help="车辆初始 X 坐标")
|
||||||
@@ -257,12 +265,12 @@ def validate_args(args):
|
|||||||
raise ValueError("sensor_quality_score 必须在 [0,1] 范围内。")
|
raise ValueError("sensor_quality_score 必须在 [0,1] 范围内。")
|
||||||
if args.external_observed_target_count < 0 or args.sensor_target_sample_count < 0:
|
if args.external_observed_target_count < 0 or args.sensor_target_sample_count < 0:
|
||||||
raise ValueError("目标数量/样本数不能为负数。")
|
raise ValueError("目标数量/样本数不能为负数。")
|
||||||
|
if args.external_target_ball_radius <= 0:
|
||||||
|
raise ValueError("external_target_ball_radius 必须大于 0。")
|
||||||
if args.wheel_radius <= 0 or args.wheel_track <= 0 or args.wheel_base <= 0:
|
if args.wheel_radius <= 0 or args.wheel_track <= 0 or args.wheel_base <= 0:
|
||||||
raise ValueError("wheel_radius、wheel_track 和 wheel_base 必须大于 0。")
|
raise ValueError("wheel_radius、wheel_track 和 wheel_base 必须大于 0。")
|
||||||
if args.odom_noise_stddev_m < 0 or args.yaw_noise_stddev_rad < 0:
|
if args.odom_noise_stddev_m < 0 or args.yaw_noise_stddev_rad < 0:
|
||||||
raise ValueError("里程计噪声不能为负数。")
|
raise ValueError("里程计噪声不能为负数。")
|
||||||
if args.straight_track_length <= 0 or args.straight_track_width <= 0 or args.arc_track_radius <= 0:
|
|
||||||
raise ValueError("标定路线尺寸必须大于 0。")
|
|
||||||
if args.lidar_2d_target_width <= 0 or args.lidar_2d_target_height <= 0 or args.lidar_2d_target_thickness <= 0:
|
if args.lidar_2d_target_width <= 0 or args.lidar_2d_target_height <= 0 or args.lidar_2d_target_thickness <= 0:
|
||||||
raise ValueError("2D LiDAR 靶标尺寸必须大于 0。")
|
raise ValueError("2D LiDAR 靶标尺寸必须大于 0。")
|
||||||
if args.lidar_2d_target_bottom_z < 0:
|
if args.lidar_2d_target_bottom_z < 0:
|
||||||
@@ -318,7 +326,7 @@ import omni.graph.core as og
|
|||||||
import omni.kit.commands
|
import omni.kit.commands
|
||||||
import omni.usd
|
import omni.usd
|
||||||
import omni.replicator.core as rep
|
import omni.replicator.core as rep
|
||||||
from pxr import Gf, PhysxSchema, Sdf, UsdGeom, UsdShade, Vt
|
from pxr import Gf, PhysxSchema, Sdf, UsdGeom, UsdShade
|
||||||
|
|
||||||
from omni.isaac.core import World
|
from omni.isaac.core import World
|
||||||
from omni.isaac.core.objects import FixedCuboid
|
from omni.isaac.core.objects import FixedCuboid
|
||||||
@@ -331,7 +339,6 @@ from omni.isaac.core.utils.viewports import set_camera_view
|
|||||||
from usd_utils import create_raw_usd_material, create_textured_board, create_textured_top_strip
|
from usd_utils import create_raw_usd_material, create_textured_board, create_textured_top_strip
|
||||||
from calibration_targets import (
|
from calibration_targets import (
|
||||||
add_calibration_boards,
|
add_calibration_boards,
|
||||||
add_calibration_floor_fixtures,
|
|
||||||
add_down_camera_intrinsic_target,
|
add_down_camera_intrinsic_target,
|
||||||
add_2d_lidar_calibration_targets,
|
add_2d_lidar_calibration_targets,
|
||||||
lidar_2d_checkerboard_reserved_zones,
|
lidar_2d_checkerboard_reserved_zones,
|
||||||
@@ -381,6 +388,45 @@ except ImportError:
|
|||||||
Imu = None
|
Imu = None
|
||||||
LaserScan = None
|
LaserScan = None
|
||||||
|
|
||||||
|
try:
|
||||||
|
from geometry_msgs.msg import Twist
|
||||||
|
except ImportError:
|
||||||
|
Twist = None
|
||||||
|
|
||||||
|
|
||||||
|
def external_lidar_mount_specs(room_length, room_width, height):
|
||||||
|
"""Return the four fixed workshop LiDAR poses used by external truth visualization."""
|
||||||
|
offset = 0.3
|
||||||
|
x_pos = (room_length / 2.0) - offset
|
||||||
|
y_pos = (room_width / 2.0) - offset
|
||||||
|
return [
|
||||||
|
{"name": "FL", "pos": [x_pos, y_pos, height], "yaw": np.degrees(np.arctan2(-y_pos, -x_pos))},
|
||||||
|
{"name": "FR", "pos": [x_pos, -y_pos, height], "yaw": np.degrees(np.arctan2(y_pos, -x_pos))},
|
||||||
|
{"name": "BL", "pos": [-x_pos, y_pos, height], "yaw": np.degrees(np.arctan2(-y_pos, x_pos))},
|
||||||
|
{"name": "BR", "pos": [-x_pos, -y_pos, height], "yaw": np.degrees(np.arctan2(y_pos, x_pos))},
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def create_preview_material(stage, mat_path, color, opacity=1.0, emissive_scale=0.0):
|
||||||
|
material = UsdShade.Material.Define(stage, mat_path)
|
||||||
|
shader = UsdShade.Shader.Define(stage, f"{mat_path}/PreviewSurface")
|
||||||
|
shader.CreateIdAttr("UsdPreviewSurface")
|
||||||
|
shader.CreateInput("diffuseColor", Sdf.ValueTypeNames.Color3f).Set(Gf.Vec3f(*color))
|
||||||
|
shader.CreateInput("roughness", Sdf.ValueTypeNames.Float).Set(0.55)
|
||||||
|
shader.CreateInput("metallic", Sdf.ValueTypeNames.Float).Set(0.0)
|
||||||
|
if emissive_scale > 0.0:
|
||||||
|
shader.CreateInput("emissiveColor", Sdf.ValueTypeNames.Color3f).Set(
|
||||||
|
Gf.Vec3f(*(min(1.0, channel * emissive_scale) for channel in color))
|
||||||
|
)
|
||||||
|
if opacity < 1.0:
|
||||||
|
shader.CreateInput("opacity", Sdf.ValueTypeNames.Float).Set(float(opacity))
|
||||||
|
material.CreateSurfaceOutput().ConnectToSource(shader.ConnectableAPI(), "surface")
|
||||||
|
return material
|
||||||
|
|
||||||
|
|
||||||
|
def bind_material(prim, material):
|
||||||
|
UsdShade.MaterialBindingAPI.Apply(prim).Bind(material)
|
||||||
|
|
||||||
|
|
||||||
def add_corner_rotary_lidars(room_length, room_width, height, lidar_config, topic_prefix):
|
def add_corner_rotary_lidars(room_length, room_width, height, lidar_config, topic_prefix):
|
||||||
"""添加四角旋转 LiDAR"""
|
"""添加四角旋转 LiDAR"""
|
||||||
@@ -390,16 +436,7 @@ def add_corner_rotary_lidars(room_length, room_width, height, lidar_config, topi
|
|||||||
import omni.replicator.core as rep
|
import omni.replicator.core as rep
|
||||||
from pxr import Gf
|
from pxr import Gf
|
||||||
|
|
||||||
offset = 0.3
|
lidar_configs = external_lidar_mount_specs(room_length, room_width, height)
|
||||||
x_pos = (room_length / 2.0) - offset
|
|
||||||
y_pos = (room_width / 2.0) - offset
|
|
||||||
|
|
||||||
lidar_configs = [
|
|
||||||
{"name": "FL", "pos": [x_pos, y_pos, height], "yaw": np.degrees(np.arctan2(-y_pos, -x_pos))},
|
|
||||||
{"name": "FR", "pos": [x_pos, -y_pos, height], "yaw": np.degrees(np.arctan2(y_pos, -x_pos))},
|
|
||||||
{"name": "BL", "pos": [-x_pos, y_pos, height], "yaw": np.degrees(np.arctan2(-y_pos, x_pos))},
|
|
||||||
{"name": "BR", "pos": [-x_pos, -y_pos, height], "yaw": np.degrees(np.arctan2(y_pos, x_pos))},
|
|
||||||
]
|
|
||||||
|
|
||||||
keys = og.Controller.Keys
|
keys = og.Controller.Keys
|
||||||
graph_path = "/World/ROS2_Lidar_Graph"
|
graph_path = "/World/ROS2_Lidar_Graph"
|
||||||
@@ -447,6 +484,194 @@ def add_corner_rotary_lidars(room_length, room_width, height, lidar_config, topi
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class ExternalTruthSceneVisualization:
|
||||||
|
"""Isaac viewport visualization for the external truth localization flow."""
|
||||||
|
|
||||||
|
TARGET_BALLS = [
|
||||||
|
{"name": "front_left", "pos": np.array([0.58, 0.32, 1.28], dtype=float)},
|
||||||
|
{"name": "front_right", "pos": np.array([0.58, -0.32, 1.28], dtype=float)},
|
||||||
|
{"name": "rear_center", "pos": np.array([-0.50, 0.0, 1.22], dtype=float)},
|
||||||
|
]
|
||||||
|
|
||||||
|
def __init__(self, args, world, vehicle_mount_parent_path):
|
||||||
|
self.enabled = args.enable_external_truth_visualization and not args.disable_external_truth_visualization
|
||||||
|
self.args = args
|
||||||
|
self.stage = omni.usd.get_context().get_stage()
|
||||||
|
self.root_path = "/World/ExternalTruthVisualization"
|
||||||
|
self.vehicle_mount_parent_path = vehicle_mount_parent_path
|
||||||
|
self.lidar_specs = external_lidar_mount_specs(
|
||||||
|
args.room_length,
|
||||||
|
args.room_width,
|
||||||
|
args.room_height - 0.2,
|
||||||
|
)
|
||||||
|
self.observation_segments = []
|
||||||
|
self.heading_segments = []
|
||||||
|
self.uncertainty_segments = []
|
||||||
|
self.last_update_wall_time = 0.0
|
||||||
|
|
||||||
|
if not self.enabled:
|
||||||
|
print("[*] Isaac 外部真值定位调试可视化未启用。")
|
||||||
|
return
|
||||||
|
|
||||||
|
UsdGeom.Xform.Define(self.stage, self.root_path)
|
||||||
|
UsdGeom.Xform.Define(self.stage, f"{self.root_path}/Materials")
|
||||||
|
UsdGeom.Xform.Define(self.stage, f"{self.root_path}/LidarStations")
|
||||||
|
UsdGeom.Xform.Define(self.stage, f"{self.root_path}/ObservationLines")
|
||||||
|
UsdGeom.Xform.Define(self.stage, f"{self.root_path}/PoseOverlay")
|
||||||
|
self.materials = {
|
||||||
|
"tower": create_preview_material(self.stage, f"{self.root_path}/Materials/TowerCyan", (0.10, 0.72, 0.84), emissive_scale=0.35),
|
||||||
|
"beam": create_preview_material(self.stage, f"{self.root_path}/Materials/ObservationBeam", (0.20, 0.85, 1.0), opacity=0.72, emissive_scale=0.25),
|
||||||
|
"ball": create_preview_material(self.stage, f"{self.root_path}/Materials/TargetBall", (1.0, 0.72, 0.12), emissive_scale=0.2),
|
||||||
|
"pose": create_preview_material(self.stage, f"{self.root_path}/Materials/PoseArrow", (0.24, 0.90, 0.45), emissive_scale=0.35),
|
||||||
|
"uncertainty": create_preview_material(self.stage, f"{self.root_path}/Materials/PoseUncertainty", (0.98, 0.84, 0.25), opacity=0.85, emissive_scale=0.15),
|
||||||
|
}
|
||||||
|
self._create_lidar_station_visuals(world)
|
||||||
|
self._create_vehicle_target_balls()
|
||||||
|
self._create_dynamic_segments()
|
||||||
|
print("[*] Isaac 外部真值定位可视化已启用:四角 LiDAR、车端标定球、观测连线、真值姿态箭头。")
|
||||||
|
|
||||||
|
def _create_lidar_station_visuals(self, world):
|
||||||
|
for spec in self.lidar_specs:
|
||||||
|
name = spec["name"]
|
||||||
|
x, y, z = spec["pos"]
|
||||||
|
stand_height = max(z, 0.4)
|
||||||
|
world.scene.add(FixedCuboid(
|
||||||
|
prim_path=f"{self.root_path}/LidarStations/{name}_Stand",
|
||||||
|
name=f"external_truth_lidar_{name.lower()}_stand",
|
||||||
|
position=np.array([x, y, stand_height / 2.0]),
|
||||||
|
scale=np.array([0.08, 0.08, stand_height]),
|
||||||
|
color=np.array([0.08, 0.52, 0.62]),
|
||||||
|
))
|
||||||
|
world.scene.add(FixedCuboid(
|
||||||
|
prim_path=f"{self.root_path}/LidarStations/{name}_Head",
|
||||||
|
name=f"external_truth_lidar_{name.lower()}_head",
|
||||||
|
position=np.array([x, y, z]),
|
||||||
|
scale=np.array([0.22, 0.22, 0.16]),
|
||||||
|
color=np.array([0.12, 0.78, 0.90]),
|
||||||
|
))
|
||||||
|
|
||||||
|
def _create_vehicle_target_balls(self):
|
||||||
|
target_root = f"{self.vehicle_mount_parent_path}/ExternalTruthTargets"
|
||||||
|
UsdGeom.Xform.Define(self.stage, target_root)
|
||||||
|
for target in self.TARGET_BALLS:
|
||||||
|
sphere_path = f"{target_root}/Ball_{target['name']}"
|
||||||
|
sphere = UsdGeom.Sphere.Define(self.stage, sphere_path)
|
||||||
|
sphere.GetRadiusAttr().Set(float(self.args.external_target_ball_radius))
|
||||||
|
xform = UsdGeom.Xformable(sphere.GetPrim())
|
||||||
|
xform.AddTranslateOp().Set(Gf.Vec3d(*target["pos"]))
|
||||||
|
bind_material(sphere.GetPrim(), self.materials["ball"])
|
||||||
|
|
||||||
|
def _make_segment(self, path, width, material):
|
||||||
|
cube = UsdGeom.Cube.Define(self.stage, path)
|
||||||
|
cube.CreateSizeAttr(1.0)
|
||||||
|
xform = UsdGeom.Xformable(cube.GetPrim())
|
||||||
|
segment = {
|
||||||
|
"translate": xform.AddTranslateOp(),
|
||||||
|
"rotate": xform.AddRotateXYZOp(),
|
||||||
|
"scale": xform.AddScaleOp(),
|
||||||
|
"width": float(width),
|
||||||
|
}
|
||||||
|
segment["translate"].Set(Gf.Vec3d(0.0, 0.0, 0.0))
|
||||||
|
segment["rotate"].Set(Gf.Vec3f(0.0, 0.0, 0.0))
|
||||||
|
segment["scale"].Set(Gf.Vec3f(0.001, float(width), float(width)))
|
||||||
|
bind_material(cube.GetPrim(), material)
|
||||||
|
return segment
|
||||||
|
|
||||||
|
def _create_dynamic_segments(self):
|
||||||
|
for lidar in self.lidar_specs:
|
||||||
|
for target in self.TARGET_BALLS:
|
||||||
|
segment = self._make_segment(
|
||||||
|
f"{self.root_path}/ObservationLines/{lidar['name']}_to_{target['name']}",
|
||||||
|
0.014,
|
||||||
|
self.materials["beam"],
|
||||||
|
)
|
||||||
|
self.observation_segments.append((segment, lidar, target))
|
||||||
|
|
||||||
|
for name in ("Shaft", "ArrowLeft", "ArrowRight"):
|
||||||
|
self.heading_segments.append(self._make_segment(
|
||||||
|
f"{self.root_path}/PoseOverlay/ExternalPoseHeading{name}",
|
||||||
|
0.045,
|
||||||
|
self.materials["pose"],
|
||||||
|
))
|
||||||
|
for index in range(32):
|
||||||
|
self.uncertainty_segments.append(self._make_segment(
|
||||||
|
f"{self.root_path}/PoseOverlay/ExternalPoseUncertainty_{index:02d}",
|
||||||
|
0.025,
|
||||||
|
self.materials["uncertainty"],
|
||||||
|
))
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _rotate_local_by_yaw(local_pos, yaw_rad):
|
||||||
|
cos_yaw = math.cos(yaw_rad)
|
||||||
|
sin_yaw = math.sin(yaw_rad)
|
||||||
|
return np.array([
|
||||||
|
cos_yaw * local_pos[0] - sin_yaw * local_pos[1],
|
||||||
|
sin_yaw * local_pos[0] + cos_yaw * local_pos[1],
|
||||||
|
local_pos[2],
|
||||||
|
], dtype=float)
|
||||||
|
|
||||||
|
def _target_world_position(self, vehicle_position, yaw_rad, target):
|
||||||
|
return np.array(vehicle_position, dtype=float) + self._rotate_local_by_yaw(target["pos"], yaw_rad)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _update_segment(segment, start, end):
|
||||||
|
start = np.array(start, dtype=float)
|
||||||
|
end = np.array(end, dtype=float)
|
||||||
|
delta = end - start
|
||||||
|
length = float(np.linalg.norm(delta))
|
||||||
|
width = float(segment["width"])
|
||||||
|
if length < 1e-4:
|
||||||
|
midpoint = start
|
||||||
|
yaw_deg = 0.0
|
||||||
|
pitch_deg = 0.0
|
||||||
|
length = 1e-4
|
||||||
|
else:
|
||||||
|
midpoint = (start + end) * 0.5
|
||||||
|
horizontal = math.hypot(float(delta[0]), float(delta[1]))
|
||||||
|
yaw_deg = math.degrees(math.atan2(float(delta[1]), float(delta[0])))
|
||||||
|
pitch_deg = math.degrees(math.atan2(float(delta[2]), horizontal))
|
||||||
|
|
||||||
|
segment["translate"].Set(Gf.Vec3d(float(midpoint[0]), float(midpoint[1]), float(midpoint[2])))
|
||||||
|
segment["rotate"].Set(Gf.Vec3f(0.0, float(-pitch_deg), float(yaw_deg)))
|
||||||
|
segment["scale"].Set(Gf.Vec3f(float(length), width, width))
|
||||||
|
|
||||||
|
def update(self, agv):
|
||||||
|
if not self.enabled or agv is None:
|
||||||
|
return
|
||||||
|
|
||||||
|
now = time.time()
|
||||||
|
if now - self.last_update_wall_time < 0.02:
|
||||||
|
return
|
||||||
|
self.last_update_wall_time = now
|
||||||
|
|
||||||
|
vehicle_position, quat = agv.get_world_pose()
|
||||||
|
yaw_rad = quat_wxyz_to_yaw(quat)
|
||||||
|
target_positions = {
|
||||||
|
target["name"]: self._target_world_position(vehicle_position, yaw_rad, target)
|
||||||
|
for target in self.TARGET_BALLS
|
||||||
|
}
|
||||||
|
|
||||||
|
for segment, lidar, target in self.observation_segments:
|
||||||
|
self._update_segment(segment, np.array(lidar["pos"], dtype=float), target_positions[target["name"]])
|
||||||
|
|
||||||
|
pose_anchor = np.array(vehicle_position, dtype=float) + np.array([0.0, 0.0, 1.55])
|
||||||
|
heading_length = 0.80
|
||||||
|
heading = np.array([math.cos(yaw_rad), math.sin(yaw_rad), 0.0], dtype=float)
|
||||||
|
side = np.array([-math.sin(yaw_rad), math.cos(yaw_rad), 0.0], dtype=float)
|
||||||
|
tip = pose_anchor + heading * heading_length
|
||||||
|
self._update_segment(self.heading_segments[0], pose_anchor, tip)
|
||||||
|
self._update_segment(self.heading_segments[1], tip, tip - heading * 0.22 + side * 0.13)
|
||||||
|
self._update_segment(self.heading_segments[2], tip, tip - heading * 0.22 - side * 0.13)
|
||||||
|
|
||||||
|
radius = max(0.18, min(0.65, self.args.external_position_stddev_m * 18.0))
|
||||||
|
ring_center = np.array(vehicle_position, dtype=float) + np.array([0.0, 0.0, 0.045])
|
||||||
|
ring_points = []
|
||||||
|
for index in range(len(self.uncertainty_segments) + 1):
|
||||||
|
angle = 2.0 * math.pi * index / len(self.uncertainty_segments)
|
||||||
|
ring_points.append(ring_center + np.array([math.cos(angle) * radius, math.sin(angle) * radius, 0.0]))
|
||||||
|
for index, segment in enumerate(self.uncertainty_segments):
|
||||||
|
self._update_segment(segment, ring_points[index], ring_points[index + 1])
|
||||||
|
|
||||||
|
|
||||||
class ExternalTruthTelemetryPublisher:
|
class ExternalTruthTelemetryPublisher:
|
||||||
def __init__(self, args):
|
def __init__(self, args):
|
||||||
@@ -1068,8 +1293,6 @@ class ControlCalibrationTelemetryPublisher:
|
|||||||
self.enabled = not args.disable_control_telemetry
|
self.enabled = not args.disable_control_telemetry
|
||||||
self.topic = args.control_telemetry_topic
|
self.topic = args.control_telemetry_topic
|
||||||
self.publish_period_sec = 0.0 if args.control_telemetry_hz <= 0.0 else 1.0 / args.control_telemetry_hz
|
self.publish_period_sec = 0.0 if args.control_telemetry_hz <= 0.0 else 1.0 / args.control_telemetry_hz
|
||||||
self.reference_speed_ms = args.control_reference_speed_ms
|
|
||||||
self.reference_y_m = args.control_reference_y_m
|
|
||||||
self.parameter_version = args.control_parameter_version
|
self.parameter_version = args.control_parameter_version
|
||||||
self.last_publish_wall_time = 0.0
|
self.last_publish_wall_time = 0.0
|
||||||
self.node = None
|
self.node = None
|
||||||
@@ -1102,12 +1325,13 @@ class ControlCalibrationTelemetryPublisher:
|
|||||||
position, yaw_rad, linear_velocity, angular_velocity = vehicle_state_from_isaac(agv)
|
position, yaw_rad, linear_velocity, angular_velocity = vehicle_state_from_isaac(agv)
|
||||||
forward_axis = np.array([math.cos(yaw_rad), math.sin(yaw_rad)])
|
forward_axis = np.array([math.cos(yaw_rad), math.sin(yaw_rad)])
|
||||||
forward_velocity = float(np.dot(np.array([linear_velocity[0], linear_velocity[1]]), forward_axis))
|
forward_velocity = float(np.dot(np.array([linear_velocity[0], linear_velocity[1]]), forward_axis))
|
||||||
lateral_error = float(position[1] - self.reference_y_m)
|
# Isaac 不再持有标定参考路径;路径误差由标定任务按所选 YAML 路径计算。
|
||||||
heading_error = float(normalize_angle(yaw_rad))
|
lateral_error = 0.0
|
||||||
speed_error = float(forward_velocity - self.reference_speed_ms)
|
heading_error = 0.0
|
||||||
steering_output = float(clamp(-0.8 * lateral_error - 1.2 * heading_error, -1.0, 1.0))
|
speed_error = 0.0
|
||||||
throttle_output = float(clamp(-2.0 * speed_error, 0.0, 1.0))
|
steering_output = 0.0
|
||||||
brake_output = float(clamp(2.0 * speed_error, 0.0, 1.0))
|
throttle_output = 0.0
|
||||||
|
brake_output = 0.0
|
||||||
|
|
||||||
msg = ControlTelemetry()
|
msg = ControlTelemetry()
|
||||||
msg.hardware_timestamp_us = time.time_ns() // 1000
|
msg.hardware_timestamp_us = time.time_ns() // 1000
|
||||||
@@ -1274,6 +1498,87 @@ class AckermannUrdfJointDriver:
|
|||||||
self.warned = True
|
self.warned = True
|
||||||
|
|
||||||
|
|
||||||
|
class VehicleCmdVelSubscriber:
|
||||||
|
"""Direct ROS subscriber used by Isaac's Python loop to avoid OmniGraph command lag."""
|
||||||
|
|
||||||
|
def __init__(self, args):
|
||||||
|
self.topic = args.cmd_vel_topic
|
||||||
|
self.node = None
|
||||||
|
self.latest_linear_x = 0.0
|
||||||
|
self.latest_angular_z = 0.0
|
||||||
|
self.last_message_wall_time = 0.0
|
||||||
|
|
||||||
|
if rclpy is None or Twist is None:
|
||||||
|
print("[WARN] 未找到 rclpy 或 geometry_msgs/Twist,Isaac 直接 cmd_vel 订阅已禁用。")
|
||||||
|
return
|
||||||
|
|
||||||
|
if not rclpy.ok():
|
||||||
|
rclpy.init(args=None)
|
||||||
|
|
||||||
|
self.node = rclpy.create_node("isaac_vehicle_cmd_vel_subscriber")
|
||||||
|
self.node.create_subscription(Twist, self.topic, self.on_cmd_vel, 10)
|
||||||
|
print(f"[*] Isaac 直接订阅车辆 cmd_vel: topic={self.topic}")
|
||||||
|
|
||||||
|
def on_cmd_vel(self, msg):
|
||||||
|
self.latest_linear_x = float(msg.linear.x)
|
||||||
|
self.latest_angular_z = float(msg.angular.z)
|
||||||
|
self.last_message_wall_time = time.time()
|
||||||
|
|
||||||
|
def spin_once(self):
|
||||||
|
if self.node is None or not rclpy.ok():
|
||||||
|
return
|
||||||
|
rclpy.spin_once(self.node, timeout_sec=0.0)
|
||||||
|
|
||||||
|
def command(self, stale_timeout_sec=0.5):
|
||||||
|
if self.node is None or self.last_message_wall_time <= 0.0:
|
||||||
|
return None
|
||||||
|
age_sec = time.time() - self.last_message_wall_time
|
||||||
|
if age_sec > stale_timeout_sec:
|
||||||
|
return 0.0, 0.0
|
||||||
|
return self.latest_linear_x, self.latest_angular_z
|
||||||
|
|
||||||
|
def shutdown(self):
|
||||||
|
if self.node is not None:
|
||||||
|
self.node.destroy_node()
|
||||||
|
self.node = None
|
||||||
|
|
||||||
|
|
||||||
|
class KinematicCommandFollower:
|
||||||
|
def __init__(self, enabled=True):
|
||||||
|
self.enabled = enabled
|
||||||
|
self.last_wall_time = time.time()
|
||||||
|
|
||||||
|
def apply(self, agv, linear_x, angular_z, ackermann_joint_driver):
|
||||||
|
now = time.time()
|
||||||
|
dt = max(0.0, min(now - self.last_wall_time, 0.1))
|
||||||
|
self.last_wall_time = now
|
||||||
|
|
||||||
|
position, quat = agv.get_world_pose()
|
||||||
|
yaw = quat_wxyz_to_yaw(quat)
|
||||||
|
current_linear_velocity = agv.get_linear_velocity()
|
||||||
|
world_linear_velocity = np.array([
|
||||||
|
linear_x * math.cos(yaw),
|
||||||
|
linear_x * math.sin(yaw),
|
||||||
|
current_linear_velocity[2],
|
||||||
|
])
|
||||||
|
agv.set_linear_velocity(world_linear_velocity)
|
||||||
|
agv.set_angular_velocity(np.array([0.0, 0.0, angular_z]))
|
||||||
|
ackermann_joint_driver.apply(agv, linear_x, angular_z)
|
||||||
|
|
||||||
|
if not self.enabled or dt <= 0.0:
|
||||||
|
return
|
||||||
|
if abs(linear_x) <= 1e-4 and abs(angular_z) <= 1e-4:
|
||||||
|
return
|
||||||
|
|
||||||
|
next_yaw = normalize_angle(yaw + angular_z * dt)
|
||||||
|
travel_yaw = yaw + 0.5 * angular_z * dt
|
||||||
|
next_position = np.array(position, dtype=float)
|
||||||
|
next_position[0] += linear_x * math.cos(travel_yaw) * dt
|
||||||
|
next_position[1] += linear_x * math.sin(travel_yaw) * dt
|
||||||
|
next_quat = euler_angles_to_quat(np.array([0.0, 0.0, next_yaw]), degrees=False)
|
||||||
|
agv.set_world_pose(position=next_position, orientation=np.array(next_quat))
|
||||||
|
|
||||||
|
|
||||||
class IsaacWorkshopRuntime:
|
class IsaacWorkshopRuntime:
|
||||||
def __init__(self, args):
|
def __init__(self, args):
|
||||||
self.args = args
|
self.args = args
|
||||||
@@ -1304,6 +1609,7 @@ class IsaacWorkshopRuntime:
|
|||||||
self.calibration_board_specs = []
|
self.calibration_board_specs = []
|
||||||
self.lidar_2d_target_specs = []
|
self.lidar_2d_target_specs = []
|
||||||
self.down_camera_target_specs = []
|
self.down_camera_target_specs = []
|
||||||
|
self.external_truth_visualization = None
|
||||||
self.scene_manifest_path = (
|
self.scene_manifest_path = (
|
||||||
Path(args.scene_manifest_path).expanduser().resolve()
|
Path(args.scene_manifest_path).expanduser().resolve()
|
||||||
if args.scene_manifest_path
|
if args.scene_manifest_path
|
||||||
@@ -1390,18 +1696,11 @@ class IsaacWorkshopRuntime:
|
|||||||
},
|
},
|
||||||
"chassis_calibration": {
|
"chassis_calibration": {
|
||||||
"chassis_type": self.args.chassis_type,
|
"chassis_type": self.args.chassis_type,
|
||||||
"straight_track_length_m": self.args.straight_track_length,
|
|
||||||
"straight_track_width_m": self.args.straight_track_width,
|
|
||||||
"arc_track_radius_m": self.args.arc_track_radius,
|
|
||||||
"wheel_radius_m": self.args.wheel_radius,
|
"wheel_radius_m": self.args.wheel_radius,
|
||||||
"wheel_track_m": self.args.wheel_track,
|
"wheel_track_m": self.args.wheel_track,
|
||||||
"wheel_base_m": self.args.wheel_base,
|
"wheel_base_m": self.args.wheel_base,
|
||||||
},
|
},
|
||||||
"control_calibration": {
|
"control_calibration": {
|
||||||
"reference_path": [
|
|
||||||
{"x_m": -self.args.straight_track_length / 2.0, "y_m": self.args.control_reference_y_m, "yaw_rad": 0.0, "target_speed_ms": self.args.control_reference_speed_ms},
|
|
||||||
{"x_m": self.args.straight_track_length / 2.0, "y_m": self.args.control_reference_y_m, "yaw_rad": 0.0, "target_speed_ms": self.args.control_reference_speed_ms},
|
|
||||||
],
|
|
||||||
"parameter_version": self.args.control_parameter_version,
|
"parameter_version": self.args.control_parameter_version,
|
||||||
},
|
},
|
||||||
"external_truth": {
|
"external_truth": {
|
||||||
@@ -1411,6 +1710,23 @@ class IsaacWorkshopRuntime:
|
|||||||
"expected_position_stddev_m": self.args.external_position_stddev_m,
|
"expected_position_stddev_m": self.args.external_position_stddev_m,
|
||||||
"expected_yaw_stddev_rad": self.args.external_yaw_stddev_rad,
|
"expected_yaw_stddev_rad": self.args.external_yaw_stddev_rad,
|
||||||
"expected_time_sync_offset_ms": self.args.external_time_sync_offset_ms,
|
"expected_time_sync_offset_ms": self.args.external_time_sync_offset_ms,
|
||||||
|
"visualization_enabled": (
|
||||||
|
self.args.enable_external_truth_visualization
|
||||||
|
and not self.args.disable_external_truth_visualization
|
||||||
|
),
|
||||||
|
"visualization_root_prim": "/World/ExternalTruthVisualization",
|
||||||
|
"target_ball_radius_m": self.args.external_target_ball_radius,
|
||||||
|
"target_balls": [
|
||||||
|
{
|
||||||
|
"ball_id": target["name"],
|
||||||
|
"center_in_base_link": {
|
||||||
|
"x_m": float(target["pos"][0]),
|
||||||
|
"y_m": float(target["pos"][1]),
|
||||||
|
"z_m": float(target["pos"][2]),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for target in ExternalTruthSceneVisualization.TARGET_BALLS
|
||||||
|
],
|
||||||
},
|
},
|
||||||
"sensors": [
|
"sensors": [
|
||||||
{
|
{
|
||||||
@@ -1756,6 +2072,14 @@ class IsaacWorkshopRuntime:
|
|||||||
{keys.CREATE_NODES: nodes, keys.CONNECT: connections, keys.SET_VALUES: set_values},
|
{keys.CREATE_NODES: nodes, keys.CONNECT: connections, keys.SET_VALUES: set_values},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def add_external_truth_visualization(self, world):
|
||||||
|
vehicle_mount_parent_path = self.get_vehicle_sensor_mount_parent_path()
|
||||||
|
self.external_truth_visualization = ExternalTruthSceneVisualization(
|
||||||
|
self.args,
|
||||||
|
world,
|
||||||
|
vehicle_mount_parent_path,
|
||||||
|
)
|
||||||
|
|
||||||
def build_workshop(self):
|
def build_workshop(self):
|
||||||
world = World(stage_units_in_meters=1.0)
|
world = World(stage_units_in_meters=1.0)
|
||||||
room_length = self.args.room_length
|
room_length = self.args.room_length
|
||||||
@@ -1868,9 +2192,6 @@ class IsaacWorkshopRuntime:
|
|||||||
self.lidar_2d_target_specs = add_2d_lidar_calibration_targets(world, self.args)
|
self.lidar_2d_target_specs = add_2d_lidar_calibration_targets(world, self.args)
|
||||||
print(f"[*] 已创建 {len(self.lidar_2d_target_specs)} 个 2D LiDAR 角落外参标定靶标。")
|
print(f"[*] 已创建 {len(self.lidar_2d_target_specs)} 个 2D LiDAR 角落外参标定靶标。")
|
||||||
|
|
||||||
if not self.args.disable_calibration_fixtures:
|
|
||||||
add_calibration_floor_fixtures(world, self.args)
|
|
||||||
|
|
||||||
if self.args.disable_down_camera_intrinsic_target:
|
if self.args.disable_down_camera_intrinsic_target:
|
||||||
print("[*] 已禁用下视相机 3D ChArUco 内参标定台。")
|
print("[*] 已禁用下视相机 3D ChArUco 内参标定台。")
|
||||||
else:
|
else:
|
||||||
@@ -1918,6 +2239,7 @@ class IsaacWorkshopRuntime:
|
|||||||
)
|
)
|
||||||
|
|
||||||
self.import_vehicle_to_stage(world)
|
self.import_vehicle_to_stage(world)
|
||||||
|
self.add_external_truth_visualization(world)
|
||||||
self.hide_sensor_placeholder_visuals()
|
self.hide_sensor_placeholder_visuals()
|
||||||
self.add_vehicle_sensor_publishers()
|
self.add_vehicle_sensor_publishers()
|
||||||
|
|
||||||
@@ -1968,7 +2290,12 @@ def main():
|
|||||||
print(f" - 2D LiDAR topic: {runtime.vehicle_2d_lidar_topic}")
|
print(f" - 2D LiDAR topic: {runtime.vehicle_2d_lidar_topic}")
|
||||||
print(f" - IMU topic: {runtime.vehicle_imu_topic}")
|
print(f" - IMU topic: {runtime.vehicle_imu_topic}")
|
||||||
print(f" - /cmd_vel topic: {runtime.cmd_vel_topic}")
|
print(f" - /cmd_vel topic: {runtime.cmd_vel_topic}")
|
||||||
|
print(f" - kinematic command follow: enabled={not ARGS.disable_kinematic_command_follow}")
|
||||||
print(f" - external telemetry topic: {ARGS.external_telemetry_topic}")
|
print(f" - external telemetry topic: {ARGS.external_telemetry_topic}")
|
||||||
|
print(
|
||||||
|
" - external truth visualization: "
|
||||||
|
f"enabled={ARGS.enable_external_truth_visualization and not ARGS.disable_external_truth_visualization}"
|
||||||
|
)
|
||||||
print(f" - chassis telemetry topic: {ARGS.chassis_telemetry_topic}")
|
print(f" - chassis telemetry topic: {ARGS.chassis_telemetry_topic}")
|
||||||
print(f" - vehicle TF: enabled={not ARGS.disable_vehicle_tf}, hz={ARGS.vehicle_tf_publish_hz}")
|
print(f" - vehicle TF: enabled={not ARGS.disable_vehicle_tf}, hz={ARGS.vehicle_tf_publish_hz}")
|
||||||
print(f" - control telemetry topic: {ARGS.control_telemetry_topic}")
|
print(f" - control telemetry topic: {ARGS.control_telemetry_topic}")
|
||||||
@@ -1979,6 +2306,8 @@ def main():
|
|||||||
agv = runtime.vehicle or world.scene.get_object("agv_vehicle")
|
agv = runtime.vehicle or world.scene.get_object("agv_vehicle")
|
||||||
ackermann_joint_driver = AckermannUrdfJointDriver(ARGS)
|
ackermann_joint_driver = AckermannUrdfJointDriver(ARGS)
|
||||||
ackermann_joint_driver.configure(agv)
|
ackermann_joint_driver.configure(agv)
|
||||||
|
cmd_vel_subscriber = VehicleCmdVelSubscriber(ARGS)
|
||||||
|
command_follower = KinematicCommandFollower(enabled=not ARGS.disable_kinematic_command_follow)
|
||||||
render_sensor_outputs = (
|
render_sensor_outputs = (
|
||||||
not ARGS.disable_camera
|
not ARGS.disable_camera
|
||||||
or not ARGS.disable_lidars
|
or not ARGS.disable_lidars
|
||||||
@@ -2003,7 +2332,9 @@ def main():
|
|||||||
(control_telemetry_publisher, (agv,)),
|
(control_telemetry_publisher, (agv,)),
|
||||||
(sensor_telemetry_publisher, ()),
|
(sensor_telemetry_publisher, ()),
|
||||||
)
|
)
|
||||||
ros_context_required = any(getattr(publisher, "node", None) is not None for publisher, _ in ros_publishers)
|
ros_context_required = any(getattr(publisher, "node", None) is not None for publisher, _ in ros_publishers) or (
|
||||||
|
cmd_vel_subscriber.node is not None
|
||||||
|
)
|
||||||
try:
|
try:
|
||||||
while True:
|
while True:
|
||||||
if SHUTDOWN_REQUESTED:
|
if SHUTDOWN_REQUESTED:
|
||||||
@@ -2017,29 +2348,29 @@ def main():
|
|||||||
exit_reason = "simulation_app.is_running() returned False"
|
exit_reason = "simulation_app.is_running() returned False"
|
||||||
break
|
break
|
||||||
try:
|
try:
|
||||||
lin_vel = og.Controller.get(og.Controller.attribute("/World/ROS2_Twist_Graph/TwistSub.outputs:linearVelocity"))
|
cmd_vel_subscriber.spin_once()
|
||||||
ang_vel = og.Controller.get(og.Controller.attribute("/World/ROS2_Twist_Graph/TwistSub.outputs:angularVelocity"))
|
command = cmd_vel_subscriber.command()
|
||||||
|
if command is None:
|
||||||
|
lin_vel = og.Controller.get(
|
||||||
|
og.Controller.attribute("/World/ROS2_Twist_Graph/TwistSub.outputs:linearVelocity")
|
||||||
|
)
|
||||||
|
ang_vel = og.Controller.get(
|
||||||
|
og.Controller.attribute("/World/ROS2_Twist_Graph/TwistSub.outputs:angularVelocity")
|
||||||
|
)
|
||||||
if lin_vel is not None and ang_vel is not None and len(lin_vel) == 3:
|
if lin_vel is not None and ang_vel is not None and len(lin_vel) == 3:
|
||||||
_, quat = agv.get_world_pose()
|
command = float(lin_vel[0]), float(ang_vel[2])
|
||||||
current_linear_velocity = agv.get_linear_velocity()
|
|
||||||
|
|
||||||
q = Gf.Quatd(float(quat[0]), float(quat[1]), float(quat[2]), float(quat[3]))
|
if command is not None:
|
||||||
rot_mat = Gf.Matrix3d(Gf.Rotation(q))
|
linear_x, angular_z = command
|
||||||
world_linear_velocity = Gf.Vec3d(*lin_vel) * rot_mat
|
command_follower.apply(agv, linear_x, angular_z, ackermann_joint_driver)
|
||||||
world_angular_velocity = Gf.Vec3d(*ang_vel) * rot_mat
|
if abs(linear_x) > 0.01 or abs(angular_z) > 0.01:
|
||||||
|
print(f"\r[ROS2 Debug] 车辆移动中: 线速度 {linear_x:.2f}, 角速度 {angular_z:.2f}", end="")
|
||||||
target_linear_velocity = np.array([world_linear_velocity[0], world_linear_velocity[1], current_linear_velocity[2]])
|
|
||||||
target_angular_velocity = np.array([0.0, 0.0, world_angular_velocity[2]])
|
|
||||||
agv.set_linear_velocity(target_linear_velocity)
|
|
||||||
agv.set_angular_velocity(target_angular_velocity)
|
|
||||||
ackermann_joint_driver.apply(agv, lin_vel[0], ang_vel[2])
|
|
||||||
|
|
||||||
if abs(lin_vel[0]) > 0.01 or abs(ang_vel[2]) > 0.01:
|
|
||||||
print(f"\r[ROS2 Debug] 车辆移动中: 线速度 {lin_vel[0]:.2f}, 角速度 {ang_vel[2]:.2f}", end="")
|
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
print(f"\n[ERROR] 运动学控制循环异常: {exc}")
|
print(f"\n[ERROR] 运动学控制循环异常: {exc}")
|
||||||
|
|
||||||
|
if runtime.external_truth_visualization is not None:
|
||||||
|
runtime.external_truth_visualization.update(agv)
|
||||||
|
|
||||||
world.step(render=render_frame)
|
world.step(render=render_frame)
|
||||||
try:
|
try:
|
||||||
for publisher, publish_args in ros_publishers:
|
for publisher, publish_args in ros_publishers:
|
||||||
@@ -2079,6 +2410,7 @@ def main():
|
|||||||
chassis_telemetry_publisher.shutdown()
|
chassis_telemetry_publisher.shutdown()
|
||||||
control_telemetry_publisher.shutdown()
|
control_telemetry_publisher.shutdown()
|
||||||
sensor_telemetry_publisher.shutdown()
|
sensor_telemetry_publisher.shutdown()
|
||||||
|
cmd_vel_subscriber.shutdown()
|
||||||
if rclpy is not None and rclpy.ok():
|
if rclpy is not None and rclpy.ok():
|
||||||
rclpy.shutdown()
|
rclpy.shutdown()
|
||||||
if SHUTDOWN_REQUESTED or ros_context_shutdown_seen:
|
if SHUTDOWN_REQUESTED or ros_context_shutdown_seen:
|
||||||
|
|||||||
@@ -360,93 +360,3 @@ def add_2d_lidar_calibration_targets(world, args):
|
|||||||
)
|
)
|
||||||
|
|
||||||
return specs
|
return specs
|
||||||
|
|
||||||
|
|
||||||
def add_floor_marker(world, prim_path, name, position, scale, color):
|
|
||||||
"""添加地面标记"""
|
|
||||||
world.scene.add(FixedCuboid(
|
|
||||||
prim_path=prim_path,
|
|
||||||
name=name,
|
|
||||||
position=np.array(position),
|
|
||||||
scale=np.array(scale),
|
|
||||||
color=np.array(color),
|
|
||||||
))
|
|
||||||
|
|
||||||
|
|
||||||
def add_calibration_floor_fixtures(world, args):
|
|
||||||
"""添加地面标定设施"""
|
|
||||||
z = 0.012
|
|
||||||
thickness = 0.012
|
|
||||||
length = args.straight_track_length
|
|
||||||
width = args.straight_track_width
|
|
||||||
|
|
||||||
add_floor_marker(
|
|
||||||
world,
|
|
||||||
"/World/Workshop/CalibrationFixtures/StraightTrack",
|
|
||||||
"straight_track",
|
|
||||||
[0.0, 0.0, z],
|
|
||||||
[length, width, thickness],
|
|
||||||
[0.08, 0.12, 0.10],
|
|
||||||
)
|
|
||||||
add_floor_marker(
|
|
||||||
world,
|
|
||||||
"/World/Workshop/CalibrationFixtures/StraightCenterLine",
|
|
||||||
"straight_center_line",
|
|
||||||
[0.0, 0.0, z + thickness],
|
|
||||||
[length, 0.035, thickness],
|
|
||||||
[0.95, 0.95, 0.15],
|
|
||||||
)
|
|
||||||
for side, y in (("Left", width / 2.0), ("Right", -width / 2.0)):
|
|
||||||
add_floor_marker(
|
|
||||||
world,
|
|
||||||
f"/World/Workshop/CalibrationFixtures/StraightBoundary{side}",
|
|
||||||
f"straight_boundary_{side.lower()}",
|
|
||||||
[0.0, y, z + thickness],
|
|
||||||
[length, 0.025, thickness],
|
|
||||||
[0.95, 0.78, 0.08],
|
|
||||||
)
|
|
||||||
|
|
||||||
stop_x = min(length / 2.0 - 0.35, args.room_length / 2.0 - 1.0)
|
|
||||||
add_floor_marker(
|
|
||||||
world,
|
|
||||||
"/World/Workshop/CalibrationFixtures/StopAccuracyTarget",
|
|
||||||
"stop_accuracy_target",
|
|
||||||
[stop_x, 0.0, z + 2.0 * thickness],
|
|
||||||
[0.6, 0.9, thickness],
|
|
||||||
[0.88, 0.10, 0.10],
|
|
||||||
)
|
|
||||||
|
|
||||||
lateral_x = -min(length / 2.0 - 0.8, args.room_length / 2.0 - 1.2)
|
|
||||||
add_floor_marker(
|
|
||||||
world,
|
|
||||||
"/World/Workshop/CalibrationFixtures/LateralMotionPad",
|
|
||||||
"lateral_motion_pad",
|
|
||||||
[lateral_x, 0.0, z + thickness],
|
|
||||||
[0.75, 2.0, thickness],
|
|
||||||
[0.10, 0.34, 0.88],
|
|
||||||
)
|
|
||||||
|
|
||||||
arc_center = np.array([0.0, -args.room_width * 0.20])
|
|
||||||
arc_radius = min(args.arc_track_radius, args.room_width * 0.32)
|
|
||||||
marker_count = 25
|
|
||||||
for index in range(marker_count):
|
|
||||||
theta = math.radians(20.0 + 140.0 * index / (marker_count - 1))
|
|
||||||
x = arc_center[0] + arc_radius * math.cos(theta)
|
|
||||||
y = arc_center[1] + arc_radius * math.sin(theta)
|
|
||||||
add_floor_marker(
|
|
||||||
world,
|
|
||||||
f"/World/Workshop/CalibrationFixtures/ArcMarker_{index:02d}",
|
|
||||||
f"arc_marker_{index:02d}",
|
|
||||||
[x, y, z + 3.0 * thickness],
|
|
||||||
[0.10, 0.10, thickness],
|
|
||||||
[0.12, 0.75, 0.35],
|
|
||||||
)
|
|
||||||
|
|
||||||
add_floor_marker(
|
|
||||||
world,
|
|
||||||
"/World/Workshop/CalibrationFixtures/SensorCapturePad",
|
|
||||||
"sensor_capture_pad",
|
|
||||||
[0.0, args.room_width * 0.28, z + thickness],
|
|
||||||
[1.2, 0.9, thickness],
|
|
||||||
[0.42, 0.18, 0.78],
|
|
||||||
)
|
|
||||||
|
|||||||
@@ -42,7 +42,9 @@ def chassis_type_value(name, chassis_type_cls=None):
|
|||||||
"ackermann": chassis_type_cls.ACKERMANN,
|
"ackermann": chassis_type_cls.ACKERMANN,
|
||||||
"differential": chassis_type_cls.DIFFERENTIAL,
|
"differential": chassis_type_cls.DIFFERENTIAL,
|
||||||
"single_steer": chassis_type_cls.SINGLE_STEER_WHEEL,
|
"single_steer": chassis_type_cls.SINGLE_STEER_WHEEL,
|
||||||
|
"single_steer_wheel": chassis_type_cls.SINGLE_STEER_WHEEL,
|
||||||
"multi_steer": chassis_type_cls.MULTI_STEER_WHEEL,
|
"multi_steer": chassis_type_cls.MULTI_STEER_WHEEL,
|
||||||
|
"multi_steer_wheel": chassis_type_cls.MULTI_STEER_WHEEL,
|
||||||
}
|
}
|
||||||
return mapping.get(name, chassis_type_cls.CHASSIS_TYPE_UNSPECIFIED)
|
return mapping.get(name, chassis_type_cls.CHASSIS_TYPE_UNSPECIFIED)
|
||||||
|
|
||||||
|
|||||||
@@ -205,6 +205,8 @@ def build_isaac_command(profile: dict, python_executable: str, headless: bool, e
|
|||||||
isaac = profile["isaac"]
|
isaac = profile["isaac"]
|
||||||
targets = profile.get("targets", {})
|
targets = profile.get("targets", {})
|
||||||
down_camera = targets.get("down_camera_charuco", {})
|
down_camera = targets.get("down_camera_charuco", {})
|
||||||
|
external_localization = profile.get("external_localization", {})
|
||||||
|
external_pose_bridge = profile.get("external_pose_bridge", {})
|
||||||
command = [
|
command = [
|
||||||
python_executable,
|
python_executable,
|
||||||
str(repo_path(isaac["scene_script"])),
|
str(repo_path(isaac["scene_script"])),
|
||||||
@@ -222,6 +224,20 @@ def build_isaac_command(profile: dict, python_executable: str, headless: bool, e
|
|||||||
str(get_nested(profile, "vehicle_sensor_agent.lidar_2d_topic", "/sensor/lidar_2d/scan")),
|
str(get_nested(profile, "vehicle_sensor_agent.lidar_2d_topic", "/sensor/lidar_2d/scan")),
|
||||||
"--vehicle-imu-topic",
|
"--vehicle-imu-topic",
|
||||||
str(get_nested(profile, "vehicle_sensor_agent.imu_topic", "/sensor/imu/data")),
|
str(get_nested(profile, "vehicle_sensor_agent.imu_topic", "/sensor/imu/data")),
|
||||||
|
"--external-telemetry-topic",
|
||||||
|
str(
|
||||||
|
external_localization.get("output_topic")
|
||||||
|
or external_pose_bridge.get("source_topic")
|
||||||
|
or get_nested(profile, "vehicle_agent.external_pose_topic", "/isaac/external_localization/vehicle/pose")
|
||||||
|
),
|
||||||
|
"--external-reference-source-name",
|
||||||
|
str(
|
||||||
|
external_localization.get("reference_source_name")
|
||||||
|
or external_pose_bridge.get("reference_source_name")
|
||||||
|
or "isaac_sim_truth_source"
|
||||||
|
),
|
||||||
|
"--external-workcell-zone-id",
|
||||||
|
str(external_localization.get("workcell_zone_id", "isaac_workcell_zone_a")),
|
||||||
"--scene-manifest-path",
|
"--scene-manifest-path",
|
||||||
str(repo_path(isaac["scene_manifest_path"])),
|
str(repo_path(isaac["scene_manifest_path"])),
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -625,7 +625,7 @@ def make_chassis_profile_tasks(args: argparse.Namespace) -> list[RequestedCalibr
|
|||||||
)
|
)
|
||||||
profile_path = Path(args.chassis_action_profile).expanduser().resolve(strict=False)
|
profile_path = Path(args.chassis_action_profile).expanduser().resolve(strict=False)
|
||||||
profile = tool.validate_profile(tool.load_yaml(profile_path))
|
profile = tool.validate_profile(tool.load_yaml(profile_path))
|
||||||
exported = tool.export_requested_tasks(profile, args.chassis_profile_type)
|
exported = tool.export_requested_tasks(profile, args.chassis_profile_type, profile_path)
|
||||||
return [
|
return [
|
||||||
make_requested_task_from_profile(
|
make_requested_task_from_profile(
|
||||||
raw_task,
|
raw_task,
|
||||||
@@ -643,7 +643,7 @@ def make_control_profile_tasks(args: argparse.Namespace) -> list[RequestedCalibr
|
|||||||
)
|
)
|
||||||
profile_path = Path(args.control_evaluation_profile).expanduser().resolve(strict=False)
|
profile_path = Path(args.control_evaluation_profile).expanduser().resolve(strict=False)
|
||||||
profile = tool.validate_profile(tool.load_yaml(profile_path))
|
profile = tool.validate_profile(tool.load_yaml(profile_path))
|
||||||
exported = tool.export_requested_tasks(profile, args.chassis_profile_type)
|
exported = tool.export_requested_tasks(profile, args.chassis_profile_type, profile_path)
|
||||||
return [
|
return [
|
||||||
make_requested_task_from_profile(
|
make_requested_task_from_profile(
|
||||||
raw_task,
|
raw_task,
|
||||||
@@ -661,7 +661,7 @@ def make_sensor_profile_tasks(task_name: str, args: argparse.Namespace) -> list[
|
|||||||
)
|
)
|
||||||
profile_path = Path(args.sensor_calibration_profile).expanduser().resolve(strict=False)
|
profile_path = Path(args.sensor_calibration_profile).expanduser().resolve(strict=False)
|
||||||
profile = tool.validate_profile(tool.load_yaml(profile_path))
|
profile = tool.validate_profile(tool.load_yaml(profile_path))
|
||||||
exported = tool.export_requested_tasks(profile, {task_name})
|
exported = tool.export_requested_tasks(profile, {task_name}, profile_path)
|
||||||
return [
|
return [
|
||||||
make_requested_task_from_profile(
|
make_requested_task_from_profile(
|
||||||
raw_task,
|
raw_task,
|
||||||
|
|||||||
@@ -56,6 +56,14 @@ src/site_deployment/workshop_chassis_calibration_real/config/chassis_action_prof
|
|||||||
|
|
||||||
这个 profile 固定了四类底盘在真实标定车间中建议执行的动作序列、速度、距离、超时和采集要求。现场部署时需要先按工位尺寸、安全速度、真实模块 ID 修改它,再用工具校验:
|
这个 profile 固定了四类底盘在真实标定车间中建议执行的动作序列、速度、距离、超时和采集要求。现场部署时需要先按工位尺寸、安全速度、真实模块 ID 修改它,再用工具校验:
|
||||||
|
|
||||||
|
底盘参考路径单独放在:
|
||||||
|
|
||||||
|
```text
|
||||||
|
src/site_deployment/workshop_chassis_calibration_real/reference_paths/
|
||||||
|
```
|
||||||
|
|
||||||
|
每条路径一个 YAML 文件,必须配置 `path_id` 和中文 `display_name`。`chassis_action_profile.yaml` 中每个动作通过 `reference_path_id` 选择要用的路径,导出任务时会自动展开为 `reference_path.*` metadata。
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
python3 src/site_deployment/workshop_chassis_calibration_real/chassis_action_profile.py \
|
python3 src/site_deployment/workshop_chassis_calibration_real/chassis_action_profile.py \
|
||||||
--profile /data/agv_calib/site_a/chassis_action_profile.yaml
|
--profile /data/agv_calib/site_a/chassis_action_profile.yaml
|
||||||
@@ -125,6 +133,73 @@ python3 src/site_deployment/workshop_chassis_calibration_real/run_chassis_profil
|
|||||||
- 命令话题不可用时,`command_source=auto` 会按动作 profile 写计划命令行。
|
- 命令话题不可用时,`command_source=auto` 会按动作 profile 写计划命令行。
|
||||||
- 全部动作结束后统一写 `dataset_index.yaml`。
|
- 全部动作结束后统一写 `dataset_index.yaml`。
|
||||||
|
|
||||||
|
## 车间电脑侧底盘遥测 WiFi/TCP bridge
|
||||||
|
|
||||||
|
如果真实车端通过 WiFi/TCP 把底盘遥测发到车间电脑,而不是直接发布 ROS2 topic,先启动桥接节点:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
source /opt/ros/humble/setup.bash
|
||||||
|
source install/setup.bash
|
||||||
|
|
||||||
|
python3 src/site_deployment/workshop_chassis_calibration_real/workshop_chassis_telemetry_bridge.py \
|
||||||
|
--bind-host 0.0.0.0 \
|
||||||
|
--bind-port 9010 \
|
||||||
|
--protocol frame \
|
||||||
|
--output-topic /chassis/telemetry \
|
||||||
|
--default-chassis-type ackermann
|
||||||
|
```
|
||||||
|
|
||||||
|
桥接节点接收 JSON payload,并发布 `calibration_chassis_interfaces/msg/ChassisTelemetry`。支持两种常用输入:
|
||||||
|
|
||||||
|
- `frame`:项目统一 TCP 帧,`<uint32 msg_type><uint32 payload_len><json payload>`,建议车端使用 `msg_type=7`。
|
||||||
|
- `json_lines`:每行一条 UTF-8 JSON。
|
||||||
|
|
||||||
|
payload 字段示例:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"hardware_timestamp_us": 1711234567000000,
|
||||||
|
"chassis_type": "ackermann",
|
||||||
|
"odom_x_m": 0.12,
|
||||||
|
"odom_y_m": 0.0,
|
||||||
|
"odom_yaw_rad": 0.0,
|
||||||
|
"linear_velocity_ms": 0.1,
|
||||||
|
"angular_velocity_rads": 0.0,
|
||||||
|
"estop_engaged": false,
|
||||||
|
"driver_error_code": 0,
|
||||||
|
"active_job_id": "chassis_req_001",
|
||||||
|
"modules": [
|
||||||
|
{
|
||||||
|
"module_id": "rear_left",
|
||||||
|
"encoder_ticks": 12345,
|
||||||
|
"wheel_speed_rpm": 18.0,
|
||||||
|
"steer_angle_deg": 0.0,
|
||||||
|
"motor_current_amp": 1.2
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
确认车间电脑侧已经收到并转成 ROS topic:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ros2 topic hz /chassis/telemetry
|
||||||
|
ros2 topic echo /chassis/telemetry --once
|
||||||
|
```
|
||||||
|
|
||||||
|
通过操作台 UI 启动“现场服务”时,如果现场配置里存在 `chassis_telemetry_bridge`,UI 会把监听地址、端口、协议和输出 topic 传给
|
||||||
|
`minimal_workshop_demo.launch.py`,并自动启动这个 bridge。也可以手动通过 launch 参数启用:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ros2 launch win_ubuntu_bridge minimal_workshop_demo.launch.py \
|
||||||
|
use_gateway:=true \
|
||||||
|
enable_chassis_telemetry_bridge:=true \
|
||||||
|
chassis_telemetry_bind_host:=0.0.0.0 \
|
||||||
|
chassis_telemetry_bind_port:=9010 \
|
||||||
|
chassis_telemetry_protocol:=frame \
|
||||||
|
chassis_telemetry_topic:=/chassis/telemetry
|
||||||
|
```
|
||||||
|
|
||||||
本地最小 smoke:
|
本地最小 smoke:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
|||||||
+2
@@ -176,6 +176,8 @@ class ChassisSessionCapture:
|
|||||||
|
|
||||||
def on_chassis_telemetry(self, msg: Any) -> None:
|
def on_chassis_telemetry(self, msg: Any) -> None:
|
||||||
chassis_type = normalize_chassis_type_name(getattr(getattr(msg, "chassis_type", None), "value", ""))
|
chassis_type = normalize_chassis_type_name(getattr(getattr(msg, "chassis_type", None), "value", ""))
|
||||||
|
if not chassis_type:
|
||||||
|
chassis_type = normalize_chassis_type_name(self.config.get("chassis_type", ""))
|
||||||
self.chassis_csv.write_row({
|
self.chassis_csv.write_row({
|
||||||
"hardware_timestamp_us": int_value(getattr(msg, "hardware_timestamp_us", 0), now_us()),
|
"hardware_timestamp_us": int_value(getattr(msg, "hardware_timestamp_us", 0), now_us()),
|
||||||
"chassis_type": chassis_type,
|
"chassis_type": chassis_type,
|
||||||
|
|||||||
+24
-2
@@ -13,6 +13,13 @@ try:
|
|||||||
except ImportError as exc:
|
except ImportError as exc:
|
||||||
raise SystemExit("缺少 PyYAML,请先在当前 Python 环境安装 yaml 模块。") from exc
|
raise SystemExit("缺少 PyYAML,请先在当前 Python 环境安装 yaml 模块。") from exc
|
||||||
|
|
||||||
|
SCRIPT_DIR = Path(__file__).resolve().parent
|
||||||
|
REFERENCE_PATH_TOOL_DIR = SCRIPT_DIR.parents[0] / "workshop_reference_paths"
|
||||||
|
if str(REFERENCE_PATH_TOOL_DIR) not in sys.path:
|
||||||
|
sys.path.insert(0, str(REFERENCE_PATH_TOOL_DIR))
|
||||||
|
|
||||||
|
from calibration_reference_paths import selected_path_metadata # noqa: E402
|
||||||
|
|
||||||
|
|
||||||
SUPPORTED_CHASSIS_TYPES = {
|
SUPPORTED_CHASSIS_TYPES = {
|
||||||
"ackermann",
|
"ackermann",
|
||||||
@@ -230,6 +237,8 @@ def validate_action(
|
|||||||
|
|
||||||
for key, value in metadata.items():
|
for key, value in metadata.items():
|
||||||
validate_metadata_value(key, value, task_code, max_linear_speed_ms)
|
validate_metadata_value(key, value, task_code, max_linear_speed_ms)
|
||||||
|
if action.get("reference_path_id") not in (None, ""):
|
||||||
|
require_string(action.get("reference_path_id"), f"{action_name}.reference_path_id")
|
||||||
|
|
||||||
|
|
||||||
def validate_profile(profile: dict[str, Any]) -> dict[str, Any]:
|
def validate_profile(profile: dict[str, Any]) -> dict[str, Any]:
|
||||||
@@ -281,12 +290,25 @@ def make_task_param(key: str, value: Any) -> dict[str, str]:
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def export_requested_tasks(profile: dict[str, Any], chassis_type: str) -> dict[str, Any]:
|
def export_requested_tasks(
|
||||||
|
profile: dict[str, Any],
|
||||||
|
chassis_type: str,
|
||||||
|
profile_path: Path | None = None,
|
||||||
|
) -> dict[str, Any]:
|
||||||
section = profile["chassis_profiles"][chassis_type]
|
section = profile["chassis_profiles"][chassis_type]
|
||||||
|
reference_path_dir = profile.get("reference_path_dir", "reference_paths")
|
||||||
tasks: list[dict[str, Any]] = []
|
tasks: list[dict[str, Any]] = []
|
||||||
for action in section["actions"]:
|
for action in section["actions"]:
|
||||||
metadata = dict(action["metadata"])
|
metadata = dict(action["metadata"])
|
||||||
metadata["primitive_type"] = action["primitive_type"]
|
metadata["primitive_type"] = action["primitive_type"]
|
||||||
|
reference_metadata, _ = selected_path_metadata(
|
||||||
|
reference_path_dir,
|
||||||
|
profile_path,
|
||||||
|
str(action.get("reference_path_id", "")),
|
||||||
|
chassis_type,
|
||||||
|
str(action["primitive_type"]),
|
||||||
|
)
|
||||||
|
metadata.update(reference_metadata)
|
||||||
task_params = [
|
task_params = [
|
||||||
make_task_param(key, metadata[key])
|
make_task_param(key, metadata[key])
|
||||||
for key in sorted(metadata)
|
for key in sorted(metadata)
|
||||||
@@ -341,7 +363,7 @@ def main() -> int:
|
|||||||
if args.format == "requested_tasks":
|
if args.format == "requested_tasks":
|
||||||
if not args.chassis_type:
|
if not args.chassis_type:
|
||||||
raise ValueError("--format requested_tasks 必须指定 --chassis-type。")
|
raise ValueError("--format requested_tasks 必须指定 --chassis-type。")
|
||||||
output = export_requested_tasks(profile, args.chassis_type)
|
output = export_requested_tasks(profile, args.chassis_type, profile_path)
|
||||||
else:
|
else:
|
||||||
output = build_summary(profile)
|
output = build_summary(profile)
|
||||||
|
|
||||||
|
|||||||
+22
@@ -0,0 +1,22 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""兼容入口:底盘参考路径已迁移到通用标定参考路径工具。"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
COMMON_TOOL_DIR = Path(__file__).resolve().parents[1] / "workshop_reference_paths"
|
||||||
|
if str(COMMON_TOOL_DIR) not in sys.path:
|
||||||
|
sys.path.insert(0, str(COMMON_TOOL_DIR))
|
||||||
|
|
||||||
|
from calibration_reference_paths import main # noqa: E402
|
||||||
|
|
||||||
|
|
||||||
|
DEFAULT_PATH_DIR = Path(__file__).resolve().parent / "reference_paths"
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
if "--path-dir" not in sys.argv:
|
||||||
|
sys.argv.extend(["--path-dir", str(DEFAULT_PATH_DIR)])
|
||||||
|
raise SystemExit(main())
|
||||||
+67
@@ -4,6 +4,7 @@
|
|||||||
|
|
||||||
schema_version: 1
|
schema_version: 1
|
||||||
profile_name: workshop_chassis_calibration_action_profile
|
profile_name: workshop_chassis_calibration_action_profile
|
||||||
|
reference_path_dir: ../reference_paths
|
||||||
|
|
||||||
safety:
|
safety:
|
||||||
max_linear_speed_ms: 0.1
|
max_linear_speed_ms: 0.1
|
||||||
@@ -33,6 +34,7 @@ chassis_profiles:
|
|||||||
- task_code: chassis.ackermann.straight_forward
|
- task_code: chassis.ackermann.straight_forward
|
||||||
display_name: 阿克曼直线前进
|
display_name: 阿克曼直线前进
|
||||||
primitive_type: straight_line
|
primitive_type: straight_line
|
||||||
|
reference_path_id: chassis_straight_forward_1m
|
||||||
metadata:
|
metadata:
|
||||||
straight_line.target_distance_m: 1.0
|
straight_line.target_distance_m: 1.0
|
||||||
straight_line.target_speed_ms: 0.1
|
straight_line.target_speed_ms: 0.1
|
||||||
@@ -42,6 +44,7 @@ chassis_profiles:
|
|||||||
- task_code: chassis.ackermann.straight_reverse
|
- task_code: chassis.ackermann.straight_reverse
|
||||||
display_name: 阿克曼直线倒车
|
display_name: 阿克曼直线倒车
|
||||||
primitive_type: straight_line
|
primitive_type: straight_line
|
||||||
|
reference_path_id: chassis_straight_reverse_0_6m
|
||||||
metadata:
|
metadata:
|
||||||
straight_line.target_distance_m: 0.6
|
straight_line.target_distance_m: 0.6
|
||||||
straight_line.target_speed_ms: 0.08
|
straight_line.target_speed_ms: 0.08
|
||||||
@@ -51,6 +54,7 @@ chassis_profiles:
|
|||||||
- task_code: chassis.ackermann.arc_left
|
- task_code: chassis.ackermann.arc_left
|
||||||
display_name: 阿克曼左圆弧
|
display_name: 阿克曼左圆弧
|
||||||
primitive_type: arc
|
primitive_type: arc
|
||||||
|
reference_path_id: chassis_arc_left_r1_45deg
|
||||||
metadata:
|
metadata:
|
||||||
arc.target_speed_ms: 0.08
|
arc.target_speed_ms: 0.08
|
||||||
arc.radius_m: 1.0
|
arc.radius_m: 1.0
|
||||||
@@ -61,6 +65,7 @@ chassis_profiles:
|
|||||||
- task_code: chassis.ackermann.arc_right
|
- task_code: chassis.ackermann.arc_right
|
||||||
display_name: 阿克曼右圆弧
|
display_name: 阿克曼右圆弧
|
||||||
primitive_type: arc
|
primitive_type: arc
|
||||||
|
reference_path_id: chassis_arc_right_r1_45deg
|
||||||
metadata:
|
metadata:
|
||||||
arc.target_speed_ms: 0.08
|
arc.target_speed_ms: 0.08
|
||||||
arc.radius_m: 1.0
|
arc.radius_m: 1.0
|
||||||
@@ -68,9 +73,54 @@ chassis_profiles:
|
|||||||
arc.clockwise: true
|
arc.clockwise: true
|
||||||
brake_when_finished: true
|
brake_when_finished: true
|
||||||
timeout_sec: 45.0
|
timeout_sec: 45.0
|
||||||
|
- task_code: chassis.ackermann.s_curve_left_entry
|
||||||
|
display_name: 阿克曼 S 形 1/4 左入弯
|
||||||
|
primitive_type: arc
|
||||||
|
reference_path_id: chassis_ackermann_s_curve_r1_5
|
||||||
|
metadata:
|
||||||
|
arc.target_speed_ms: 0.1
|
||||||
|
arc.radius_m: 1.5
|
||||||
|
arc.sweep_angle_deg: 25.0
|
||||||
|
arc.clockwise: false
|
||||||
|
brake_when_finished: false
|
||||||
|
timeout_sec: 30.0
|
||||||
|
- task_code: chassis.ackermann.s_curve_right_middle
|
||||||
|
display_name: 阿克曼 S 形 2/4 右转
|
||||||
|
primitive_type: arc
|
||||||
|
reference_path_id: chassis_ackermann_s_curve_r1_5
|
||||||
|
metadata:
|
||||||
|
arc.target_speed_ms: 0.1
|
||||||
|
arc.radius_m: 1.5
|
||||||
|
arc.sweep_angle_deg: 60.0
|
||||||
|
arc.clockwise: true
|
||||||
|
brake_when_finished: false
|
||||||
|
timeout_sec: 40.0
|
||||||
|
- task_code: chassis.ackermann.s_curve_left_middle
|
||||||
|
display_name: 阿克曼 S 形 3/4 左转
|
||||||
|
primitive_type: arc
|
||||||
|
reference_path_id: chassis_ackermann_s_curve_r1_5
|
||||||
|
metadata:
|
||||||
|
arc.target_speed_ms: 0.1
|
||||||
|
arc.radius_m: 1.5
|
||||||
|
arc.sweep_angle_deg: 60.0
|
||||||
|
arc.clockwise: false
|
||||||
|
brake_when_finished: false
|
||||||
|
timeout_sec: 40.0
|
||||||
|
- task_code: chassis.ackermann.s_curve_right_exit
|
||||||
|
display_name: 阿克曼 S 形 4/4 右出弯
|
||||||
|
primitive_type: arc
|
||||||
|
reference_path_id: chassis_ackermann_s_curve_r1_5
|
||||||
|
metadata:
|
||||||
|
arc.target_speed_ms: 0.1
|
||||||
|
arc.radius_m: 1.5
|
||||||
|
arc.sweep_angle_deg: 25.0
|
||||||
|
arc.clockwise: true
|
||||||
|
brake_when_finished: true
|
||||||
|
timeout_sec: 30.0
|
||||||
- task_code: chassis.ackermann.steering_sweep
|
- task_code: chassis.ackermann.steering_sweep
|
||||||
display_name: 阿克曼舵角扫动
|
display_name: 阿克曼舵角扫动
|
||||||
primitive_type: steering_sweep
|
primitive_type: steering_sweep
|
||||||
|
reference_path_id: chassis_static_station
|
||||||
metadata:
|
metadata:
|
||||||
steering_sweep.target_angle_deg: 0.0
|
steering_sweep.target_angle_deg: 0.0
|
||||||
steering_sweep.sweep_amplitude_deg: 8.0
|
steering_sweep.sweep_amplitude_deg: 8.0
|
||||||
@@ -89,6 +139,7 @@ chassis_profiles:
|
|||||||
- task_code: chassis.differential.straight_forward
|
- task_code: chassis.differential.straight_forward
|
||||||
display_name: 差速直线前进
|
display_name: 差速直线前进
|
||||||
primitive_type: straight_line
|
primitive_type: straight_line
|
||||||
|
reference_path_id: chassis_straight_forward_1m
|
||||||
metadata:
|
metadata:
|
||||||
straight_line.target_distance_m: 1.0
|
straight_line.target_distance_m: 1.0
|
||||||
straight_line.target_speed_ms: 0.1
|
straight_line.target_speed_ms: 0.1
|
||||||
@@ -98,6 +149,7 @@ chassis_profiles:
|
|||||||
- task_code: chassis.differential.straight_reverse
|
- task_code: chassis.differential.straight_reverse
|
||||||
display_name: 差速直线倒车
|
display_name: 差速直线倒车
|
||||||
primitive_type: straight_line
|
primitive_type: straight_line
|
||||||
|
reference_path_id: chassis_straight_reverse_0_6m
|
||||||
metadata:
|
metadata:
|
||||||
straight_line.target_distance_m: 0.6
|
straight_line.target_distance_m: 0.6
|
||||||
straight_line.target_speed_ms: 0.08
|
straight_line.target_speed_ms: 0.08
|
||||||
@@ -107,6 +159,7 @@ chassis_profiles:
|
|||||||
- task_code: chassis.differential.rotate_left
|
- task_code: chassis.differential.rotate_left
|
||||||
display_name: 差速左原地旋转
|
display_name: 差速左原地旋转
|
||||||
primitive_type: in_place_rotation
|
primitive_type: in_place_rotation
|
||||||
|
reference_path_id: chassis_rotate_left_90deg
|
||||||
metadata:
|
metadata:
|
||||||
in_place_rotation.target_yaw_deg: 90.0
|
in_place_rotation.target_yaw_deg: 90.0
|
||||||
in_place_rotation.target_angular_vel_deg_s: 10.0
|
in_place_rotation.target_angular_vel_deg_s: 10.0
|
||||||
@@ -115,6 +168,7 @@ chassis_profiles:
|
|||||||
- task_code: chassis.differential.rotate_right
|
- task_code: chassis.differential.rotate_right
|
||||||
display_name: 差速右原地旋转
|
display_name: 差速右原地旋转
|
||||||
primitive_type: in_place_rotation
|
primitive_type: in_place_rotation
|
||||||
|
reference_path_id: chassis_rotate_right_90deg
|
||||||
metadata:
|
metadata:
|
||||||
in_place_rotation.target_yaw_deg: -90.0
|
in_place_rotation.target_yaw_deg: -90.0
|
||||||
in_place_rotation.target_angular_vel_deg_s: 10.0
|
in_place_rotation.target_angular_vel_deg_s: 10.0
|
||||||
@@ -123,6 +177,7 @@ chassis_profiles:
|
|||||||
- task_code: chassis.differential.arc_left
|
- task_code: chassis.differential.arc_left
|
||||||
display_name: 差速左转圆弧
|
display_name: 差速左转圆弧
|
||||||
primitive_type: arc
|
primitive_type: arc
|
||||||
|
reference_path_id: chassis_arc_left_r1_45deg
|
||||||
metadata:
|
metadata:
|
||||||
arc.target_speed_ms: 0.08
|
arc.target_speed_ms: 0.08
|
||||||
arc.radius_m: 1.0
|
arc.radius_m: 1.0
|
||||||
@@ -133,6 +188,7 @@ chassis_profiles:
|
|||||||
- task_code: chassis.differential.arc_right
|
- task_code: chassis.differential.arc_right
|
||||||
display_name: 差速右转圆弧
|
display_name: 差速右转圆弧
|
||||||
primitive_type: arc
|
primitive_type: arc
|
||||||
|
reference_path_id: chassis_arc_right_r1_45deg
|
||||||
metadata:
|
metadata:
|
||||||
arc.target_speed_ms: 0.08
|
arc.target_speed_ms: 0.08
|
||||||
arc.radius_m: 1.0
|
arc.radius_m: 1.0
|
||||||
@@ -152,6 +208,7 @@ chassis_profiles:
|
|||||||
- task_code: chassis.single_steer.straight_forward
|
- task_code: chassis.single_steer.straight_forward
|
||||||
display_name: 单舵轮直线前进
|
display_name: 单舵轮直线前进
|
||||||
primitive_type: straight_line
|
primitive_type: straight_line
|
||||||
|
reference_path_id: chassis_straight_forward_1m
|
||||||
metadata:
|
metadata:
|
||||||
straight_line.target_distance_m: 1.0
|
straight_line.target_distance_m: 1.0
|
||||||
straight_line.target_speed_ms: 0.1
|
straight_line.target_speed_ms: 0.1
|
||||||
@@ -161,6 +218,7 @@ chassis_profiles:
|
|||||||
- task_code: chassis.single_steer.steering_sweep
|
- task_code: chassis.single_steer.steering_sweep
|
||||||
display_name: 单舵轮舵角扫动
|
display_name: 单舵轮舵角扫动
|
||||||
primitive_type: steering_sweep
|
primitive_type: steering_sweep
|
||||||
|
reference_path_id: chassis_static_station
|
||||||
metadata:
|
metadata:
|
||||||
steering_sweep.target_angle_deg: 0.0
|
steering_sweep.target_angle_deg: 0.0
|
||||||
steering_sweep.sweep_amplitude_deg: 10.0
|
steering_sweep.sweep_amplitude_deg: 10.0
|
||||||
@@ -171,6 +229,7 @@ chassis_profiles:
|
|||||||
- task_code: chassis.single_steer.arc_left
|
- task_code: chassis.single_steer.arc_left
|
||||||
display_name: 单舵轮左圆弧
|
display_name: 单舵轮左圆弧
|
||||||
primitive_type: arc
|
primitive_type: arc
|
||||||
|
reference_path_id: chassis_arc_left_r1_45deg
|
||||||
metadata:
|
metadata:
|
||||||
arc.target_speed_ms: 0.08
|
arc.target_speed_ms: 0.08
|
||||||
arc.radius_m: 1.0
|
arc.radius_m: 1.0
|
||||||
@@ -181,6 +240,7 @@ chassis_profiles:
|
|||||||
- task_code: chassis.single_steer.arc_right
|
- task_code: chassis.single_steer.arc_right
|
||||||
display_name: 单舵轮右圆弧
|
display_name: 单舵轮右圆弧
|
||||||
primitive_type: arc
|
primitive_type: arc
|
||||||
|
reference_path_id: chassis_arc_right_r1_45deg
|
||||||
metadata:
|
metadata:
|
||||||
arc.target_speed_ms: 0.08
|
arc.target_speed_ms: 0.08
|
||||||
arc.radius_m: 1.0
|
arc.radius_m: 1.0
|
||||||
@@ -200,6 +260,7 @@ chassis_profiles:
|
|||||||
- task_code: chassis.multi_steer.straight_forward
|
- task_code: chassis.multi_steer.straight_forward
|
||||||
display_name: 多舵轮直线前进
|
display_name: 多舵轮直线前进
|
||||||
primitive_type: straight_line
|
primitive_type: straight_line
|
||||||
|
reference_path_id: chassis_straight_forward_1m
|
||||||
metadata:
|
metadata:
|
||||||
straight_line.target_distance_m: 1.0
|
straight_line.target_distance_m: 1.0
|
||||||
straight_line.target_speed_ms: 0.1
|
straight_line.target_speed_ms: 0.1
|
||||||
@@ -209,6 +270,7 @@ chassis_profiles:
|
|||||||
- task_code: chassis.multi_steer.lateral_left
|
- task_code: chassis.multi_steer.lateral_left
|
||||||
display_name: 多舵轮左横移
|
display_name: 多舵轮左横移
|
||||||
primitive_type: lateral_translation
|
primitive_type: lateral_translation
|
||||||
|
reference_path_id: chassis_lateral_left_0_5m
|
||||||
metadata:
|
metadata:
|
||||||
lateral_translation.target_speed_ms: 0.06
|
lateral_translation.target_speed_ms: 0.06
|
||||||
lateral_translation.target_distance_m: 0.5
|
lateral_translation.target_distance_m: 0.5
|
||||||
@@ -218,6 +280,7 @@ chassis_profiles:
|
|||||||
- task_code: chassis.multi_steer.lateral_right
|
- task_code: chassis.multi_steer.lateral_right
|
||||||
display_name: 多舵轮右横移
|
display_name: 多舵轮右横移
|
||||||
primitive_type: lateral_translation
|
primitive_type: lateral_translation
|
||||||
|
reference_path_id: chassis_lateral_right_0_5m
|
||||||
metadata:
|
metadata:
|
||||||
lateral_translation.target_speed_ms: 0.06
|
lateral_translation.target_speed_ms: 0.06
|
||||||
lateral_translation.target_distance_m: 0.5
|
lateral_translation.target_distance_m: 0.5
|
||||||
@@ -227,6 +290,7 @@ chassis_profiles:
|
|||||||
- task_code: chassis.multi_steer.diagonal_forward_left
|
- task_code: chassis.multi_steer.diagonal_forward_left
|
||||||
display_name: 多舵轮左前斜移
|
display_name: 多舵轮左前斜移
|
||||||
primitive_type: diagonal_motion
|
primitive_type: diagonal_motion
|
||||||
|
reference_path_id: chassis_diagonal_forward_left_0_5m
|
||||||
metadata:
|
metadata:
|
||||||
diagonal_motion.target_speed_ms: 0.06
|
diagonal_motion.target_speed_ms: 0.06
|
||||||
diagonal_motion.target_distance_m: 0.5
|
diagonal_motion.target_distance_m: 0.5
|
||||||
@@ -236,6 +300,7 @@ chassis_profiles:
|
|||||||
- task_code: chassis.multi_steer.diagonal_forward_right
|
- task_code: chassis.multi_steer.diagonal_forward_right
|
||||||
display_name: 多舵轮右前斜移
|
display_name: 多舵轮右前斜移
|
||||||
primitive_type: diagonal_motion
|
primitive_type: diagonal_motion
|
||||||
|
reference_path_id: chassis_diagonal_forward_right_0_5m
|
||||||
metadata:
|
metadata:
|
||||||
diagonal_motion.target_speed_ms: 0.06
|
diagonal_motion.target_speed_ms: 0.06
|
||||||
diagonal_motion.target_distance_m: 0.5
|
diagonal_motion.target_distance_m: 0.5
|
||||||
@@ -245,6 +310,7 @@ chassis_profiles:
|
|||||||
- task_code: chassis.multi_steer.module_alignment
|
- task_code: chassis.multi_steer.module_alignment
|
||||||
display_name: 多舵轮模块零位检查
|
display_name: 多舵轮模块零位检查
|
||||||
primitive_type: module_alignment
|
primitive_type: module_alignment
|
||||||
|
reference_path_id: chassis_static_station
|
||||||
metadata:
|
metadata:
|
||||||
module_alignment.module_ids:
|
module_alignment.module_ids:
|
||||||
- front_left
|
- front_left
|
||||||
@@ -258,6 +324,7 @@ chassis_profiles:
|
|||||||
- task_code: chassis.multi_steer.coordinated_steering
|
- task_code: chassis.multi_steer.coordinated_steering
|
||||||
display_name: 多舵轮协同转向
|
display_name: 多舵轮协同转向
|
||||||
primitive_type: coordinated_steering
|
primitive_type: coordinated_steering
|
||||||
|
reference_path_id: chassis_static_station
|
||||||
metadata:
|
metadata:
|
||||||
coordinated_steering.module_ids:
|
coordinated_steering.module_ids:
|
||||||
- front_left
|
- front_left
|
||||||
|
|||||||
+36
@@ -0,0 +1,36 @@
|
|||||||
|
# Isaac 仿真底盘路径执行/采集配置。
|
||||||
|
# 用于验证:选择底盘 reference path -> 执行动作 -> 采集底盘遥测和 external truth。
|
||||||
|
|
||||||
|
csv_contract_version: 1
|
||||||
|
chassis_type: ackermann
|
||||||
|
action_profile_file: src/site_deployment/workshop_chassis_calibration_real/config/chassis_action_profile.yaml
|
||||||
|
session_id: sim_chassis_path_test
|
||||||
|
site_id: isaac_workcell_zone_a
|
||||||
|
vehicle_id: demo_agv_001
|
||||||
|
session_dir: /tmp/agv_calib_chassis_sim/session_latest
|
||||||
|
dataset_index_path: /tmp/agv_calib_chassis_sim/session_latest/dataset_index.yaml
|
||||||
|
|
||||||
|
files:
|
||||||
|
chassis_motion_data_file: chassis/chassis_motion.csv
|
||||||
|
actuator_command_file: chassis/actuator_commands.csv
|
||||||
|
truth_trajectory_file: external/truth_trajectory.csv
|
||||||
|
diagnostics_file: chassis/chassis_diagnostics.json
|
||||||
|
|
||||||
|
capture:
|
||||||
|
chassis_telemetry_topic: /chassis/telemetry
|
||||||
|
external_pose_topic: /isaac/external_localization/vehicle/pose
|
||||||
|
ackermann_command_topic: /vehicle/demo_agv_001/internal/ackermann_cmd
|
||||||
|
command_source: auto
|
||||||
|
|
||||||
|
validation:
|
||||||
|
min_chassis_motion_samples: 2
|
||||||
|
min_actuator_command_samples: 1
|
||||||
|
min_truth_trajectory_samples: 2
|
||||||
|
min_motion_distance_m: 0.01
|
||||||
|
max_time_gap_ms: 300.0
|
||||||
|
|
||||||
|
synthetic:
|
||||||
|
enabled: true
|
||||||
|
sample_period_ms: 100
|
||||||
|
target_speed_ms: 0.1
|
||||||
|
target_distance_m: 0.2
|
||||||
+47
@@ -0,0 +1,47 @@
|
|||||||
|
schema_version: 1
|
||||||
|
path_id: chassis_ackermann_s_curve_r1_5
|
||||||
|
display_name: 阿克曼 S 形路径 R1.5
|
||||||
|
module_type: chassis
|
||||||
|
frame_id: workshop
|
||||||
|
path_type: s_curve
|
||||||
|
recommended_task_types:
|
||||||
|
- arc
|
||||||
|
- s_curve
|
||||||
|
description: 由左 25deg、右 60deg、左 60deg、右 25deg 四段 R1.5 圆弧组成的低速阿克曼 S 形路径。
|
||||||
|
points:
|
||||||
|
- x_m: 0.000
|
||||||
|
y_m: 0.000
|
||||||
|
yaw_rad: 0.000
|
||||||
|
target_speed_ms: 0.10
|
||||||
|
- x_m: 0.325
|
||||||
|
y_m: 0.036
|
||||||
|
yaw_rad: 0.218
|
||||||
|
target_speed_ms: 0.10
|
||||||
|
- x_m: 0.634
|
||||||
|
y_m: 0.141
|
||||||
|
yaw_rad: 0.436
|
||||||
|
target_speed_ms: 0.10
|
||||||
|
- x_m: 1.399
|
||||||
|
y_m: 0.275
|
||||||
|
yaw_rad: -0.087
|
||||||
|
target_speed_ms: 0.10
|
||||||
|
- x_m: 2.128
|
||||||
|
y_m: 0.010
|
||||||
|
yaw_rad: -0.611
|
||||||
|
target_speed_ms: 0.10
|
||||||
|
- x_m: 2.858
|
||||||
|
y_m: -0.256
|
||||||
|
yaw_rad: -0.087
|
||||||
|
target_speed_ms: 0.10
|
||||||
|
- x_m: 3.623
|
||||||
|
y_m: -0.121
|
||||||
|
yaw_rad: 0.436
|
||||||
|
target_speed_ms: 0.10
|
||||||
|
- x_m: 3.932
|
||||||
|
y_m: -0.016
|
||||||
|
yaw_rad: 0.218
|
||||||
|
target_speed_ms: 0.10
|
||||||
|
- x_m: 4.256
|
||||||
|
y_m: 0.020
|
||||||
|
yaw_rad: 0.000
|
||||||
|
target_speed_ms: 0.10
|
||||||
+21
@@ -0,0 +1,21 @@
|
|||||||
|
schema_version: 1
|
||||||
|
path_id: chassis_arc_left_r1_45deg
|
||||||
|
display_name: 底盘左圆弧 R1 45deg
|
||||||
|
module_type: chassis
|
||||||
|
frame_id: workshop
|
||||||
|
path_type: arc
|
||||||
|
recommended_task_types:
|
||||||
|
- arc
|
||||||
|
points:
|
||||||
|
- x_m: 0.0
|
||||||
|
y_m: 0.0
|
||||||
|
yaw_rad: 0.0
|
||||||
|
target_speed_ms: 0.08
|
||||||
|
- x_m: 0.383
|
||||||
|
y_m: 0.076
|
||||||
|
yaw_rad: 0.393
|
||||||
|
target_speed_ms: 0.08
|
||||||
|
- x_m: 0.707
|
||||||
|
y_m: 0.293
|
||||||
|
yaw_rad: 0.785
|
||||||
|
target_speed_ms: 0.08
|
||||||
+21
@@ -0,0 +1,21 @@
|
|||||||
|
schema_version: 1
|
||||||
|
path_id: chassis_arc_right_r1_45deg
|
||||||
|
display_name: 底盘右圆弧 R1 45deg
|
||||||
|
module_type: chassis
|
||||||
|
frame_id: workshop
|
||||||
|
path_type: arc
|
||||||
|
recommended_task_types:
|
||||||
|
- arc
|
||||||
|
points:
|
||||||
|
- x_m: 0.0
|
||||||
|
y_m: 0.0
|
||||||
|
yaw_rad: 0.0
|
||||||
|
target_speed_ms: 0.08
|
||||||
|
- x_m: 0.383
|
||||||
|
y_m: -0.076
|
||||||
|
yaw_rad: -0.393
|
||||||
|
target_speed_ms: 0.08
|
||||||
|
- x_m: 0.707
|
||||||
|
y_m: -0.293
|
||||||
|
yaw_rad: -0.785
|
||||||
|
target_speed_ms: 0.08
|
||||||
+17
@@ -0,0 +1,17 @@
|
|||||||
|
schema_version: 1
|
||||||
|
path_id: chassis_diagonal_forward_left_0_5m
|
||||||
|
display_name: 多舵轮左前斜移 0.5m
|
||||||
|
module_type: chassis
|
||||||
|
frame_id: workshop
|
||||||
|
path_type: diagonal_motion
|
||||||
|
recommended_task_types:
|
||||||
|
- diagonal_motion
|
||||||
|
points:
|
||||||
|
- x_m: 0.0
|
||||||
|
y_m: 0.0
|
||||||
|
yaw_rad: 0.0
|
||||||
|
target_speed_ms: 0.06
|
||||||
|
- x_m: 0.354
|
||||||
|
y_m: 0.354
|
||||||
|
yaw_rad: 0.0
|
||||||
|
target_speed_ms: 0.06
|
||||||
+17
@@ -0,0 +1,17 @@
|
|||||||
|
schema_version: 1
|
||||||
|
path_id: chassis_diagonal_forward_right_0_5m
|
||||||
|
display_name: 多舵轮右前斜移 0.5m
|
||||||
|
module_type: chassis
|
||||||
|
frame_id: workshop
|
||||||
|
path_type: diagonal_motion
|
||||||
|
recommended_task_types:
|
||||||
|
- diagonal_motion
|
||||||
|
points:
|
||||||
|
- x_m: 0.0
|
||||||
|
y_m: 0.0
|
||||||
|
yaw_rad: 0.0
|
||||||
|
target_speed_ms: 0.06
|
||||||
|
- x_m: 0.354
|
||||||
|
y_m: -0.354
|
||||||
|
yaw_rad: 0.0
|
||||||
|
target_speed_ms: 0.06
|
||||||
+17
@@ -0,0 +1,17 @@
|
|||||||
|
schema_version: 1
|
||||||
|
path_id: chassis_lateral_left_0_5m
|
||||||
|
display_name: 多舵轮左横移 0.5m
|
||||||
|
module_type: chassis
|
||||||
|
frame_id: workshop
|
||||||
|
path_type: lateral_translation
|
||||||
|
recommended_task_types:
|
||||||
|
- lateral_translation
|
||||||
|
points:
|
||||||
|
- x_m: 0.0
|
||||||
|
y_m: 0.0
|
||||||
|
yaw_rad: 0.0
|
||||||
|
target_speed_ms: 0.06
|
||||||
|
- x_m: 0.0
|
||||||
|
y_m: 0.5
|
||||||
|
yaw_rad: 0.0
|
||||||
|
target_speed_ms: 0.06
|
||||||
+17
@@ -0,0 +1,17 @@
|
|||||||
|
schema_version: 1
|
||||||
|
path_id: chassis_lateral_right_0_5m
|
||||||
|
display_name: 多舵轮右横移 0.5m
|
||||||
|
module_type: chassis
|
||||||
|
frame_id: workshop
|
||||||
|
path_type: lateral_translation
|
||||||
|
recommended_task_types:
|
||||||
|
- lateral_translation
|
||||||
|
points:
|
||||||
|
- x_m: 0.0
|
||||||
|
y_m: 0.0
|
||||||
|
yaw_rad: 0.0
|
||||||
|
target_speed_ms: 0.06
|
||||||
|
- x_m: 0.0
|
||||||
|
y_m: -0.5
|
||||||
|
yaw_rad: 0.0
|
||||||
|
target_speed_ms: 0.06
|
||||||
+17
@@ -0,0 +1,17 @@
|
|||||||
|
schema_version: 1
|
||||||
|
path_id: chassis_rotate_left_90deg
|
||||||
|
display_name: 底盘原地左转 90deg
|
||||||
|
module_type: chassis
|
||||||
|
frame_id: workshop
|
||||||
|
path_type: in_place_rotation
|
||||||
|
recommended_task_types:
|
||||||
|
- in_place_rotation
|
||||||
|
points:
|
||||||
|
- x_m: 0.0
|
||||||
|
y_m: 0.0
|
||||||
|
yaw_rad: 0.0
|
||||||
|
target_speed_ms: 0.0
|
||||||
|
- x_m: 0.0
|
||||||
|
y_m: 0.0
|
||||||
|
yaw_rad: 1.570796327
|
||||||
|
target_speed_ms: 0.0
|
||||||
+17
@@ -0,0 +1,17 @@
|
|||||||
|
schema_version: 1
|
||||||
|
path_id: chassis_rotate_right_90deg
|
||||||
|
display_name: 底盘原地右转 90deg
|
||||||
|
module_type: chassis
|
||||||
|
frame_id: workshop
|
||||||
|
path_type: in_place_rotation
|
||||||
|
recommended_task_types:
|
||||||
|
- in_place_rotation
|
||||||
|
points:
|
||||||
|
- x_m: 0.0
|
||||||
|
y_m: 0.0
|
||||||
|
yaw_rad: 0.0
|
||||||
|
target_speed_ms: 0.0
|
||||||
|
- x_m: 0.0
|
||||||
|
y_m: 0.0
|
||||||
|
yaw_rad: -1.570796327
|
||||||
|
target_speed_ms: 0.0
|
||||||
+15
@@ -0,0 +1,15 @@
|
|||||||
|
schema_version: 1
|
||||||
|
path_id: chassis_static_station
|
||||||
|
display_name: 底盘静态检查点
|
||||||
|
module_type: chassis
|
||||||
|
frame_id: workshop
|
||||||
|
path_type: static_station
|
||||||
|
recommended_task_types:
|
||||||
|
- steering_sweep
|
||||||
|
- module_alignment
|
||||||
|
- coordinated_steering
|
||||||
|
points:
|
||||||
|
- x_m: 0.0
|
||||||
|
y_m: 0.0
|
||||||
|
yaw_rad: 0.0
|
||||||
|
target_speed_ms: 0.0
|
||||||
+18
@@ -0,0 +1,18 @@
|
|||||||
|
schema_version: 1
|
||||||
|
path_id: chassis_straight_forward_1m
|
||||||
|
display_name: 底盘直线前进 1m
|
||||||
|
description: 用于轮径、里程计比例和直线跑偏检查。
|
||||||
|
module_type: chassis
|
||||||
|
frame_id: workshop
|
||||||
|
path_type: straight_line
|
||||||
|
recommended_task_types:
|
||||||
|
- straight_line
|
||||||
|
points:
|
||||||
|
- x_m: 0.0
|
||||||
|
y_m: 0.0
|
||||||
|
yaw_rad: 0.0
|
||||||
|
target_speed_ms: 0.10
|
||||||
|
- x_m: 1.0
|
||||||
|
y_m: 0.0
|
||||||
|
yaw_rad: 0.0
|
||||||
|
target_speed_ms: 0.10
|
||||||
+17
@@ -0,0 +1,17 @@
|
|||||||
|
schema_version: 1
|
||||||
|
path_id: chassis_straight_reverse_0_6m
|
||||||
|
display_name: 底盘直线倒车 0.6m
|
||||||
|
module_type: chassis
|
||||||
|
frame_id: workshop
|
||||||
|
path_type: straight_line
|
||||||
|
recommended_task_types:
|
||||||
|
- straight_line
|
||||||
|
points:
|
||||||
|
- x_m: 0.0
|
||||||
|
y_m: 0.0
|
||||||
|
yaw_rad: 0.0
|
||||||
|
target_speed_ms: 0.08
|
||||||
|
- x_m: -0.6
|
||||||
|
y_m: 0.0
|
||||||
|
yaw_rad: 3.141592654
|
||||||
|
target_speed_ms: 0.08
|
||||||
+19
@@ -0,0 +1,19 @@
|
|||||||
|
profile_name: chassis_calibration_reference_paths
|
||||||
|
frame_id: workshop
|
||||||
|
|
||||||
|
default_path_id_by_chassis:
|
||||||
|
ackermann: chassis_straight_forward_1m
|
||||||
|
differential: chassis_straight_forward_1m
|
||||||
|
single_steer_wheel: chassis_straight_forward_1m
|
||||||
|
multi_steer_wheel: chassis_lateral_left_0_5m
|
||||||
|
|
||||||
|
default_path_id_by_task_type:
|
||||||
|
straight_line: chassis_straight_forward_1m
|
||||||
|
arc: chassis_arc_left_r1_45deg
|
||||||
|
in_place_rotation: chassis_rotate_left_90deg
|
||||||
|
s_curve: chassis_ackermann_s_curve_r1_5
|
||||||
|
steering_sweep: chassis_static_station
|
||||||
|
lateral_translation: chassis_lateral_left_0_5m
|
||||||
|
diagonal_motion: chassis_diagonal_forward_left_0_5m
|
||||||
|
module_alignment: chassis_static_station
|
||||||
|
coordinated_steering: chassis_static_station
|
||||||
+20
-5
@@ -51,6 +51,7 @@ def parse_args() -> argparse.Namespace:
|
|||||||
)
|
)
|
||||||
parser.add_argument("--chassis-type", default="", help="覆盖 chassis_type,并选择对应动作序列。")
|
parser.add_argument("--chassis-type", default="", help="覆盖 chassis_type,并选择对应动作序列。")
|
||||||
parser.add_argument("--task-code", default="", help="只执行指定 task_code;为空时执行该底盘类型全部动作。")
|
parser.add_argument("--task-code", default="", help="只执行指定 task_code;为空时执行该底盘类型全部动作。")
|
||||||
|
parser.add_argument("--reference-path-id", default="", help="只执行绑定到指定 reference_path_id 的动作。")
|
||||||
parser.add_argument("--session-id", default="", help="覆盖 session_id。")
|
parser.add_argument("--session-id", default="", help="覆盖 session_id。")
|
||||||
parser.add_argument("--site-id", default="", help="覆盖 site_id。")
|
parser.add_argument("--site-id", default="", help="覆盖 site_id。")
|
||||||
parser.add_argument("--vehicle-id", default="", help="覆盖 vehicle_id。")
|
parser.add_argument("--vehicle-id", default="", help="覆盖 vehicle_id。")
|
||||||
@@ -130,14 +131,28 @@ def spin_until_future(
|
|||||||
return future.result()
|
return future.result()
|
||||||
|
|
||||||
|
|
||||||
def select_actions(profile: dict[str, Any], chassis_type: str, task_code: str) -> list[dict[str, Any]]:
|
def select_actions(
|
||||||
|
profile: dict[str, Any],
|
||||||
|
chassis_type: str,
|
||||||
|
task_code: str,
|
||||||
|
reference_path_id: str,
|
||||||
|
) -> list[dict[str, Any]]:
|
||||||
section = profile["chassis_profiles"][chassis_type]
|
section = profile["chassis_profiles"][chassis_type]
|
||||||
actions = list(section["actions"])
|
actions = list(section["actions"])
|
||||||
if not task_code:
|
if task_code:
|
||||||
|
actions = [action for action in actions if action.get("task_code") == task_code]
|
||||||
|
if reference_path_id:
|
||||||
|
actions = [action for action in actions if action.get("reference_path_id") == reference_path_id]
|
||||||
|
if not task_code and not reference_path_id:
|
||||||
return actions
|
return actions
|
||||||
selected = [action for action in actions if action.get("task_code") == task_code]
|
selected = actions
|
||||||
if not selected:
|
if not selected:
|
||||||
raise ValueError(f"动作 profile 中没有 task_code={task_code!r}。")
|
detail = []
|
||||||
|
if task_code:
|
||||||
|
detail.append(f"task_code={task_code!r}")
|
||||||
|
if reference_path_id:
|
||||||
|
detail.append(f"reference_path_id={reference_path_id!r}")
|
||||||
|
raise ValueError(f"动作 profile 中没有 {' 且 '.join(detail)} 的动作。")
|
||||||
return selected
|
return selected
|
||||||
|
|
||||||
|
|
||||||
@@ -349,7 +364,7 @@ def main() -> int:
|
|||||||
profile_path = Path(args.action_profile).expanduser().resolve(strict=False)
|
profile_path = Path(args.action_profile).expanduser().resolve(strict=False)
|
||||||
action_profile = validate_action_profile(load_action_profile_yaml(profile_path))
|
action_profile = validate_action_profile(load_action_profile_yaml(profile_path))
|
||||||
chassis_type = str(config["chassis_type"])
|
chassis_type = str(config["chassis_type"])
|
||||||
actions = select_actions(action_profile, chassis_type, args.task_code)
|
actions = select_actions(action_profile, chassis_type, args.task_code, args.reference_path_id)
|
||||||
data_capture = action_profile.get("data_capture", {}) or {}
|
data_capture = action_profile.get("data_capture", {}) or {}
|
||||||
start_before_sec = (
|
start_before_sec = (
|
||||||
as_float(data_capture.get("start_before_motion_sec"), 1.0)
|
as_float(data_capture.get("start_before_motion_sec"), 1.0)
|
||||||
|
|||||||
+322
@@ -0,0 +1,322 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""车间电脑侧 WiFi/TCP 底盘遥测接收桥。
|
||||||
|
|
||||||
|
车端通过 TCP 推送 JSON 遥测,本节点转换成标准 ChassisTelemetry 并发布到 ROS2。
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import json
|
||||||
|
import socket
|
||||||
|
import socketserver
|
||||||
|
import struct
|
||||||
|
import threading
|
||||||
|
import time
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
try:
|
||||||
|
import rclpy
|
||||||
|
from calibration_chassis_interfaces.msg import ChassisTelemetry, WheelModuleState
|
||||||
|
except ImportError:
|
||||||
|
rclpy = None
|
||||||
|
ChassisTelemetry = None
|
||||||
|
WheelModuleState = None
|
||||||
|
|
||||||
|
|
||||||
|
CHASSIS_TELEMETRY_PUSH_REQ = 7
|
||||||
|
CHASSIS_TELEMETRY_PUSH_RSP = 8
|
||||||
|
|
||||||
|
CHASSIS_TYPE_BY_NAME = {
|
||||||
|
"ackermann": 1,
|
||||||
|
"differential": 2,
|
||||||
|
"single_steer_wheel": 3,
|
||||||
|
"multi_steer_wheel": 4,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def now_us() -> int:
|
||||||
|
return int(time.time() * 1_000_000)
|
||||||
|
|
||||||
|
|
||||||
|
def read_exactly(conn: socket.socket, size: int) -> bytes:
|
||||||
|
chunks: list[bytes] = []
|
||||||
|
remaining = size
|
||||||
|
while remaining > 0:
|
||||||
|
chunk = conn.recv(remaining)
|
||||||
|
if not chunk:
|
||||||
|
raise ConnectionError("连接已关闭")
|
||||||
|
chunks.append(chunk)
|
||||||
|
remaining -= len(chunk)
|
||||||
|
return b"".join(chunks)
|
||||||
|
|
||||||
|
|
||||||
|
def send_frame(conn: socket.socket, msg_type: int, payload: dict[str, Any]) -> None:
|
||||||
|
encoded = json.dumps(payload, ensure_ascii=False, separators=(",", ":")).encode("utf-8")
|
||||||
|
conn.sendall(struct.pack("<II", msg_type, len(encoded)) + encoded)
|
||||||
|
|
||||||
|
|
||||||
|
def as_float(value: Any, default: float = 0.0) -> float:
|
||||||
|
if value in (None, ""):
|
||||||
|
return default
|
||||||
|
return float(value)
|
||||||
|
|
||||||
|
|
||||||
|
def as_int(value: Any, default: int = 0) -> int:
|
||||||
|
if value in (None, ""):
|
||||||
|
return default
|
||||||
|
return int(float(value))
|
||||||
|
|
||||||
|
|
||||||
|
def as_bool(value: Any, default: bool = False) -> bool:
|
||||||
|
if value in (None, ""):
|
||||||
|
return default
|
||||||
|
if isinstance(value, bool):
|
||||||
|
return value
|
||||||
|
return str(value).strip().lower() in {"1", "true", "yes", "on"}
|
||||||
|
|
||||||
|
|
||||||
|
def chassis_type_value(value: Any, default_name: str) -> int:
|
||||||
|
if isinstance(value, dict):
|
||||||
|
value = value.get("value", "")
|
||||||
|
if value in (None, ""):
|
||||||
|
return CHASSIS_TYPE_BY_NAME.get(default_name, 0)
|
||||||
|
if isinstance(value, (int, float)):
|
||||||
|
return int(value)
|
||||||
|
text = str(value).strip().lower()
|
||||||
|
if text.isdigit():
|
||||||
|
return int(text)
|
||||||
|
return CHASSIS_TYPE_BY_NAME.get(text, CHASSIS_TYPE_BY_NAME.get(default_name, 0))
|
||||||
|
|
||||||
|
|
||||||
|
def nested_payload(payload: dict[str, Any]) -> dict[str, Any]:
|
||||||
|
for key in ("telemetry", "chassis_telemetry", "chassis"):
|
||||||
|
value = payload.get(key)
|
||||||
|
if isinstance(value, dict):
|
||||||
|
return value
|
||||||
|
return payload
|
||||||
|
|
||||||
|
|
||||||
|
def module_payloads(payload: dict[str, Any]) -> list[dict[str, Any]]:
|
||||||
|
raw = payload.get("modules", payload.get("module_states", []))
|
||||||
|
if isinstance(raw, dict):
|
||||||
|
modules = []
|
||||||
|
for module_id, value in raw.items():
|
||||||
|
if isinstance(value, dict):
|
||||||
|
modules.append({"module_id": module_id, **value})
|
||||||
|
return modules
|
||||||
|
if isinstance(raw, list):
|
||||||
|
return [value for value in raw if isinstance(value, dict)]
|
||||||
|
return []
|
||||||
|
|
||||||
|
|
||||||
|
def build_module_msg(payload: dict[str, Any]) -> Any:
|
||||||
|
module = WheelModuleState()
|
||||||
|
module.module_id = str(payload.get("module_id", payload.get("id", "")))
|
||||||
|
module.encoder_ticks = as_int(payload.get("encoder_ticks", payload.get("ticks", 0)))
|
||||||
|
module.wheel_speed_rpm = as_float(payload.get("wheel_speed_rpm", payload.get("rpm", 0.0)))
|
||||||
|
module.steer_angle_deg = as_float(payload.get("steer_angle_deg", payload.get("steer_deg", 0.0)))
|
||||||
|
module.motor_current_amp = as_float(payload.get("motor_current_amp", payload.get("current_amp", 0.0)))
|
||||||
|
return module
|
||||||
|
|
||||||
|
|
||||||
|
def build_chassis_telemetry(payload: dict[str, Any], default_chassis_type: str) -> Any:
|
||||||
|
data = nested_payload(payload)
|
||||||
|
msg = ChassisTelemetry()
|
||||||
|
raw_timestamp = data.get("hardware_timestamp_us", data.get("timestamp_us", data.get("timestamp", "")))
|
||||||
|
msg.hardware_timestamp_us = as_int(raw_timestamp, now_us())
|
||||||
|
msg.chassis_type.value = chassis_type_value(data.get("chassis_type", ""), default_chassis_type)
|
||||||
|
msg.odom_x_m = as_float(data.get("odom_x_m", data.get("x_m", 0.0)))
|
||||||
|
msg.odom_y_m = as_float(data.get("odom_y_m", data.get("y_m", 0.0)))
|
||||||
|
msg.odom_yaw_rad = as_float(data.get("odom_yaw_rad", data.get("yaw_rad", 0.0)))
|
||||||
|
msg.linear_velocity_ms = as_float(data.get("linear_velocity_ms", data.get("linear_speed_ms", 0.0)))
|
||||||
|
msg.angular_velocity_rads = as_float(data.get("angular_velocity_rads", data.get("angular_speed_rads", 0.0)))
|
||||||
|
msg.estop_engaged = as_bool(data.get("estop_engaged", data.get("estop", False)))
|
||||||
|
msg.driver_error_code = as_int(data.get("driver_error_code", data.get("error_code", 0)))
|
||||||
|
msg.active_job_id = str(data.get("active_job_id", data.get("job_id", "")))
|
||||||
|
msg.lateral_slip_estimate = as_float(data.get("lateral_slip_estimate", 0.0))
|
||||||
|
msg.curvature_estimate = as_float(data.get("curvature_estimate", 0.0))
|
||||||
|
msg.modules = [build_module_msg(module) for module in module_payloads(data)]
|
||||||
|
return msg
|
||||||
|
|
||||||
|
|
||||||
|
class TelemetryTCPServer(socketserver.ThreadingMixIn, socketserver.TCPServer):
|
||||||
|
allow_reuse_address = True
|
||||||
|
daemon_threads = True
|
||||||
|
|
||||||
|
|
||||||
|
class TelemetryHandler(socketserver.BaseRequestHandler):
|
||||||
|
def handle(self) -> None:
|
||||||
|
self.server.bridge.handle_connection(self.request, self.client_address)
|
||||||
|
|
||||||
|
|
||||||
|
class WorkshopChassisTelemetryBridge:
|
||||||
|
def __init__(self, args: argparse.Namespace) -> None:
|
||||||
|
if rclpy is None or ChassisTelemetry is None or WheelModuleState is None:
|
||||||
|
raise RuntimeError("缺少 ROS 2 Python 依赖或 calibration_chassis_interfaces。")
|
||||||
|
|
||||||
|
self.args = args
|
||||||
|
self.node = rclpy.create_node("workshop_chassis_telemetry_bridge")
|
||||||
|
self.publisher = self.node.create_publisher(ChassisTelemetry, args.output_topic, 50)
|
||||||
|
self.received_count = 0
|
||||||
|
self.published_count = 0
|
||||||
|
self.error_count = 0
|
||||||
|
self.lock = threading.Lock()
|
||||||
|
self.last_stats_log_monotonic = 0.0
|
||||||
|
self.server = TelemetryTCPServer((args.bind_host, args.bind_port), TelemetryHandler)
|
||||||
|
self.server.bridge = self
|
||||||
|
self.thread = threading.Thread(target=self.server.serve_forever, name="chassis-telemetry-bridge", daemon=True)
|
||||||
|
|
||||||
|
def start(self) -> None:
|
||||||
|
self.thread.start()
|
||||||
|
self.node.get_logger().info(
|
||||||
|
"底盘遥测 WiFi/TCP bridge 已启动: "
|
||||||
|
f"{self.args.bind_host}:{self.args.bind_port} -> {self.args.output_topic}, "
|
||||||
|
f"protocol={self.args.protocol}"
|
||||||
|
)
|
||||||
|
|
||||||
|
def shutdown(self) -> None:
|
||||||
|
self.server.shutdown()
|
||||||
|
self.server.server_close()
|
||||||
|
|
||||||
|
def log_stats(self) -> None:
|
||||||
|
now = time.monotonic()
|
||||||
|
if now - self.last_stats_log_monotonic < self.args.stats_log_interval_sec:
|
||||||
|
return
|
||||||
|
self.last_stats_log_monotonic = now
|
||||||
|
self.node.get_logger().info(
|
||||||
|
"底盘遥测 bridge 统计: "
|
||||||
|
f"received={self.received_count}, published={self.published_count}, errors={self.error_count}"
|
||||||
|
)
|
||||||
|
|
||||||
|
def detect_protocol(self, conn: socket.socket) -> str:
|
||||||
|
if self.args.protocol != "auto":
|
||||||
|
return self.args.protocol
|
||||||
|
peek = conn.recv(8, socket.MSG_PEEK)
|
||||||
|
first = peek.lstrip()[:1]
|
||||||
|
if first in (b"{", b"["):
|
||||||
|
return "json_lines"
|
||||||
|
return "frame"
|
||||||
|
|
||||||
|
def handle_connection(self, conn: socket.socket, address: tuple[str, int]) -> None:
|
||||||
|
conn.settimeout(self.args.connection_timeout_sec)
|
||||||
|
try:
|
||||||
|
protocol = self.detect_protocol(conn)
|
||||||
|
if protocol == "frame":
|
||||||
|
self.handle_frame_connection(conn)
|
||||||
|
elif protocol == "json_lines":
|
||||||
|
self.handle_json_lines_connection(conn)
|
||||||
|
elif protocol == "raw_json":
|
||||||
|
self.handle_raw_json_connection(conn)
|
||||||
|
else:
|
||||||
|
raise ValueError(f"未知协议: {protocol}")
|
||||||
|
except ConnectionError:
|
||||||
|
return
|
||||||
|
except Exception as exc:
|
||||||
|
with self.lock:
|
||||||
|
self.error_count += 1
|
||||||
|
self.node.get_logger().warning(f"底盘遥测连接处理失败 {address}: {exc}")
|
||||||
|
|
||||||
|
def publish_payload(self, payload: dict[str, Any]) -> None:
|
||||||
|
msg = build_chassis_telemetry(payload, self.args.default_chassis_type)
|
||||||
|
self.publisher.publish(msg)
|
||||||
|
with self.lock:
|
||||||
|
self.received_count += 1
|
||||||
|
self.published_count += 1
|
||||||
|
self.log_stats()
|
||||||
|
|
||||||
|
def handle_frame_connection(self, conn: socket.socket) -> None:
|
||||||
|
while rclpy.ok():
|
||||||
|
header = read_exactly(conn, 8)
|
||||||
|
msg_type, payload_len = struct.unpack("<II", header)
|
||||||
|
if payload_len > self.args.max_payload_bytes:
|
||||||
|
raise ValueError(f"payload 过大: {payload_len} > {self.args.max_payload_bytes}")
|
||||||
|
raw_payload = read_exactly(conn, payload_len) if payload_len else b"{}"
|
||||||
|
payload = json.loads(raw_payload.decode("utf-8"))
|
||||||
|
if self.args.expected_msg_type and msg_type != self.args.expected_msg_type:
|
||||||
|
raise ValueError(f"msg_type 不匹配: expected={self.args.expected_msg_type}, actual={msg_type}")
|
||||||
|
self.publish_payload(payload)
|
||||||
|
if self.args.send_ack:
|
||||||
|
send_frame(
|
||||||
|
conn,
|
||||||
|
self.args.ack_msg_type,
|
||||||
|
{"success": True, "message": "chassis telemetry accepted", "timestamp_us": now_us()},
|
||||||
|
)
|
||||||
|
|
||||||
|
def handle_json_lines_connection(self, conn: socket.socket) -> None:
|
||||||
|
with conn.makefile("rb") as stream:
|
||||||
|
for raw_line in stream:
|
||||||
|
line = raw_line.strip()
|
||||||
|
if not line:
|
||||||
|
continue
|
||||||
|
if len(line) > self.args.max_payload_bytes:
|
||||||
|
raise ValueError(f"payload 过大: {len(line)} > {self.args.max_payload_bytes}")
|
||||||
|
payload = json.loads(line.decode("utf-8"))
|
||||||
|
self.publish_payload(payload)
|
||||||
|
if self.args.send_ack:
|
||||||
|
conn.sendall(
|
||||||
|
(json.dumps({"success": True, "timestamp_us": now_us()}, separators=(",", ":")) + "\n").encode(
|
||||||
|
"utf-8"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
def handle_raw_json_connection(self, conn: socket.socket) -> None:
|
||||||
|
chunks: list[bytes] = []
|
||||||
|
total = 0
|
||||||
|
while True:
|
||||||
|
chunk = conn.recv(4096)
|
||||||
|
if not chunk:
|
||||||
|
break
|
||||||
|
chunks.append(chunk)
|
||||||
|
total += len(chunk)
|
||||||
|
if total > self.args.max_payload_bytes:
|
||||||
|
raise ValueError(f"payload 过大: {total} > {self.args.max_payload_bytes}")
|
||||||
|
if not chunks:
|
||||||
|
return
|
||||||
|
payload = json.loads(b"".join(chunks).decode("utf-8"))
|
||||||
|
self.publish_payload(payload)
|
||||||
|
if self.args.send_ack:
|
||||||
|
conn.sendall(json.dumps({"success": True, "timestamp_us": now_us()}).encode("utf-8"))
|
||||||
|
|
||||||
|
|
||||||
|
def parse_args() -> argparse.Namespace:
|
||||||
|
parser = argparse.ArgumentParser(description="车间电脑侧 WiFi/TCP 底盘遥测 -> ROS2 /chassis/telemetry bridge")
|
||||||
|
parser.add_argument("--bind-host", default="0.0.0.0")
|
||||||
|
parser.add_argument("--bind-port", type=int, default=9010)
|
||||||
|
parser.add_argument("--output-topic", default="/chassis/telemetry")
|
||||||
|
parser.add_argument("--protocol", choices=["auto", "frame", "json_lines", "raw_json"], default="frame")
|
||||||
|
parser.add_argument("--expected-msg-type", type=int, default=0, help="0 表示接受任意帧类型;建议车端使用 7。")
|
||||||
|
parser.add_argument("--ack-msg-type", type=int, default=CHASSIS_TELEMETRY_PUSH_RSP)
|
||||||
|
parser.add_argument("--send-ack", action=argparse.BooleanOptionalAction, default=True)
|
||||||
|
parser.add_argument("--default-chassis-type", choices=sorted(CHASSIS_TYPE_BY_NAME), default="ackermann")
|
||||||
|
parser.add_argument("--max-payload-bytes", type=int, default=262144)
|
||||||
|
parser.add_argument("--connection-timeout-sec", type=float, default=30.0)
|
||||||
|
parser.add_argument("--stats-log-interval-sec", type=float, default=10.0)
|
||||||
|
return parser.parse_args()
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> int:
|
||||||
|
args = parse_args()
|
||||||
|
if rclpy is None:
|
||||||
|
print("[错误] 缺少 ROS 2 Python 依赖,无法启动底盘遥测 bridge。")
|
||||||
|
return 1
|
||||||
|
rclpy.init(args=None)
|
||||||
|
bridge = None
|
||||||
|
try:
|
||||||
|
bridge = WorkshopChassisTelemetryBridge(args)
|
||||||
|
bridge.start()
|
||||||
|
while rclpy.ok():
|
||||||
|
rclpy.spin_once(bridge.node, timeout_sec=0.2)
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
pass
|
||||||
|
finally:
|
||||||
|
if bridge is not None:
|
||||||
|
bridge.shutdown()
|
||||||
|
bridge.node.destroy_node()
|
||||||
|
if rclpy.ok():
|
||||||
|
rclpy.shutdown()
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
raise SystemExit(main())
|
||||||
@@ -95,7 +95,15 @@ python3 src/site_deployment/workshop_control_calibration_real/capture_control_se
|
|||||||
src/site_deployment/workshop_control_calibration_real/config/control_evaluation_profile.yaml
|
src/site_deployment/workshop_control_calibration_real/config/control_evaluation_profile.yaml
|
||||||
```
|
```
|
||||||
|
|
||||||
该文件按 `ackermann`、`differential`、`single_steer_wheel`、`multi_steer_wheel` 拆分,固定每类底盘建议跑的轨迹跟踪、速度阶跃、加减速和停车精度任务。每个任务同时声明 `control_axis`、`controller_algorithm`、`control_role` 和必要的外部真值质量门限。
|
该文件按 `ackermann`、`differential`、`single_steer_wheel`、`multi_steer_wheel` 拆分,固定每类底盘建议跑的轨迹跟踪、速度阶跃、加减速和停车精度任务。每个任务同时声明 `control_axis`、`controller_algorithm`、`control_role`、`reference_path_id` 和必要的外部真值质量门限。
|
||||||
|
|
||||||
|
运控参考路径单独放在:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
src/site_deployment/workshop_control_calibration_real/reference_paths/
|
||||||
|
```
|
||||||
|
|
||||||
|
每条路径一个 YAML 文件,必须配置 `path_id` 和中文 `display_name`。导出 `requested_tasks` 时,`reference_path_id` 会被展开成 `reference_path.*` metadata;轨迹跟踪任务还会同步展开为现有 `traj_pt_*` 字段,供运控执行链路读取。
|
||||||
|
|
||||||
校验并查看摘要:
|
校验并查看摘要:
|
||||||
|
|
||||||
|
|||||||
+17
-120
@@ -4,6 +4,7 @@
|
|||||||
|
|
||||||
schema_version: 1
|
schema_version: 1
|
||||||
profile_name: workshop_control_evaluation_profile
|
profile_name: workshop_control_evaluation_profile
|
||||||
|
reference_path_dir: ../reference_paths
|
||||||
|
|
||||||
safety:
|
safety:
|
||||||
max_linear_speed_ms: 0.2
|
max_linear_speed_ms: 0.2
|
||||||
@@ -34,6 +35,7 @@ chassis_profiles:
|
|||||||
- task_code: control.ackermann.path_tracking_s_curve
|
- task_code: control.ackermann.path_tracking_s_curve
|
||||||
display_name: 阿克曼低速 S 形轨迹跟踪
|
display_name: 阿克曼低速 S 形轨迹跟踪
|
||||||
selected_task: trajectory_tracking
|
selected_task: trajectory_tracking
|
||||||
|
reference_path_id: control_s_curve_low_speed
|
||||||
control_axis: combined
|
control_axis: combined
|
||||||
controller_algorithm: pure_pursuit
|
controller_algorithm: pure_pursuit
|
||||||
control_role: path_tracking_outer_loop
|
control_role: path_tracking_outer_loop
|
||||||
@@ -47,26 +49,10 @@ chassis_profiles:
|
|||||||
segment_index: 0
|
segment_index: 0
|
||||||
total_segments: 1
|
total_segments: 1
|
||||||
is_final_segment: true
|
is_final_segment: true
|
||||||
path:
|
|
||||||
- x_m: 0.0
|
|
||||||
y_m: 0.0
|
|
||||||
yaw_rad: 0.0
|
|
||||||
target_speed_ms: 0.10
|
|
||||||
- x_m: 0.6
|
|
||||||
y_m: 0.10
|
|
||||||
yaw_rad: 0.12
|
|
||||||
target_speed_ms: 0.12
|
|
||||||
- x_m: 1.2
|
|
||||||
y_m: -0.10
|
|
||||||
yaw_rad: -0.12
|
|
||||||
target_speed_ms: 0.12
|
|
||||||
- x_m: 1.8
|
|
||||||
y_m: 0.0
|
|
||||||
yaw_rad: 0.0
|
|
||||||
target_speed_ms: 0.10
|
|
||||||
- task_code: control.ackermann.speed_step_low
|
- task_code: control.ackermann.speed_step_low
|
||||||
display_name: 阿克曼低速速度阶跃
|
display_name: 阿克曼低速速度阶跃
|
||||||
selected_task: velocity_step
|
selected_task: velocity_step
|
||||||
|
reference_path_id: control_line_low_speed
|
||||||
control_axis: longitudinal_control
|
control_axis: longitudinal_control
|
||||||
controller_algorithm: pid
|
controller_algorithm: pid
|
||||||
control_role: speed_loop
|
control_role: speed_loop
|
||||||
@@ -78,6 +64,7 @@ chassis_profiles:
|
|||||||
- task_code: control.ackermann.steering_response_arc
|
- task_code: control.ackermann.steering_response_arc
|
||||||
display_name: 阿克曼转角响应轨迹
|
display_name: 阿克曼转角响应轨迹
|
||||||
selected_task: trajectory_tracking
|
selected_task: trajectory_tracking
|
||||||
|
reference_path_id: control_arc_low_speed
|
||||||
control_axis: lateral_control
|
control_axis: lateral_control
|
||||||
controller_algorithm: pid
|
controller_algorithm: pid
|
||||||
control_role: steering_angle_inner_loop
|
control_role: steering_angle_inner_loop
|
||||||
@@ -89,22 +76,10 @@ chassis_profiles:
|
|||||||
required_external_pose_source_id: workshop_external_localization
|
required_external_pose_source_id: workshop_external_localization
|
||||||
max_external_pose_age_ms: 100.0
|
max_external_pose_age_ms: 100.0
|
||||||
min_external_pose_quality_score: 0.7
|
min_external_pose_quality_score: 0.7
|
||||||
path:
|
|
||||||
- x_m: 0.0
|
|
||||||
y_m: 0.0
|
|
||||||
yaw_rad: 0.0
|
|
||||||
target_speed_ms: 0.08
|
|
||||||
- x_m: 0.5
|
|
||||||
y_m: 0.08
|
|
||||||
yaw_rad: 0.15
|
|
||||||
target_speed_ms: 0.08
|
|
||||||
- x_m: 1.0
|
|
||||||
y_m: 0.28
|
|
||||||
yaw_rad: 0.30
|
|
||||||
target_speed_ms: 0.08
|
|
||||||
- task_code: control.ackermann.stop_accuracy
|
- task_code: control.ackermann.stop_accuracy
|
||||||
display_name: 阿克曼停车精度
|
display_name: 阿克曼停车精度
|
||||||
selected_task: stop_accuracy
|
selected_task: stop_accuracy
|
||||||
|
reference_path_id: control_stop_accuracy_1_2m
|
||||||
control_axis: longitudinal_control
|
control_axis: longitudinal_control
|
||||||
controller_algorithm: pid
|
controller_algorithm: pid
|
||||||
control_role: speed_loop
|
control_role: speed_loop
|
||||||
@@ -126,6 +101,7 @@ chassis_profiles:
|
|||||||
- task_code: control.differential.path_tracking_line
|
- task_code: control.differential.path_tracking_line
|
||||||
display_name: 差速低速直线轨迹跟踪
|
display_name: 差速低速直线轨迹跟踪
|
||||||
selected_task: trajectory_tracking
|
selected_task: trajectory_tracking
|
||||||
|
reference_path_id: control_line_low_speed
|
||||||
control_axis: combined
|
control_axis: combined
|
||||||
controller_algorithm: pure_pursuit
|
controller_algorithm: pure_pursuit
|
||||||
control_role: path_tracking_outer_loop
|
control_role: path_tracking_outer_loop
|
||||||
@@ -136,22 +112,10 @@ chassis_profiles:
|
|||||||
required_external_pose_source_id: workshop_external_localization
|
required_external_pose_source_id: workshop_external_localization
|
||||||
max_external_pose_age_ms: 100.0
|
max_external_pose_age_ms: 100.0
|
||||||
min_external_pose_quality_score: 0.7
|
min_external_pose_quality_score: 0.7
|
||||||
path:
|
|
||||||
- x_m: 0.0
|
|
||||||
y_m: 0.0
|
|
||||||
yaw_rad: 0.0
|
|
||||||
target_speed_ms: 0.10
|
|
||||||
- x_m: 0.8
|
|
||||||
y_m: 0.0
|
|
||||||
yaw_rad: 0.0
|
|
||||||
target_speed_ms: 0.12
|
|
||||||
- x_m: 1.6
|
|
||||||
y_m: 0.0
|
|
||||||
yaw_rad: 0.0
|
|
||||||
target_speed_ms: 0.10
|
|
||||||
- task_code: control.differential.speed_step_low
|
- task_code: control.differential.speed_step_low
|
||||||
display_name: 差速低速速度阶跃
|
display_name: 差速低速速度阶跃
|
||||||
selected_task: velocity_step
|
selected_task: velocity_step
|
||||||
|
reference_path_id: control_line_low_speed
|
||||||
control_axis: longitudinal_control
|
control_axis: longitudinal_control
|
||||||
controller_algorithm: pid
|
controller_algorithm: pid
|
||||||
control_role: speed_loop
|
control_role: speed_loop
|
||||||
@@ -163,6 +127,7 @@ chassis_profiles:
|
|||||||
- task_code: control.differential.yaw_rate_arc
|
- task_code: control.differential.yaw_rate_arc
|
||||||
display_name: 差速角速度响应圆弧
|
display_name: 差速角速度响应圆弧
|
||||||
selected_task: trajectory_tracking
|
selected_task: trajectory_tracking
|
||||||
|
reference_path_id: control_arc_low_speed
|
||||||
control_axis: lateral_control
|
control_axis: lateral_control
|
||||||
controller_algorithm: pid
|
controller_algorithm: pid
|
||||||
control_role: yaw_rate_loop
|
control_role: yaw_rate_loop
|
||||||
@@ -174,22 +139,10 @@ chassis_profiles:
|
|||||||
required_external_pose_source_id: workshop_external_localization
|
required_external_pose_source_id: workshop_external_localization
|
||||||
max_external_pose_age_ms: 100.0
|
max_external_pose_age_ms: 100.0
|
||||||
min_external_pose_quality_score: 0.7
|
min_external_pose_quality_score: 0.7
|
||||||
path:
|
|
||||||
- x_m: 0.0
|
|
||||||
y_m: 0.0
|
|
||||||
yaw_rad: 0.0
|
|
||||||
target_speed_ms: 0.08
|
|
||||||
- x_m: 0.45
|
|
||||||
y_m: 0.08
|
|
||||||
yaw_rad: 0.18
|
|
||||||
target_speed_ms: 0.08
|
|
||||||
- x_m: 0.85
|
|
||||||
y_m: 0.30
|
|
||||||
yaw_rad: 0.36
|
|
||||||
target_speed_ms: 0.08
|
|
||||||
- task_code: control.differential.accel_decel_low
|
- task_code: control.differential.accel_decel_low
|
||||||
display_name: 差速低速加减速响应
|
display_name: 差速低速加减速响应
|
||||||
selected_task: acceleration_deceleration
|
selected_task: acceleration_deceleration
|
||||||
|
reference_path_id: control_line_low_speed
|
||||||
control_axis: longitudinal_control
|
control_axis: longitudinal_control
|
||||||
controller_algorithm: pid
|
controller_algorithm: pid
|
||||||
control_role: acceleration_loop
|
control_role: acceleration_loop
|
||||||
@@ -211,6 +164,7 @@ chassis_profiles:
|
|||||||
- task_code: control.single_steer.path_tracking_arc
|
- task_code: control.single_steer.path_tracking_arc
|
||||||
display_name: 单舵轮低速圆弧轨迹跟踪
|
display_name: 单舵轮低速圆弧轨迹跟踪
|
||||||
selected_task: trajectory_tracking
|
selected_task: trajectory_tracking
|
||||||
|
reference_path_id: control_arc_low_speed
|
||||||
control_axis: combined
|
control_axis: combined
|
||||||
controller_algorithm: pure_pursuit
|
controller_algorithm: pure_pursuit
|
||||||
control_role: path_tracking_outer_loop
|
control_role: path_tracking_outer_loop
|
||||||
@@ -221,22 +175,10 @@ chassis_profiles:
|
|||||||
required_external_pose_source_id: workshop_external_localization
|
required_external_pose_source_id: workshop_external_localization
|
||||||
max_external_pose_age_ms: 100.0
|
max_external_pose_age_ms: 100.0
|
||||||
min_external_pose_quality_score: 0.7
|
min_external_pose_quality_score: 0.7
|
||||||
path:
|
|
||||||
- x_m: 0.0
|
|
||||||
y_m: 0.0
|
|
||||||
yaw_rad: 0.0
|
|
||||||
target_speed_ms: 0.09
|
|
||||||
- x_m: 0.55
|
|
||||||
y_m: 0.08
|
|
||||||
yaw_rad: 0.12
|
|
||||||
target_speed_ms: 0.10
|
|
||||||
- x_m: 1.10
|
|
||||||
y_m: 0.28
|
|
||||||
yaw_rad: 0.24
|
|
||||||
target_speed_ms: 0.09
|
|
||||||
- task_code: control.single_steer.speed_step_low
|
- task_code: control.single_steer.speed_step_low
|
||||||
display_name: 单舵轮低速速度阶跃
|
display_name: 单舵轮低速速度阶跃
|
||||||
selected_task: velocity_step
|
selected_task: velocity_step
|
||||||
|
reference_path_id: control_line_low_speed
|
||||||
control_axis: longitudinal_control
|
control_axis: longitudinal_control
|
||||||
controller_algorithm: pid
|
controller_algorithm: pid
|
||||||
control_role: speed_loop
|
control_role: speed_loop
|
||||||
@@ -248,6 +190,7 @@ chassis_profiles:
|
|||||||
- task_code: control.single_steer.steering_response_s_curve
|
- task_code: control.single_steer.steering_response_s_curve
|
||||||
display_name: 单舵轮舵角响应 S 形轨迹
|
display_name: 单舵轮舵角响应 S 形轨迹
|
||||||
selected_task: trajectory_tracking
|
selected_task: trajectory_tracking
|
||||||
|
reference_path_id: control_s_curve_low_speed
|
||||||
control_axis: lateral_control
|
control_axis: lateral_control
|
||||||
controller_algorithm: pid
|
controller_algorithm: pid
|
||||||
control_role: steering_angle_inner_loop
|
control_role: steering_angle_inner_loop
|
||||||
@@ -259,26 +202,10 @@ chassis_profiles:
|
|||||||
required_external_pose_source_id: workshop_external_localization
|
required_external_pose_source_id: workshop_external_localization
|
||||||
max_external_pose_age_ms: 100.0
|
max_external_pose_age_ms: 100.0
|
||||||
min_external_pose_quality_score: 0.7
|
min_external_pose_quality_score: 0.7
|
||||||
path:
|
|
||||||
- x_m: 0.0
|
|
||||||
y_m: 0.0
|
|
||||||
yaw_rad: 0.0
|
|
||||||
target_speed_ms: 0.08
|
|
||||||
- x_m: 0.5
|
|
||||||
y_m: 0.10
|
|
||||||
yaw_rad: 0.12
|
|
||||||
target_speed_ms: 0.08
|
|
||||||
- x_m: 1.0
|
|
||||||
y_m: -0.10
|
|
||||||
yaw_rad: -0.12
|
|
||||||
target_speed_ms: 0.08
|
|
||||||
- x_m: 1.5
|
|
||||||
y_m: 0.0
|
|
||||||
yaw_rad: 0.0
|
|
||||||
target_speed_ms: 0.08
|
|
||||||
- task_code: control.single_steer.stop_accuracy
|
- task_code: control.single_steer.stop_accuracy
|
||||||
display_name: 单舵轮停车精度
|
display_name: 单舵轮停车精度
|
||||||
selected_task: stop_accuracy
|
selected_task: stop_accuracy
|
||||||
|
reference_path_id: control_stop_accuracy_1_2m
|
||||||
control_axis: longitudinal_control
|
control_axis: longitudinal_control
|
||||||
controller_algorithm: pid
|
controller_algorithm: pid
|
||||||
control_role: speed_loop
|
control_role: speed_loop
|
||||||
@@ -300,6 +227,7 @@ chassis_profiles:
|
|||||||
- task_code: control.multi_steer.path_tracking_lateral_offset
|
- task_code: control.multi_steer.path_tracking_lateral_offset
|
||||||
display_name: 多舵轮横向偏移轨迹跟踪
|
display_name: 多舵轮横向偏移轨迹跟踪
|
||||||
selected_task: trajectory_tracking
|
selected_task: trajectory_tracking
|
||||||
|
reference_path_id: control_lateral_offset_low_speed
|
||||||
control_axis: combined
|
control_axis: combined
|
||||||
controller_algorithm: mpc
|
controller_algorithm: mpc
|
||||||
control_role: path_tracking_outer_loop
|
control_role: path_tracking_outer_loop
|
||||||
@@ -310,26 +238,10 @@ chassis_profiles:
|
|||||||
required_external_pose_source_id: workshop_external_localization
|
required_external_pose_source_id: workshop_external_localization
|
||||||
max_external_pose_age_ms: 100.0
|
max_external_pose_age_ms: 100.0
|
||||||
min_external_pose_quality_score: 0.7
|
min_external_pose_quality_score: 0.7
|
||||||
path:
|
|
||||||
- x_m: 0.0
|
|
||||||
y_m: 0.0
|
|
||||||
yaw_rad: 0.0
|
|
||||||
target_speed_ms: 0.08
|
|
||||||
- x_m: 0.4
|
|
||||||
y_m: 0.2
|
|
||||||
yaw_rad: 0.0
|
|
||||||
target_speed_ms: 0.08
|
|
||||||
- x_m: 0.8
|
|
||||||
y_m: 0.2
|
|
||||||
yaw_rad: 0.0
|
|
||||||
target_speed_ms: 0.08
|
|
||||||
- x_m: 1.2
|
|
||||||
y_m: 0.0
|
|
||||||
yaw_rad: 0.0
|
|
||||||
target_speed_ms: 0.08
|
|
||||||
- task_code: control.multi_steer.wheel_speed_step_low
|
- task_code: control.multi_steer.wheel_speed_step_low
|
||||||
display_name: 多舵轮轮速阶跃
|
display_name: 多舵轮轮速阶跃
|
||||||
selected_task: velocity_step
|
selected_task: velocity_step
|
||||||
|
reference_path_id: control_line_low_speed
|
||||||
control_axis: longitudinal_control
|
control_axis: longitudinal_control
|
||||||
controller_algorithm: pid
|
controller_algorithm: pid
|
||||||
control_role: wheel_speed_inner_loop
|
control_role: wheel_speed_inner_loop
|
||||||
@@ -341,6 +253,7 @@ chassis_profiles:
|
|||||||
- task_code: control.multi_steer.module_steering_response
|
- task_code: control.multi_steer.module_steering_response
|
||||||
display_name: 多舵轮模块转角响应轨迹
|
display_name: 多舵轮模块转角响应轨迹
|
||||||
selected_task: trajectory_tracking
|
selected_task: trajectory_tracking
|
||||||
|
reference_path_id: control_lateral_offset_low_speed
|
||||||
control_axis: lateral_control
|
control_axis: lateral_control
|
||||||
controller_algorithm: pid
|
controller_algorithm: pid
|
||||||
control_role: module_steering_inner_loop
|
control_role: module_steering_inner_loop
|
||||||
@@ -352,26 +265,10 @@ chassis_profiles:
|
|||||||
required_external_pose_source_id: workshop_external_localization
|
required_external_pose_source_id: workshop_external_localization
|
||||||
max_external_pose_age_ms: 100.0
|
max_external_pose_age_ms: 100.0
|
||||||
min_external_pose_quality_score: 0.7
|
min_external_pose_quality_score: 0.7
|
||||||
path:
|
|
||||||
- x_m: 0.0
|
|
||||||
y_m: 0.0
|
|
||||||
yaw_rad: 0.0
|
|
||||||
target_speed_ms: 0.06
|
|
||||||
- x_m: 0.3
|
|
||||||
y_m: 0.18
|
|
||||||
yaw_rad: 0.0
|
|
||||||
target_speed_ms: 0.06
|
|
||||||
- x_m: 0.6
|
|
||||||
y_m: -0.18
|
|
||||||
yaw_rad: 0.0
|
|
||||||
target_speed_ms: 0.06
|
|
||||||
- x_m: 0.9
|
|
||||||
y_m: 0.0
|
|
||||||
yaw_rad: 0.0
|
|
||||||
target_speed_ms: 0.06
|
|
||||||
- task_code: control.multi_steer.stop_accuracy
|
- task_code: control.multi_steer.stop_accuracy
|
||||||
display_name: 多舵轮停车精度
|
display_name: 多舵轮停车精度
|
||||||
selected_task: stop_accuracy
|
selected_task: stop_accuracy
|
||||||
|
reference_path_id: control_stop_accuracy_1_2m
|
||||||
control_axis: longitudinal_control
|
control_axis: longitudinal_control
|
||||||
controller_algorithm: pid
|
controller_algorithm: pid
|
||||||
control_role: wheel_speed_inner_loop
|
control_role: wheel_speed_inner_loop
|
||||||
|
|||||||
+53
-9
@@ -16,6 +16,12 @@ except ImportError as exc:
|
|||||||
SCRIPT_DIR = Path(__file__).resolve().parent
|
SCRIPT_DIR = Path(__file__).resolve().parent
|
||||||
if str(SCRIPT_DIR) not in sys.path:
|
if str(SCRIPT_DIR) not in sys.path:
|
||||||
sys.path.insert(0, str(SCRIPT_DIR))
|
sys.path.insert(0, str(SCRIPT_DIR))
|
||||||
|
REFERENCE_PATH_TOOL_DIR = SCRIPT_DIR.parents[0] / "workshop_reference_paths"
|
||||||
|
if str(REFERENCE_PATH_TOOL_DIR) not in sys.path:
|
||||||
|
sys.path.insert(0, str(REFERENCE_PATH_TOOL_DIR))
|
||||||
|
|
||||||
|
from calibration_reference_paths import path_points_as_trajectory # noqa: E402
|
||||||
|
from calibration_reference_paths import selected_path_metadata # noqa: E402
|
||||||
|
|
||||||
from stage_control_parameter_commit import (
|
from stage_control_parameter_commit import (
|
||||||
CHASSIS_TYPES,
|
CHASSIS_TYPES,
|
||||||
@@ -204,9 +210,15 @@ def validate_trajectory_payload(
|
|||||||
payload: dict[str, Any],
|
payload: dict[str, Any],
|
||||||
action_name: str,
|
action_name: str,
|
||||||
safety: dict[str, float],
|
safety: dict[str, float],
|
||||||
|
allow_reference_path: bool = False,
|
||||||
) -> None:
|
) -> None:
|
||||||
validate_positive(payload.get("timeout_sec"), f"{action_name}.trajectory_tracking.timeout_sec")
|
validate_positive(payload.get("timeout_sec"), f"{action_name}.trajectory_tracking.timeout_sec")
|
||||||
path = require_list(payload.get("path"), f"{action_name}.trajectory_tracking.path")
|
raw_path = payload.get("path")
|
||||||
|
if raw_path in (None, ""):
|
||||||
|
if not allow_reference_path:
|
||||||
|
raise ValueError(f"{action_name}.trajectory_tracking.path 不能为空,或在动作上配置 reference_path_id。")
|
||||||
|
else:
|
||||||
|
path = require_list(raw_path, f"{action_name}.trajectory_tracking.path")
|
||||||
if len(path) < 2:
|
if len(path) < 2:
|
||||||
raise ValueError(f"{action_name}.trajectory_tracking.path 至少需要 2 个轨迹点。")
|
raise ValueError(f"{action_name}.trajectory_tracking.path 至少需要 2 个轨迹点。")
|
||||||
|
|
||||||
@@ -289,8 +301,15 @@ def validate_action(
|
|||||||
|
|
||||||
payload_key = TASK_PAYLOAD_KEYS[selected_task]
|
payload_key = TASK_PAYLOAD_KEYS[selected_task]
|
||||||
payload = require_map(action.get(payload_key), f"{action_name}.{payload_key}")
|
payload = require_map(action.get(payload_key), f"{action_name}.{payload_key}")
|
||||||
|
if action.get("reference_path_id") not in (None, ""):
|
||||||
|
require_string(action.get("reference_path_id"), f"{action_name}.reference_path_id")
|
||||||
if selected_task == "trajectory_tracking":
|
if selected_task == "trajectory_tracking":
|
||||||
validate_trajectory_payload(payload, action_name, safety)
|
validate_trajectory_payload(
|
||||||
|
payload,
|
||||||
|
action_name,
|
||||||
|
safety,
|
||||||
|
allow_reference_path=action.get("reference_path_id") not in (None, ""),
|
||||||
|
)
|
||||||
elif selected_task == "velocity_step":
|
elif selected_task == "velocity_step":
|
||||||
validate_velocity_step_payload(payload, action_name, safety)
|
validate_velocity_step_payload(payload, action_name, safety)
|
||||||
elif selected_task in {"acceleration_deceleration", "accel_decel"}:
|
elif selected_task in {"acceleration_deceleration", "accel_decel"}:
|
||||||
@@ -349,7 +368,11 @@ def add_if_present(metadata: dict[str, Any], key: str, payload: dict[str, Any],
|
|||||||
metadata[key] = payload[source_key]
|
metadata[key] = payload[source_key]
|
||||||
|
|
||||||
|
|
||||||
def flatten_trajectory_payload(metadata: dict[str, Any], payload: dict[str, Any]) -> None:
|
def flatten_trajectory_payload(
|
||||||
|
metadata: dict[str, Any],
|
||||||
|
payload: dict[str, Any],
|
||||||
|
selected_path_points: list[dict[str, float]] | None = None,
|
||||||
|
) -> None:
|
||||||
metadata["control.stop_at_end"] = payload.get("stop_at_end", True)
|
metadata["control.stop_at_end"] = payload.get("stop_at_end", True)
|
||||||
metadata["control.timeout_sec"] = payload["timeout_sec"]
|
metadata["control.timeout_sec"] = payload["timeout_sec"]
|
||||||
add_if_present(metadata, "trajectory_tracking.required_external_pose_source_id", payload, "required_external_pose_source_id")
|
add_if_present(metadata, "trajectory_tracking.required_external_pose_source_id", payload, "required_external_pose_source_id")
|
||||||
@@ -360,17 +383,30 @@ def flatten_trajectory_payload(metadata: dict[str, Any], payload: dict[str, Any]
|
|||||||
add_if_present(metadata, "trajectory_tracking.total_segments", payload, "total_segments")
|
add_if_present(metadata, "trajectory_tracking.total_segments", payload, "total_segments")
|
||||||
add_if_present(metadata, "trajectory_tracking.is_final_segment", payload, "is_final_segment")
|
add_if_present(metadata, "trajectory_tracking.is_final_segment", payload, "is_final_segment")
|
||||||
|
|
||||||
for index, point in enumerate(payload["path"]):
|
path = selected_path_points if selected_path_points is not None else payload["path"]
|
||||||
|
for index, point in enumerate(path):
|
||||||
metadata[f"traj_pt_{index}_x_m"] = point["x_m"]
|
metadata[f"traj_pt_{index}_x_m"] = point["x_m"]
|
||||||
metadata[f"traj_pt_{index}_y_m"] = point["y_m"]
|
metadata[f"traj_pt_{index}_y_m"] = point["y_m"]
|
||||||
metadata[f"traj_pt_{index}_yaw_rad"] = point["yaw_rad"]
|
metadata[f"traj_pt_{index}_yaw_rad"] = point["yaw_rad"]
|
||||||
metadata[f"traj_pt_{index}_speed_ms"] = point["target_speed_ms"]
|
metadata[f"traj_pt_{index}_speed_ms"] = point["target_speed_ms"]
|
||||||
|
|
||||||
|
|
||||||
def flatten_action_metadata(action: dict[str, Any], chassis_type: str) -> dict[str, Any]:
|
def flatten_action_metadata(
|
||||||
|
action: dict[str, Any],
|
||||||
|
chassis_type: str,
|
||||||
|
profile: dict[str, Any],
|
||||||
|
profile_path: Path | None = None,
|
||||||
|
) -> dict[str, Any]:
|
||||||
selected_task = action["selected_task"]
|
selected_task = action["selected_task"]
|
||||||
payload_key = TASK_PAYLOAD_KEYS[selected_task]
|
payload_key = TASK_PAYLOAD_KEYS[selected_task]
|
||||||
payload = action[payload_key]
|
payload = action[payload_key]
|
||||||
|
reference_metadata, selected_path = selected_path_metadata(
|
||||||
|
profile.get("reference_path_dir", "reference_paths"),
|
||||||
|
profile_path,
|
||||||
|
str(action.get("reference_path_id", "")),
|
||||||
|
chassis_type,
|
||||||
|
TASK_TYPE_TO_METADATA[selected_task],
|
||||||
|
)
|
||||||
metadata: dict[str, Any] = {
|
metadata: dict[str, Any] = {
|
||||||
"control.task_type": TASK_TYPE_TO_METADATA[selected_task],
|
"control.task_type": TASK_TYPE_TO_METADATA[selected_task],
|
||||||
"control.chassis_type": chassis_type,
|
"control.chassis_type": chassis_type,
|
||||||
@@ -379,9 +415,13 @@ def flatten_action_metadata(action: dict[str, Any], chassis_type: str) -> dict[s
|
|||||||
"control.role": action["control_role"],
|
"control.role": action["control_role"],
|
||||||
}
|
}
|
||||||
add_if_present(metadata, "control.loop_name", action, "loop_name")
|
add_if_present(metadata, "control.loop_name", action, "loop_name")
|
||||||
|
metadata.update(reference_metadata)
|
||||||
|
|
||||||
if selected_task == "trajectory_tracking":
|
if selected_task == "trajectory_tracking":
|
||||||
flatten_trajectory_payload(metadata, payload)
|
selected_points = path_points_as_trajectory(selected_path)
|
||||||
|
if len(selected_points) < 2:
|
||||||
|
raise ValueError(f"{action['task_code']} 选择的 reference_path 至少需要 2 个点。")
|
||||||
|
flatten_trajectory_payload(metadata, payload, selected_points)
|
||||||
elif selected_task == "velocity_step":
|
elif selected_task == "velocity_step":
|
||||||
metadata["velocity_step.target_velocity_ms"] = payload["target_velocity_ms"]
|
metadata["velocity_step.target_velocity_ms"] = payload["target_velocity_ms"]
|
||||||
metadata["velocity_step.hold_time_sec"] = payload["hold_time_sec"]
|
metadata["velocity_step.hold_time_sec"] = payload["hold_time_sec"]
|
||||||
@@ -399,11 +439,15 @@ def flatten_action_metadata(action: dict[str, Any], chassis_type: str) -> dict[s
|
|||||||
return metadata
|
return metadata
|
||||||
|
|
||||||
|
|
||||||
def export_requested_tasks(profile: dict[str, Any], chassis_type: str) -> dict[str, Any]:
|
def export_requested_tasks(
|
||||||
|
profile: dict[str, Any],
|
||||||
|
chassis_type: str,
|
||||||
|
profile_path: Path | None = None,
|
||||||
|
) -> dict[str, Any]:
|
||||||
section = profile["chassis_profiles"][chassis_type]
|
section = profile["chassis_profiles"][chassis_type]
|
||||||
tasks: list[dict[str, Any]] = []
|
tasks: list[dict[str, Any]] = []
|
||||||
for action in section["actions"]:
|
for action in section["actions"]:
|
||||||
metadata = flatten_action_metadata(action, chassis_type)
|
metadata = flatten_action_metadata(action, chassis_type, profile, profile_path)
|
||||||
task_params = [make_task_param(key, metadata[key]) for key in sorted(metadata)]
|
task_params = [make_task_param(key, metadata[key]) for key in sorted(metadata)]
|
||||||
tasks.append({
|
tasks.append({
|
||||||
"stage_type": "CONTROL_CALIBRATION_STAGE",
|
"stage_type": "CONTROL_CALIBRATION_STAGE",
|
||||||
@@ -458,7 +502,7 @@ def main() -> int:
|
|||||||
if args.format == "requested_tasks":
|
if args.format == "requested_tasks":
|
||||||
if not args.chassis_type:
|
if not args.chassis_type:
|
||||||
raise ValueError("--format requested_tasks 必须指定 --chassis-type。")
|
raise ValueError("--format requested_tasks 必须指定 --chassis-type。")
|
||||||
output = export_requested_tasks(profile, args.chassis_type)
|
output = export_requested_tasks(profile, args.chassis_type, profile_path)
|
||||||
else:
|
else:
|
||||||
output = build_summary(profile)
|
output = build_summary(profile)
|
||||||
|
|
||||||
|
|||||||
+21
@@ -0,0 +1,21 @@
|
|||||||
|
schema_version: 1
|
||||||
|
path_id: control_arc_low_speed
|
||||||
|
display_name: 运控低速圆弧跟踪
|
||||||
|
module_type: control
|
||||||
|
frame_id: workshop
|
||||||
|
path_type: arc
|
||||||
|
recommended_task_types:
|
||||||
|
- trajectory_tracking
|
||||||
|
points:
|
||||||
|
- x_m: 0.0
|
||||||
|
y_m: 0.0
|
||||||
|
yaw_rad: 0.0
|
||||||
|
target_speed_ms: 0.08
|
||||||
|
- x_m: 0.5
|
||||||
|
y_m: 0.08
|
||||||
|
yaw_rad: 0.15
|
||||||
|
target_speed_ms: 0.08
|
||||||
|
- x_m: 1.0
|
||||||
|
y_m: 0.28
|
||||||
|
yaw_rad: 0.30
|
||||||
|
target_speed_ms: 0.08
|
||||||
+25
@@ -0,0 +1,25 @@
|
|||||||
|
schema_version: 1
|
||||||
|
path_id: control_lateral_offset_low_speed
|
||||||
|
display_name: 运控横向偏移低速跟踪
|
||||||
|
module_type: control
|
||||||
|
frame_id: workshop
|
||||||
|
path_type: lateral_offset
|
||||||
|
recommended_task_types:
|
||||||
|
- trajectory_tracking
|
||||||
|
points:
|
||||||
|
- x_m: 0.0
|
||||||
|
y_m: 0.0
|
||||||
|
yaw_rad: 0.0
|
||||||
|
target_speed_ms: 0.08
|
||||||
|
- x_m: 0.4
|
||||||
|
y_m: 0.2
|
||||||
|
yaw_rad: 0.0
|
||||||
|
target_speed_ms: 0.08
|
||||||
|
- x_m: 0.8
|
||||||
|
y_m: 0.2
|
||||||
|
yaw_rad: 0.0
|
||||||
|
target_speed_ms: 0.08
|
||||||
|
- x_m: 1.2
|
||||||
|
y_m: 0.0
|
||||||
|
yaw_rad: 0.0
|
||||||
|
target_speed_ms: 0.08
|
||||||
+23
@@ -0,0 +1,23 @@
|
|||||||
|
schema_version: 1
|
||||||
|
path_id: control_line_low_speed
|
||||||
|
display_name: 运控低速直线跟踪
|
||||||
|
module_type: control
|
||||||
|
frame_id: workshop
|
||||||
|
path_type: straight_line
|
||||||
|
recommended_task_types:
|
||||||
|
- trajectory_tracking
|
||||||
|
- velocity_step
|
||||||
|
- accel_decel
|
||||||
|
points:
|
||||||
|
- x_m: 0.0
|
||||||
|
y_m: 0.0
|
||||||
|
yaw_rad: 0.0
|
||||||
|
target_speed_ms: 0.10
|
||||||
|
- x_m: 0.8
|
||||||
|
y_m: 0.0
|
||||||
|
yaw_rad: 0.0
|
||||||
|
target_speed_ms: 0.12
|
||||||
|
- x_m: 1.6
|
||||||
|
y_m: 0.0
|
||||||
|
yaw_rad: 0.0
|
||||||
|
target_speed_ms: 0.10
|
||||||
+25
@@ -0,0 +1,25 @@
|
|||||||
|
schema_version: 1
|
||||||
|
path_id: control_s_curve_low_speed
|
||||||
|
display_name: 运控低速 S 形跟踪
|
||||||
|
module_type: control
|
||||||
|
frame_id: workshop
|
||||||
|
path_type: s_curve
|
||||||
|
recommended_task_types:
|
||||||
|
- trajectory_tracking
|
||||||
|
points:
|
||||||
|
- x_m: 0.0
|
||||||
|
y_m: 0.0
|
||||||
|
yaw_rad: 0.0
|
||||||
|
target_speed_ms: 0.10
|
||||||
|
- x_m: 0.6
|
||||||
|
y_m: 0.10
|
||||||
|
yaw_rad: 0.12
|
||||||
|
target_speed_ms: 0.12
|
||||||
|
- x_m: 1.2
|
||||||
|
y_m: -0.10
|
||||||
|
yaw_rad: -0.12
|
||||||
|
target_speed_ms: 0.12
|
||||||
|
- x_m: 1.8
|
||||||
|
y_m: 0.0
|
||||||
|
yaw_rad: 0.0
|
||||||
|
target_speed_ms: 0.10
|
||||||
+17
@@ -0,0 +1,17 @@
|
|||||||
|
schema_version: 1
|
||||||
|
path_id: control_stop_accuracy_1_2m
|
||||||
|
display_name: 运控停车精度 1.2m
|
||||||
|
module_type: control
|
||||||
|
frame_id: workshop
|
||||||
|
path_type: stop_accuracy
|
||||||
|
recommended_task_types:
|
||||||
|
- stop_accuracy
|
||||||
|
points:
|
||||||
|
- x_m: 0.0
|
||||||
|
y_m: 0.0
|
||||||
|
yaw_rad: 0.0
|
||||||
|
target_speed_ms: 0.10
|
||||||
|
- x_m: 1.2
|
||||||
|
y_m: 0.0
|
||||||
|
yaw_rad: 0.0
|
||||||
|
target_speed_ms: 0.0
|
||||||
+15
@@ -0,0 +1,15 @@
|
|||||||
|
profile_name: control_calibration_reference_paths
|
||||||
|
frame_id: workshop
|
||||||
|
|
||||||
|
default_path_id_by_chassis:
|
||||||
|
ackermann: control_s_curve_low_speed
|
||||||
|
differential: control_line_low_speed
|
||||||
|
single_steer_wheel: control_arc_low_speed
|
||||||
|
multi_steer_wheel: control_lateral_offset_low_speed
|
||||||
|
|
||||||
|
default_path_id_by_task_type:
|
||||||
|
trajectory_tracking: control_s_curve_low_speed
|
||||||
|
velocity_step: control_line_low_speed
|
||||||
|
accel_decel: control_line_low_speed
|
||||||
|
acceleration_deceleration: control_line_low_speed
|
||||||
|
stop_accuracy: control_stop_accuracy_1_2m
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
# 标定参考路径工具
|
||||||
|
|
||||||
|
三类标定任务都使用同一套路径 YAML 协议,但路径文件分别放在各自模块目录:
|
||||||
|
|
||||||
|
- `src/site_deployment/workshop_chassis_calibration_real/reference_paths/`
|
||||||
|
- `src/site_deployment/workshop_control_calibration_real/reference_paths/`
|
||||||
|
- `src/site_deployment/workshop_sensor_calibration_real/reference_paths/`
|
||||||
|
|
||||||
|
每条路径一个 YAML 文件,必须包含 `path_id` 和中文 `display_name`。`index.yaml` 只负责给 `chassis_type` 或任务类型配置默认路径,不存放路径点。
|
||||||
|
|
||||||
|
查看某个目录中的可选路径:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python3 src/site_deployment/workshop_reference_paths/calibration_reference_paths.py \
|
||||||
|
--path-dir src/site_deployment/workshop_control_calibration_real/reference_paths
|
||||||
|
```
|
||||||
|
|
||||||
|
导出一条路径的总控 metadata:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python3 src/site_deployment/workshop_reference_paths/calibration_reference_paths.py \
|
||||||
|
--path-dir src/site_deployment/workshop_control_calibration_real/reference_paths \
|
||||||
|
--path-id control_s_curve_low_speed \
|
||||||
|
--format metadata
|
||||||
|
```
|
||||||
|
|
||||||
|
从外部真值话题录制新路径:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python3 src/site_deployment/workshop_reference_paths/record_calibration_reference_path.py \
|
||||||
|
--output-dir src/site_deployment/workshop_control_calibration_real/reference_paths \
|
||||||
|
--path-id site_a_control_s_curve \
|
||||||
|
--display-name 现场A运控S形路径 \
|
||||||
|
--module-type control \
|
||||||
|
--source external_pose \
|
||||||
|
--duration-sec 20.0 \
|
||||||
|
--target-speed-ms 0.10 \
|
||||||
|
--recommended-task-type trajectory_tracking
|
||||||
|
```
|
||||||
|
|
||||||
|
也可以从已有 CSV 生成路径,CSV 支持 `x_m/y_m/yaw_rad` 或 `odom_x_m/odom_y_m/odom_yaw_rad`:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python3 src/site_deployment/workshop_reference_paths/record_calibration_reference_path.py \
|
||||||
|
--output-dir src/site_deployment/workshop_chassis_calibration_real/reference_paths \
|
||||||
|
--path-id site_a_chassis_line \
|
||||||
|
--display-name 现场A底盘直线路径 \
|
||||||
|
--module-type chassis \
|
||||||
|
--input-csv /data/agv_calib/site_a/session_001/external/truth_trajectory.csv \
|
||||||
|
--recommended-task-type straight_line
|
||||||
|
```
|
||||||
+377
@@ -0,0 +1,377 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""校验、选择和导出标定参考路径目录。"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
try:
|
||||||
|
import yaml
|
||||||
|
except ImportError as exc:
|
||||||
|
raise SystemExit("缺少 PyYAML,请先在当前 Python 环境安装 yaml 模块。") from exc
|
||||||
|
|
||||||
|
|
||||||
|
SCRIPT_DIR = Path(__file__).resolve().parent
|
||||||
|
SUPPORTED_CHASSIS_TYPES = {
|
||||||
|
"ackermann",
|
||||||
|
"differential",
|
||||||
|
"single_steer_wheel",
|
||||||
|
"multi_steer_wheel",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def parse_args() -> argparse.Namespace:
|
||||||
|
parser = argparse.ArgumentParser(description="校验并导出标定参考路径目录。")
|
||||||
|
parser.add_argument(
|
||||||
|
"--path-dir",
|
||||||
|
required=True,
|
||||||
|
help="参考路径 YAML 文件目录。目录中每条路径一个 YAML 文件。",
|
||||||
|
)
|
||||||
|
parser.add_argument("--path-id", default="", help="要导出的路径 ID。")
|
||||||
|
parser.add_argument(
|
||||||
|
"--chassis-type",
|
||||||
|
choices=sorted(SUPPORTED_CHASSIS_TYPES),
|
||||||
|
default="",
|
||||||
|
help="未指定 --path-id 时,从目录 index.yaml 的 default_path_id_by_chassis 中选择。",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--task-type",
|
||||||
|
default="",
|
||||||
|
help="未指定 --path-id/--chassis-type 时,从目录 index.yaml 的 default_path_id_by_task_type 中选择。",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--format",
|
||||||
|
choices=["summary", "metadata", "path"],
|
||||||
|
default="summary",
|
||||||
|
help="summary 输出摘要;metadata 输出总控 task_params;path 输出单条路径。",
|
||||||
|
)
|
||||||
|
parser.add_argument("-o", "--output", default="", help="输出文件路径;不填写时输出到标准输出。")
|
||||||
|
return parser.parse_args()
|
||||||
|
|
||||||
|
|
||||||
|
def load_yaml(path: Path) -> dict[str, Any]:
|
||||||
|
with path.open("r", encoding="utf-8") as stream:
|
||||||
|
data = yaml.safe_load(stream)
|
||||||
|
if data is None:
|
||||||
|
return {}
|
||||||
|
if not isinstance(data, dict):
|
||||||
|
raise ValueError(f"{path} 的顶层结构必须是 YAML map。")
|
||||||
|
return data
|
||||||
|
|
||||||
|
|
||||||
|
def require_map(value: Any, field_name: str) -> dict[str, Any]:
|
||||||
|
if not isinstance(value, dict):
|
||||||
|
raise ValueError(f"{field_name} 必须是 YAML map。")
|
||||||
|
return value
|
||||||
|
|
||||||
|
|
||||||
|
def require_list(value: Any, field_name: str) -> list[Any]:
|
||||||
|
if not isinstance(value, list):
|
||||||
|
raise ValueError(f"{field_name} 必须是 YAML list。")
|
||||||
|
return value
|
||||||
|
|
||||||
|
|
||||||
|
def require_string(value: Any, field_name: str) -> str:
|
||||||
|
if not isinstance(value, str) or not value.strip():
|
||||||
|
raise ValueError(f"{field_name} 必须是非空字符串。")
|
||||||
|
return value.strip()
|
||||||
|
|
||||||
|
|
||||||
|
def as_float(value: Any, field_name: str) -> float:
|
||||||
|
try:
|
||||||
|
return float(value)
|
||||||
|
except (TypeError, ValueError) as exc:
|
||||||
|
raise ValueError(f"{field_name} 必须是数字,当前值为 {value!r}。") from exc
|
||||||
|
|
||||||
|
|
||||||
|
def stringify_value(value: Any) -> str:
|
||||||
|
if isinstance(value, bool):
|
||||||
|
return "true" if value else "false"
|
||||||
|
if isinstance(value, float):
|
||||||
|
return f"{value:.9g}"
|
||||||
|
return str(value)
|
||||||
|
|
||||||
|
|
||||||
|
def resolve_path_dir(raw_path: str | Path, owner_profile_path: Path | None = None) -> Path:
|
||||||
|
path = Path(str(raw_path)).expanduser()
|
||||||
|
if path.is_absolute():
|
||||||
|
return path.resolve(strict=False)
|
||||||
|
if owner_profile_path is not None:
|
||||||
|
owner_relative = (owner_profile_path.parent / path).resolve(strict=False)
|
||||||
|
if owner_relative.exists():
|
||||||
|
return owner_relative
|
||||||
|
return path.resolve(strict=False)
|
||||||
|
|
||||||
|
|
||||||
|
def path_yaml_files(path_dir: Path) -> list[Path]:
|
||||||
|
if not path_dir.exists():
|
||||||
|
raise FileNotFoundError(f"参考路径目录不存在: {path_dir}")
|
||||||
|
if not path_dir.is_dir():
|
||||||
|
raise ValueError(f"参考路径路径必须是目录: {path_dir}")
|
||||||
|
return sorted(
|
||||||
|
path
|
||||||
|
for path in path_dir.glob("*.yaml")
|
||||||
|
if path.name not in {"index.yaml", "_index.yaml"}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def load_path_index(path_dir: Path) -> dict[str, Any]:
|
||||||
|
for filename in ("index.yaml", "_index.yaml"):
|
||||||
|
index_path = path_dir / filename
|
||||||
|
if index_path.exists():
|
||||||
|
return load_yaml(index_path)
|
||||||
|
return {}
|
||||||
|
|
||||||
|
|
||||||
|
def validate_point(point_value: Any, path_name: str, point_index: int) -> None:
|
||||||
|
point = require_map(point_value, f"{path_name}.points[{point_index}]")
|
||||||
|
as_float(point.get("x_m"), f"{path_name}.points[{point_index}].x_m")
|
||||||
|
as_float(point.get("y_m"), f"{path_name}.points[{point_index}].y_m")
|
||||||
|
if "z_m" in point:
|
||||||
|
as_float(point.get("z_m"), f"{path_name}.points[{point_index}].z_m")
|
||||||
|
if "yaw_rad" in point:
|
||||||
|
as_float(point.get("yaw_rad"), f"{path_name}.points[{point_index}].yaw_rad")
|
||||||
|
if "target_speed_ms" in point:
|
||||||
|
speed = as_float(point.get("target_speed_ms"), f"{path_name}.points[{point_index}].target_speed_ms")
|
||||||
|
if speed < 0.0:
|
||||||
|
raise ValueError(f"{path_name}.points[{point_index}].target_speed_ms 不能小于 0。")
|
||||||
|
|
||||||
|
|
||||||
|
def validate_reference_path(path: dict[str, Any], source_path: Path | None = None) -> dict[str, Any]:
|
||||||
|
schema_version = int(path.get("schema_version", 0))
|
||||||
|
if schema_version != 1:
|
||||||
|
raise ValueError(f"{source_path or path.get('path_id', '')} schema_version 必须为 1。")
|
||||||
|
path_id = require_string(path.get("path_id"), "path_id")
|
||||||
|
require_string(path.get("display_name"), f"{path_id}.display_name")
|
||||||
|
if "frame_id" in path:
|
||||||
|
require_string(path.get("frame_id"), f"{path_id}.frame_id")
|
||||||
|
if "path_type" in path:
|
||||||
|
require_string(path.get("path_type"), f"{path_id}.path_type")
|
||||||
|
if "module_type" in path:
|
||||||
|
require_string(path.get("module_type"), f"{path_id}.module_type")
|
||||||
|
points = require_list(path.get("points"), f"{path_id}.points")
|
||||||
|
if not points:
|
||||||
|
raise ValueError(f"{path_id}.points 不能为空。")
|
||||||
|
for index, point in enumerate(points):
|
||||||
|
validate_point(point, path_id, index)
|
||||||
|
for task_type in path.get("recommended_task_types", []) or []:
|
||||||
|
require_string(task_type, f"{path_id}.recommended_task_types[]")
|
||||||
|
if source_path is not None:
|
||||||
|
path["_source_file"] = str(source_path.resolve(strict=False))
|
||||||
|
return path
|
||||||
|
|
||||||
|
|
||||||
|
def load_reference_path_dir(raw_path_dir: str | Path, owner_profile_path: Path | None = None) -> tuple[dict[str, Any], Path]:
|
||||||
|
path_dir = resolve_path_dir(raw_path_dir, owner_profile_path)
|
||||||
|
index = load_path_index(path_dir)
|
||||||
|
paths: list[dict[str, Any]] = []
|
||||||
|
seen: set[str] = set()
|
||||||
|
for path_file in path_yaml_files(path_dir):
|
||||||
|
path = validate_reference_path(load_yaml(path_file), path_file)
|
||||||
|
path_id = str(path["path_id"])
|
||||||
|
if path_id in seen:
|
||||||
|
raise ValueError(f"重复的 path_id:{path_id}")
|
||||||
|
seen.add(path_id)
|
||||||
|
paths.append(path)
|
||||||
|
if not paths:
|
||||||
|
raise ValueError(f"参考路径目录没有路径 YAML: {path_dir}")
|
||||||
|
profile = {
|
||||||
|
"schema_version": 1,
|
||||||
|
"profile_name": index.get("profile_name", path_dir.name),
|
||||||
|
"frame_id": index.get("frame_id", "workshop"),
|
||||||
|
"default_path_id_by_chassis": index.get("default_path_id_by_chassis", {}) or {},
|
||||||
|
"default_path_id_by_task_type": index.get("default_path_id_by_task_type", {}) or {},
|
||||||
|
"paths": paths,
|
||||||
|
}
|
||||||
|
validate_path_dir_profile(profile)
|
||||||
|
return profile, path_dir
|
||||||
|
|
||||||
|
|
||||||
|
def validate_path_dir_profile(profile: dict[str, Any]) -> dict[str, Any]:
|
||||||
|
indexed = path_map(profile)
|
||||||
|
defaults_by_chassis = require_map(
|
||||||
|
profile.get("default_path_id_by_chassis", {}) or {},
|
||||||
|
"default_path_id_by_chassis",
|
||||||
|
)
|
||||||
|
for chassis_type, path_id in defaults_by_chassis.items():
|
||||||
|
if chassis_type not in SUPPORTED_CHASSIS_TYPES:
|
||||||
|
raise ValueError(f"default_path_id_by_chassis 包含未知底盘类型:{chassis_type}")
|
||||||
|
if str(path_id) not in indexed:
|
||||||
|
raise ValueError(f"default_path_id_by_chassis.{chassis_type} 引用了不存在的路径:{path_id}")
|
||||||
|
defaults_by_task_type = require_map(
|
||||||
|
profile.get("default_path_id_by_task_type", {}) or {},
|
||||||
|
"default_path_id_by_task_type",
|
||||||
|
)
|
||||||
|
for task_type, path_id in defaults_by_task_type.items():
|
||||||
|
require_string(str(task_type), "default_path_id_by_task_type key")
|
||||||
|
if str(path_id) not in indexed:
|
||||||
|
raise ValueError(f"default_path_id_by_task_type.{task_type} 引用了不存在的路径:{path_id}")
|
||||||
|
return profile
|
||||||
|
|
||||||
|
|
||||||
|
def path_map(profile: dict[str, Any]) -> dict[str, dict[str, Any]]:
|
||||||
|
indexed: dict[str, dict[str, Any]] = {}
|
||||||
|
for path in require_list(profile.get("paths"), "paths"):
|
||||||
|
path_id = require_string(path.get("path_id"), "path_id")
|
||||||
|
indexed[path_id] = path
|
||||||
|
return indexed
|
||||||
|
|
||||||
|
|
||||||
|
def select_path(
|
||||||
|
profile: dict[str, Any],
|
||||||
|
path_id: str = "",
|
||||||
|
chassis_type: str = "",
|
||||||
|
task_type: str = "",
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
indexed = path_map(profile)
|
||||||
|
selected_id = path_id.strip()
|
||||||
|
if not selected_id and chassis_type:
|
||||||
|
selected_id = str((profile.get("default_path_id_by_chassis", {}) or {}).get(chassis_type, ""))
|
||||||
|
if not selected_id and task_type:
|
||||||
|
selected_id = str((profile.get("default_path_id_by_task_type", {}) or {}).get(task_type, ""))
|
||||||
|
if not selected_id:
|
||||||
|
raise ValueError("未指定 path_id,也没有可用的默认参考路径。")
|
||||||
|
if selected_id not in indexed:
|
||||||
|
raise ValueError(f"参考路径不存在:{selected_id}")
|
||||||
|
return indexed[selected_id]
|
||||||
|
|
||||||
|
|
||||||
|
def flatten_path_metadata(path: dict[str, Any], profile: dict[str, Any], path_dir: Path | None = None) -> dict[str, str]:
|
||||||
|
points = require_list(path.get("points"), f"{path.get('path_id', '')}.points")
|
||||||
|
metadata: dict[str, str] = {
|
||||||
|
"reference_path.id": require_string(path.get("path_id"), "path_id"),
|
||||||
|
"reference_path.display_name": str(path.get("display_name", path.get("path_id", ""))),
|
||||||
|
"reference_path.frame_id": str(path.get("frame_id", profile.get("frame_id", "workshop"))),
|
||||||
|
"reference_path.path_type": str(path.get("path_type", "polyline")),
|
||||||
|
"reference_path.point_count": str(len(points)),
|
||||||
|
}
|
||||||
|
if path_dir is not None:
|
||||||
|
metadata["reference_path.path_dir"] = str(path_dir)
|
||||||
|
if path.get("_source_file"):
|
||||||
|
metadata["reference_path.file"] = str(path["_source_file"])
|
||||||
|
if path.get("module_type"):
|
||||||
|
metadata["reference_path.module_type"] = str(path["module_type"])
|
||||||
|
if path.get("description"):
|
||||||
|
metadata["reference_path.description"] = str(path["description"])
|
||||||
|
|
||||||
|
for index, point_value in enumerate(points):
|
||||||
|
point = require_map(point_value, f"{path['path_id']}.points[{index}]")
|
||||||
|
prefix = f"reference_path.pt_{index}"
|
||||||
|
metadata[f"{prefix}_x_m"] = stringify_value(as_float(point.get("x_m"), f"{prefix}_x_m"))
|
||||||
|
metadata[f"{prefix}_y_m"] = stringify_value(as_float(point.get("y_m"), f"{prefix}_y_m"))
|
||||||
|
metadata[f"{prefix}_z_m"] = stringify_value(as_float(point.get("z_m", 0.0), f"{prefix}_z_m"))
|
||||||
|
metadata[f"{prefix}_yaw_rad"] = stringify_value(as_float(point.get("yaw_rad", 0.0), f"{prefix}_yaw_rad"))
|
||||||
|
metadata[f"{prefix}_speed_ms"] = stringify_value(as_float(point.get("target_speed_ms", 0.0), f"{prefix}_speed_ms"))
|
||||||
|
return metadata
|
||||||
|
|
||||||
|
|
||||||
|
def selected_path_metadata(
|
||||||
|
reference_path_dir: str | Path,
|
||||||
|
owner_profile_path: Path | None = None,
|
||||||
|
path_id: str = "",
|
||||||
|
chassis_type: str = "",
|
||||||
|
task_type: str = "",
|
||||||
|
) -> tuple[dict[str, str], dict[str, Any]]:
|
||||||
|
profile, path_dir = load_reference_path_dir(reference_path_dir, owner_profile_path)
|
||||||
|
selected = select_path(profile, path_id, chassis_type, task_type)
|
||||||
|
return flatten_path_metadata(selected, profile, path_dir), selected
|
||||||
|
|
||||||
|
|
||||||
|
def path_points_as_trajectory(path: dict[str, Any]) -> list[dict[str, float]]:
|
||||||
|
points = require_list(path.get("points"), f"{path.get('path_id', '')}.points")
|
||||||
|
trajectory: list[dict[str, float]] = []
|
||||||
|
for index, point_value in enumerate(points):
|
||||||
|
point = require_map(point_value, f"{path.get('path_id', '')}.points[{index}]")
|
||||||
|
trajectory.append({
|
||||||
|
"x_m": as_float(point.get("x_m"), f"points[{index}].x_m"),
|
||||||
|
"y_m": as_float(point.get("y_m"), f"points[{index}].y_m"),
|
||||||
|
"yaw_rad": as_float(point.get("yaw_rad", 0.0), f"points[{index}].yaw_rad"),
|
||||||
|
"target_speed_ms": as_float(
|
||||||
|
point.get("target_speed_ms", 0.0),
|
||||||
|
f"points[{index}].target_speed_ms",
|
||||||
|
),
|
||||||
|
})
|
||||||
|
return trajectory
|
||||||
|
|
||||||
|
|
||||||
|
def make_task_param(key: str, value: Any) -> dict[str, str]:
|
||||||
|
return {"key": key, "value": stringify_value(value)}
|
||||||
|
|
||||||
|
|
||||||
|
def build_summary(profile: dict[str, Any], path_dir: Path) -> dict[str, Any]:
|
||||||
|
return {
|
||||||
|
"schema_version": profile["schema_version"],
|
||||||
|
"profile_name": profile.get("profile_name", ""),
|
||||||
|
"path_dir": str(path_dir),
|
||||||
|
"frame_id": profile.get("frame_id", "workshop"),
|
||||||
|
"default_path_id_by_chassis": profile.get("default_path_id_by_chassis", {}) or {},
|
||||||
|
"default_path_id_by_task_type": profile.get("default_path_id_by_task_type", {}) or {},
|
||||||
|
"paths": [
|
||||||
|
{
|
||||||
|
"path_id": path["path_id"],
|
||||||
|
"display_name": path.get("display_name", ""),
|
||||||
|
"module_type": path.get("module_type", ""),
|
||||||
|
"path_type": path.get("path_type", "polyline"),
|
||||||
|
"point_count": len(path.get("points", []) or []),
|
||||||
|
"recommended_task_types": path.get("recommended_task_types", []) or [],
|
||||||
|
"file": path.get("_source_file", ""),
|
||||||
|
}
|
||||||
|
for path in profile["paths"]
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def render_yaml(data: dict[str, Any]) -> str:
|
||||||
|
return yaml.safe_dump(data, sort_keys=False, allow_unicode=True)
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> int:
|
||||||
|
args = parse_args()
|
||||||
|
profile, path_dir = load_reference_path_dir(args.path_dir)
|
||||||
|
|
||||||
|
if args.format == "summary":
|
||||||
|
output = build_summary(profile, path_dir)
|
||||||
|
else:
|
||||||
|
selected = select_path(profile, args.path_id, args.chassis_type, args.task_type)
|
||||||
|
if args.format == "path":
|
||||||
|
output = {
|
||||||
|
"schema_version": 1,
|
||||||
|
"path_dir": str(path_dir),
|
||||||
|
"reference_path": {
|
||||||
|
key: value
|
||||||
|
for key, value in selected.items()
|
||||||
|
if not key.startswith("_")
|
||||||
|
},
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
metadata = flatten_path_metadata(selected, profile, path_dir)
|
||||||
|
output = {
|
||||||
|
"schema_version": 1,
|
||||||
|
"reference_path_id": metadata["reference_path.id"],
|
||||||
|
"display_name": metadata["reference_path.display_name"],
|
||||||
|
"task_params": [
|
||||||
|
make_task_param(key, metadata[key])
|
||||||
|
for key in sorted(metadata)
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
rendered = render_yaml(output)
|
||||||
|
if args.output:
|
||||||
|
output_path = Path(args.output).expanduser().resolve(strict=False)
|
||||||
|
output_path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
output_path.write_text(rendered, encoding="utf-8")
|
||||||
|
print(f"[OK] 已写入参考路径输出:{output_path}", file=sys.stderr)
|
||||||
|
else:
|
||||||
|
print(rendered, end="")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
try:
|
||||||
|
raise SystemExit(main())
|
||||||
|
except Exception as exc:
|
||||||
|
print(f"[错误] {exc}", file=sys.stderr)
|
||||||
|
raise SystemExit(1)
|
||||||
+249
@@ -0,0 +1,249 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""从 ROS 话题或 CSV 录制一条标定参考路径 YAML。"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import csv
|
||||||
|
import math
|
||||||
|
import signal
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
try:
|
||||||
|
import yaml
|
||||||
|
except ImportError as exc:
|
||||||
|
raise SystemExit("缺少 PyYAML,请先在当前 Python 环境安装 yaml 模块。") from exc
|
||||||
|
|
||||||
|
SCRIPT_DIR = Path(__file__).resolve().parent
|
||||||
|
if str(SCRIPT_DIR) not in sys.path:
|
||||||
|
sys.path.insert(0, str(SCRIPT_DIR))
|
||||||
|
|
||||||
|
from calibration_reference_paths import validate_reference_path # noqa: E402
|
||||||
|
|
||||||
|
|
||||||
|
def parse_args() -> argparse.Namespace:
|
||||||
|
parser = argparse.ArgumentParser(description="录制一条标定参考路径,并写成单独 YAML 文件。")
|
||||||
|
parser.add_argument("--output-dir", required=True, help="路径 YAML 输出目录,例如某模块的 reference_paths。")
|
||||||
|
parser.add_argument("--path-id", required=True, help="路径 ID,同时作为默认文件名。")
|
||||||
|
parser.add_argument("--display-name", required=True, help="中文路径名称,例如:底盘直线前进 1m。")
|
||||||
|
parser.add_argument(
|
||||||
|
"--module-type",
|
||||||
|
choices=["chassis", "control", "sensor"],
|
||||||
|
required=True,
|
||||||
|
help="路径归属模块。",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--allowed-chassis-type",
|
||||||
|
choices=["ackermann", "differential", "single_steer_wheel", "multi_steer_wheel"],
|
||||||
|
action="append",
|
||||||
|
default=[],
|
||||||
|
help="底盘路径允许出现在哪些底盘类型下,可重复传入。",
|
||||||
|
)
|
||||||
|
parser.add_argument("--path-type", default="recorded", help="路径类型,例如 straight_line、arc、recorded。")
|
||||||
|
parser.add_argument("--frame-id", default="workshop", help="路径坐标系。")
|
||||||
|
parser.add_argument("--description", default="", help="路径说明。")
|
||||||
|
parser.add_argument(
|
||||||
|
"--recommended-task-type",
|
||||||
|
action="append",
|
||||||
|
default=[],
|
||||||
|
help="推荐使用的任务类型,可重复传入。",
|
||||||
|
)
|
||||||
|
parser.add_argument("--target-speed-ms", type=float, default=0.0, help="写入路径点的默认目标速度。")
|
||||||
|
parser.add_argument("--duration-sec", type=float, default=0.0, help="ROS 录制时长;0 表示 Ctrl+C 结束。")
|
||||||
|
parser.add_argument("--min-distance-step-m", type=float, default=0.03, help="相邻保留点的最小平移距离。")
|
||||||
|
parser.add_argument("--min-yaw-step-rad", type=float, default=0.03, help="相邻保留点的最小 yaw 变化。")
|
||||||
|
parser.add_argument("--overwrite", action="store_true", help="允许覆盖同名路径文件。")
|
||||||
|
|
||||||
|
source = parser.add_mutually_exclusive_group(required=True)
|
||||||
|
source.add_argument("--input-csv", default="", help="从 CSV 生成路径,不启动 ROS。")
|
||||||
|
source.add_argument(
|
||||||
|
"--source",
|
||||||
|
choices=["external_pose", "chassis_telemetry"],
|
||||||
|
help="从 ROS 话题录制路径。",
|
||||||
|
)
|
||||||
|
parser.add_argument("--topic", default="", help="ROS 话题;不填时按 source 使用默认话题。")
|
||||||
|
return parser.parse_args()
|
||||||
|
|
||||||
|
|
||||||
|
def yaw_delta(a: float, b: float) -> float:
|
||||||
|
delta = (a - b + math.pi) % (2.0 * math.pi) - math.pi
|
||||||
|
return abs(delta)
|
||||||
|
|
||||||
|
|
||||||
|
def should_keep_point(
|
||||||
|
points: list[dict[str, float]],
|
||||||
|
point: dict[str, float],
|
||||||
|
min_distance_step_m: float,
|
||||||
|
min_yaw_step_rad: float,
|
||||||
|
) -> bool:
|
||||||
|
if not points:
|
||||||
|
return True
|
||||||
|
previous = points[-1]
|
||||||
|
distance = math.hypot(point["x_m"] - previous["x_m"], point["y_m"] - previous["y_m"])
|
||||||
|
return distance >= min_distance_step_m or yaw_delta(point["yaw_rad"], previous["yaw_rad"]) >= min_yaw_step_rad
|
||||||
|
|
||||||
|
|
||||||
|
def append_decimated(points: list[dict[str, float]], point: dict[str, float], args: argparse.Namespace) -> None:
|
||||||
|
if should_keep_point(points, point, args.min_distance_step_m, args.min_yaw_step_rad):
|
||||||
|
points.append(point)
|
||||||
|
|
||||||
|
|
||||||
|
def csv_float(row: dict[str, str], keys: tuple[str, ...], default: float = 0.0) -> float:
|
||||||
|
for key in keys:
|
||||||
|
value = row.get(key)
|
||||||
|
if value not in (None, ""):
|
||||||
|
return float(value)
|
||||||
|
return default
|
||||||
|
|
||||||
|
|
||||||
|
def read_points_from_csv(path: Path, args: argparse.Namespace) -> list[dict[str, float]]:
|
||||||
|
points: list[dict[str, float]] = []
|
||||||
|
with path.open("r", newline="", encoding="utf-8") as stream:
|
||||||
|
reader = csv.DictReader(stream)
|
||||||
|
for row in reader:
|
||||||
|
if row.get("pose_valid") not in (None, "", "1", "true", "True", "TRUE"):
|
||||||
|
continue
|
||||||
|
point = {
|
||||||
|
"x_m": csv_float(row, ("x_m", "odom_x_m")),
|
||||||
|
"y_m": csv_float(row, ("y_m", "odom_y_m")),
|
||||||
|
"z_m": csv_float(row, ("z_m",), 0.0),
|
||||||
|
"yaw_rad": csv_float(row, ("yaw_rad", "odom_yaw_rad"), 0.0),
|
||||||
|
"target_speed_ms": args.target_speed_ms,
|
||||||
|
}
|
||||||
|
append_decimated(points, point, args)
|
||||||
|
return points
|
||||||
|
|
||||||
|
|
||||||
|
def default_topic(source: str) -> str:
|
||||||
|
if source == "external_pose":
|
||||||
|
return "/workshop/external_localization/vehicle/pose"
|
||||||
|
if source == "chassis_telemetry":
|
||||||
|
return "/chassis/telemetry"
|
||||||
|
raise ValueError(f"不支持的 source: {source}")
|
||||||
|
|
||||||
|
|
||||||
|
def read_points_from_ros(args: argparse.Namespace) -> list[dict[str, float]]:
|
||||||
|
try:
|
||||||
|
import rclpy
|
||||||
|
from calibration_chassis_interfaces.msg import ChassisTelemetry
|
||||||
|
from calibration_external_localization_interfaces.msg import ExternalLocalizationTelemetry
|
||||||
|
except ImportError as exc:
|
||||||
|
raise RuntimeError(f"缺少 ROS 2 Python 环境或消息包: {exc}") from exc
|
||||||
|
|
||||||
|
points: list[dict[str, float]] = []
|
||||||
|
stop_requested = False
|
||||||
|
topic = args.topic or default_topic(args.source)
|
||||||
|
|
||||||
|
def on_signal(signum: int, frame: Any) -> None:
|
||||||
|
nonlocal stop_requested
|
||||||
|
(void_signum, void_frame) = (signum, frame)
|
||||||
|
_ = (void_signum, void_frame)
|
||||||
|
stop_requested = True
|
||||||
|
|
||||||
|
previous_sigint_handler = signal.signal(signal.SIGINT, on_signal)
|
||||||
|
previous_sigterm_handler = signal.signal(signal.SIGTERM, on_signal)
|
||||||
|
rclpy.init(args=None)
|
||||||
|
node = rclpy.create_node("calibration_reference_path_recorder")
|
||||||
|
|
||||||
|
def on_external_pose(msg: Any) -> None:
|
||||||
|
if hasattr(msg, "pose_valid") and not bool(msg.pose_valid):
|
||||||
|
return
|
||||||
|
pose = msg.workshop_pose
|
||||||
|
point = {
|
||||||
|
"x_m": float(pose.x_m),
|
||||||
|
"y_m": float(pose.y_m),
|
||||||
|
"z_m": float(getattr(pose, "z_m", 0.0)),
|
||||||
|
"yaw_rad": float(pose.yaw_rad),
|
||||||
|
"target_speed_ms": args.target_speed_ms,
|
||||||
|
}
|
||||||
|
append_decimated(points, point, args)
|
||||||
|
|
||||||
|
def on_chassis_telemetry(msg: Any) -> None:
|
||||||
|
point = {
|
||||||
|
"x_m": float(msg.odom_x_m),
|
||||||
|
"y_m": float(msg.odom_y_m),
|
||||||
|
"z_m": 0.0,
|
||||||
|
"yaw_rad": float(msg.odom_yaw_rad),
|
||||||
|
"target_speed_ms": args.target_speed_ms,
|
||||||
|
}
|
||||||
|
append_decimated(points, point, args)
|
||||||
|
|
||||||
|
try:
|
||||||
|
if args.source == "external_pose":
|
||||||
|
node.create_subscription(ExternalLocalizationTelemetry, topic, on_external_pose, 50)
|
||||||
|
elif args.source == "chassis_telemetry":
|
||||||
|
node.create_subscription(ChassisTelemetry, topic, on_chassis_telemetry, 50)
|
||||||
|
else:
|
||||||
|
raise ValueError(f"不支持的 source: {args.source}")
|
||||||
|
|
||||||
|
print(f"[*] 正在录制参考路径: topic={topic}, display_name={args.display_name}", file=sys.stderr)
|
||||||
|
start = time.monotonic()
|
||||||
|
while rclpy.ok() and not stop_requested:
|
||||||
|
rclpy.spin_once(node, timeout_sec=0.1)
|
||||||
|
if args.duration_sec > 0.0 and time.monotonic() - start >= args.duration_sec:
|
||||||
|
break
|
||||||
|
finally:
|
||||||
|
node.destroy_node()
|
||||||
|
rclpy.shutdown()
|
||||||
|
signal.signal(signal.SIGINT, previous_sigint_handler)
|
||||||
|
signal.signal(signal.SIGTERM, previous_sigterm_handler)
|
||||||
|
return points
|
||||||
|
|
||||||
|
|
||||||
|
def build_path_document(points: list[dict[str, float]], args: argparse.Namespace) -> dict[str, Any]:
|
||||||
|
if not points:
|
||||||
|
raise ValueError("没有录到任何路径点。")
|
||||||
|
document: dict[str, Any] = {
|
||||||
|
"schema_version": 1,
|
||||||
|
"path_id": args.path_id,
|
||||||
|
"display_name": args.display_name,
|
||||||
|
"module_type": args.module_type,
|
||||||
|
"frame_id": args.frame_id,
|
||||||
|
"path_type": args.path_type,
|
||||||
|
"points": points,
|
||||||
|
}
|
||||||
|
if args.description:
|
||||||
|
document["description"] = args.description
|
||||||
|
if args.recommended_task_type:
|
||||||
|
document["recommended_task_types"] = args.recommended_task_type
|
||||||
|
if args.module_type == "chassis" and args.allowed_chassis_type:
|
||||||
|
document["allowed_chassis_types"] = sorted(set(args.allowed_chassis_type))
|
||||||
|
validate_reference_path(dict(document))
|
||||||
|
return document
|
||||||
|
|
||||||
|
|
||||||
|
def write_path_document(document: dict[str, Any], args: argparse.Namespace) -> Path:
|
||||||
|
output_dir = Path(args.output_dir).expanduser().resolve(strict=False)
|
||||||
|
output_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
output_path = output_dir / f"{args.path_id}.yaml"
|
||||||
|
if output_path.exists() and not args.overwrite:
|
||||||
|
raise FileExistsError(f"路径文件已存在,若要覆盖请加 --overwrite: {output_path}")
|
||||||
|
output_path.write_text(
|
||||||
|
yaml.safe_dump(document, sort_keys=False, allow_unicode=True),
|
||||||
|
encoding="utf-8",
|
||||||
|
)
|
||||||
|
return output_path
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> int:
|
||||||
|
args = parse_args()
|
||||||
|
if args.input_csv:
|
||||||
|
points = read_points_from_csv(Path(args.input_csv).expanduser().resolve(strict=False), args)
|
||||||
|
else:
|
||||||
|
points = read_points_from_ros(args)
|
||||||
|
document = build_path_document(points, args)
|
||||||
|
output_path = write_path_document(document, args)
|
||||||
|
print(f"[OK] 已写入参考路径: {output_path}", file=sys.stderr)
|
||||||
|
print(yaml.safe_dump({"path_file": str(output_path), "point_count": len(points)}, sort_keys=False, allow_unicode=True), end="")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
try:
|
||||||
|
raise SystemExit(main())
|
||||||
|
except Exception as exc:
|
||||||
|
print(f"[错误] {exc}", file=sys.stderr)
|
||||||
|
raise SystemExit(1)
|
||||||
@@ -23,6 +23,14 @@
|
|||||||
src/site_deployment/workshop_sensor_calibration_real/config/sensor_calibration_profile.yaml
|
src/site_deployment/workshop_sensor_calibration_real/config/sensor_calibration_profile.yaml
|
||||||
```
|
```
|
||||||
|
|
||||||
|
传感器参考路径单独放在:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
src/site_deployment/workshop_sensor_calibration_real/reference_paths/
|
||||||
|
```
|
||||||
|
|
||||||
|
每条路径一个 YAML 文件,必须配置 `path_id` 和中文 `display_name`。`sensor_calibration_profile.yaml` 中每个任务通过 `reference_path_id` 选择路径,导出任务时会自动带上 `reference_path.*` metadata,便于 UI 或 Isaac 显示当前标定采集路径。
|
||||||
|
|
||||||
校验并查看摘要:
|
校验并查看摘要:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
|||||||
+10
@@ -4,6 +4,7 @@
|
|||||||
|
|
||||||
schema_version: 1
|
schema_version: 1
|
||||||
profile_name: workshop_sensor_calibration_profile
|
profile_name: workshop_sensor_calibration_profile
|
||||||
|
reference_path_dir: ../reference_paths
|
||||||
|
|
||||||
data_capture:
|
data_capture:
|
||||||
required_data_inputs:
|
required_data_inputs:
|
||||||
@@ -25,6 +26,7 @@ tasks:
|
|||||||
display_name: 前视相机内参
|
display_name: 前视相机内参
|
||||||
enabled: true
|
enabled: true
|
||||||
selected_task: camera_intrinsic
|
selected_task: camera_intrinsic
|
||||||
|
reference_path_id: sensor_static_front_station
|
||||||
sensor_id: demo_front_camera
|
sensor_id: demo_front_camera
|
||||||
task_subtype: front_camera_intrinsic
|
task_subtype: front_camera_intrinsic
|
||||||
camera_intrinsic:
|
camera_intrinsic:
|
||||||
@@ -36,6 +38,7 @@ tasks:
|
|||||||
display_name: 下视相机内参
|
display_name: 下视相机内参
|
||||||
enabled: true
|
enabled: true
|
||||||
selected_task: camera_intrinsic
|
selected_task: camera_intrinsic
|
||||||
|
reference_path_id: sensor_static_front_station
|
||||||
sensor_id: demo_down_camera
|
sensor_id: demo_down_camera
|
||||||
task_subtype: downward_camera_intrinsic
|
task_subtype: downward_camera_intrinsic
|
||||||
camera_intrinsic:
|
camera_intrinsic:
|
||||||
@@ -47,6 +50,7 @@ tasks:
|
|||||||
display_name: IMU 内参
|
display_name: IMU 内参
|
||||||
enabled: true
|
enabled: true
|
||||||
selected_task: imu_intrinsic
|
selected_task: imu_intrinsic
|
||||||
|
reference_path_id: sensor_imu_motion_line
|
||||||
sensor_id: demo_imu
|
sensor_id: demo_imu
|
||||||
task_subtype: imu_intrinsic
|
task_subtype: imu_intrinsic
|
||||||
imu_intrinsic:
|
imu_intrinsic:
|
||||||
@@ -58,6 +62,7 @@ tasks:
|
|||||||
display_name: 前视相机到 base_link 外参
|
display_name: 前视相机到 base_link 外参
|
||||||
enabled: true
|
enabled: true
|
||||||
selected_task: sensor_extrinsic
|
selected_task: sensor_extrinsic
|
||||||
|
reference_path_id: sensor_extrinsic_slow_line
|
||||||
sensor_id: demo_front_camera
|
sensor_id: demo_front_camera
|
||||||
task_subtype: front_camera_extrinsic
|
task_subtype: front_camera_extrinsic
|
||||||
sensor_extrinsic:
|
sensor_extrinsic:
|
||||||
@@ -69,6 +74,7 @@ tasks:
|
|||||||
display_name: 下视相机到 base_link 外参
|
display_name: 下视相机到 base_link 外参
|
||||||
enabled: true
|
enabled: true
|
||||||
selected_task: sensor_extrinsic
|
selected_task: sensor_extrinsic
|
||||||
|
reference_path_id: sensor_extrinsic_slow_line
|
||||||
sensor_id: demo_down_camera
|
sensor_id: demo_down_camera
|
||||||
task_subtype: downward_camera_extrinsic
|
task_subtype: downward_camera_extrinsic
|
||||||
sensor_extrinsic:
|
sensor_extrinsic:
|
||||||
@@ -80,6 +86,7 @@ tasks:
|
|||||||
display_name: 2D LiDAR 到 base_link 外参
|
display_name: 2D LiDAR 到 base_link 外参
|
||||||
enabled: true
|
enabled: true
|
||||||
selected_task: sensor_extrinsic
|
selected_task: sensor_extrinsic
|
||||||
|
reference_path_id: sensor_extrinsic_slow_line
|
||||||
sensor_id: demo_lidar_2d
|
sensor_id: demo_lidar_2d
|
||||||
task_subtype: lidar_2d_extrinsic
|
task_subtype: lidar_2d_extrinsic
|
||||||
sensor_extrinsic:
|
sensor_extrinsic:
|
||||||
@@ -91,6 +98,7 @@ tasks:
|
|||||||
display_name: 3D LiDAR 到 base_link 外参
|
display_name: 3D LiDAR 到 base_link 外参
|
||||||
enabled: true
|
enabled: true
|
||||||
selected_task: sensor_extrinsic
|
selected_task: sensor_extrinsic
|
||||||
|
reference_path_id: sensor_extrinsic_slow_line
|
||||||
sensor_id: demo_lidar_3d
|
sensor_id: demo_lidar_3d
|
||||||
task_subtype: lidar_3d_extrinsic
|
task_subtype: lidar_3d_extrinsic
|
||||||
sensor_extrinsic:
|
sensor_extrinsic:
|
||||||
@@ -102,6 +110,7 @@ tasks:
|
|||||||
display_name: IMU 到 base_link 外参
|
display_name: IMU 到 base_link 外参
|
||||||
enabled: true
|
enabled: true
|
||||||
selected_task: sensor_extrinsic
|
selected_task: sensor_extrinsic
|
||||||
|
reference_path_id: sensor_extrinsic_slow_line
|
||||||
sensor_id: demo_imu
|
sensor_id: demo_imu
|
||||||
task_subtype: imu_extrinsic
|
task_subtype: imu_extrinsic
|
||||||
sensor_extrinsic:
|
sensor_extrinsic:
|
||||||
@@ -113,6 +122,7 @@ tasks:
|
|||||||
display_name: 手眼相机眼在手上
|
display_name: 手眼相机眼在手上
|
||||||
enabled: true
|
enabled: true
|
||||||
selected_task: hand_eye
|
selected_task: hand_eye
|
||||||
|
reference_path_id: hand_eye_pose_sweep
|
||||||
sensor_id: demo_arm_camera
|
sensor_id: demo_arm_camera
|
||||||
task_subtype: eye_in_hand
|
task_subtype: eye_in_hand
|
||||||
hand_eye:
|
hand_eye:
|
||||||
|
|||||||
+21
@@ -0,0 +1,21 @@
|
|||||||
|
schema_version: 1
|
||||||
|
path_id: hand_eye_pose_sweep
|
||||||
|
display_name: 手眼标定位姿序列
|
||||||
|
module_type: sensor
|
||||||
|
frame_id: workshop
|
||||||
|
path_type: pose_sweep
|
||||||
|
recommended_task_types:
|
||||||
|
- hand_eye
|
||||||
|
points:
|
||||||
|
- x_m: 0.0
|
||||||
|
y_m: 0.0
|
||||||
|
yaw_rad: 0.0
|
||||||
|
target_speed_ms: 0.0
|
||||||
|
- x_m: 0.15
|
||||||
|
y_m: 0.0
|
||||||
|
yaw_rad: 0.15
|
||||||
|
target_speed_ms: 0.0
|
||||||
|
- x_m: -0.15
|
||||||
|
y_m: 0.0
|
||||||
|
yaw_rad: -0.15
|
||||||
|
target_speed_ms: 0.0
|
||||||
+8
@@ -0,0 +1,8 @@
|
|||||||
|
profile_name: sensor_calibration_reference_paths
|
||||||
|
frame_id: workshop
|
||||||
|
|
||||||
|
default_path_id_by_task_type:
|
||||||
|
camera_intrinsic: sensor_static_front_station
|
||||||
|
imu_intrinsic: sensor_imu_motion_line
|
||||||
|
sensor_extrinsic: sensor_extrinsic_slow_line
|
||||||
|
hand_eye: hand_eye_pose_sweep
|
||||||
+21
@@ -0,0 +1,21 @@
|
|||||||
|
schema_version: 1
|
||||||
|
path_id: sensor_extrinsic_slow_line
|
||||||
|
display_name: 传感器外参低速直线采样
|
||||||
|
module_type: sensor
|
||||||
|
frame_id: workshop
|
||||||
|
path_type: sampling_line
|
||||||
|
recommended_task_types:
|
||||||
|
- sensor_extrinsic
|
||||||
|
points:
|
||||||
|
- x_m: 0.0
|
||||||
|
y_m: 0.0
|
||||||
|
yaw_rad: 0.0
|
||||||
|
target_speed_ms: 0.05
|
||||||
|
- x_m: 0.4
|
||||||
|
y_m: 0.0
|
||||||
|
yaw_rad: 0.0
|
||||||
|
target_speed_ms: 0.05
|
||||||
|
- x_m: 0.8
|
||||||
|
y_m: 0.0
|
||||||
|
yaw_rad: 0.0
|
||||||
|
target_speed_ms: 0.05
|
||||||
+17
@@ -0,0 +1,17 @@
|
|||||||
|
schema_version: 1
|
||||||
|
path_id: sensor_imu_motion_line
|
||||||
|
display_name: IMU 内参低速运动段
|
||||||
|
module_type: sensor
|
||||||
|
frame_id: workshop
|
||||||
|
path_type: imu_motion
|
||||||
|
recommended_task_types:
|
||||||
|
- imu_intrinsic
|
||||||
|
points:
|
||||||
|
- x_m: 0.0
|
||||||
|
y_m: 0.0
|
||||||
|
yaw_rad: 0.0
|
||||||
|
target_speed_ms: 0.05
|
||||||
|
- x_m: 0.5
|
||||||
|
y_m: 0.0
|
||||||
|
yaw_rad: 0.0
|
||||||
|
target_speed_ms: 0.05
|
||||||
+13
@@ -0,0 +1,13 @@
|
|||||||
|
schema_version: 1
|
||||||
|
path_id: sensor_static_front_station
|
||||||
|
display_name: 传感器静态采集点
|
||||||
|
module_type: sensor
|
||||||
|
frame_id: workshop
|
||||||
|
path_type: static_station
|
||||||
|
recommended_task_types:
|
||||||
|
- camera_intrinsic
|
||||||
|
points:
|
||||||
|
- x_m: 0.0
|
||||||
|
y_m: 0.0
|
||||||
|
yaw_rad: 0.0
|
||||||
|
target_speed_ms: 0.0
|
||||||
+25
-3
@@ -13,6 +13,13 @@ try:
|
|||||||
except ImportError as exc:
|
except ImportError as exc:
|
||||||
raise SystemExit("缺少 PyYAML,请先在当前 Python 环境安装 yaml 模块。") from exc
|
raise SystemExit("缺少 PyYAML,请先在当前 Python 环境安装 yaml 模块。") from exc
|
||||||
|
|
||||||
|
SCRIPT_DIR = Path(__file__).resolve().parent
|
||||||
|
REFERENCE_PATH_TOOL_DIR = SCRIPT_DIR.parents[0] / "workshop_reference_paths"
|
||||||
|
if str(REFERENCE_PATH_TOOL_DIR) not in sys.path:
|
||||||
|
sys.path.insert(0, str(REFERENCE_PATH_TOOL_DIR))
|
||||||
|
|
||||||
|
from calibration_reference_paths import selected_path_metadata # noqa: E402
|
||||||
|
|
||||||
|
|
||||||
TASK_GROUPS = {
|
TASK_GROUPS = {
|
||||||
"camera_intrinsic",
|
"camera_intrinsic",
|
||||||
@@ -224,6 +231,8 @@ def validate_task(task: dict[str, Any], index: int) -> None:
|
|||||||
raise ValueError(
|
raise ValueError(
|
||||||
f"{task_name}.task_subtype={task_subtype} 不适用于 selected_task={selected_task}。"
|
f"{task_name}.task_subtype={task_subtype} 不适用于 selected_task={selected_task}。"
|
||||||
)
|
)
|
||||||
|
if task.get("reference_path_id") not in (None, ""):
|
||||||
|
require_string(task.get("reference_path_id"), f"{task_name}.reference_path_id")
|
||||||
|
|
||||||
if selected_task == "camera_intrinsic":
|
if selected_task == "camera_intrinsic":
|
||||||
validate_camera_intrinsic(task, task_name)
|
validate_camera_intrinsic(task, task_name)
|
||||||
@@ -276,12 +285,24 @@ def task_matches_requested(task: dict[str, Any], requested_tasks: set[str]) -> b
|
|||||||
return TASK_GROUP_TO_REQUESTED_TASK[selected_task] in requested_tasks
|
return TASK_GROUP_TO_REQUESTED_TASK[selected_task] in requested_tasks
|
||||||
|
|
||||||
|
|
||||||
def flatten_task_metadata(task: dict[str, Any]) -> list[dict[str, str]]:
|
def flatten_task_metadata(
|
||||||
|
task: dict[str, Any],
|
||||||
|
profile: dict[str, Any],
|
||||||
|
profile_path: Path | None = None,
|
||||||
|
) -> list[dict[str, str]]:
|
||||||
selected_task = str(task["selected_task"])
|
selected_task = str(task["selected_task"])
|
||||||
|
reference_metadata, _ = selected_path_metadata(
|
||||||
|
profile.get("reference_path_dir", "reference_paths"),
|
||||||
|
profile_path,
|
||||||
|
str(task.get("reference_path_id", "")),
|
||||||
|
"",
|
||||||
|
selected_task,
|
||||||
|
)
|
||||||
params = [
|
params = [
|
||||||
make_task_param("sensor.sensor_id", task["sensor_id"]),
|
make_task_param("sensor.sensor_id", task["sensor_id"]),
|
||||||
make_task_param("sensor.task_subtype", task["task_subtype"]),
|
make_task_param("sensor.task_subtype", task["task_subtype"]),
|
||||||
]
|
]
|
||||||
|
params.extend(make_task_param(key, reference_metadata[key]) for key in sorted(reference_metadata))
|
||||||
|
|
||||||
if selected_task == "camera_intrinsic":
|
if selected_task == "camera_intrinsic":
|
||||||
payload = task["camera_intrinsic"]
|
payload = task["camera_intrinsic"]
|
||||||
@@ -324,6 +345,7 @@ def flatten_task_metadata(task: dict[str, Any]) -> list[dict[str, str]]:
|
|||||||
def export_requested_tasks(
|
def export_requested_tasks(
|
||||||
profile: dict[str, Any],
|
profile: dict[str, Any],
|
||||||
requested_tasks: set[str] | list[str] | str | None = None,
|
requested_tasks: set[str] | list[str] | str | None = None,
|
||||||
|
profile_path: Path | None = None,
|
||||||
) -> dict[str, Any]:
|
) -> dict[str, Any]:
|
||||||
selected_requested_tasks = (
|
selected_requested_tasks = (
|
||||||
requested_tasks
|
requested_tasks
|
||||||
@@ -345,7 +367,7 @@ def export_requested_tasks(
|
|||||||
"reason": str(task.get("reason", "现场传感器标定 profile")),
|
"reason": str(task.get("reason", "现场传感器标定 profile")),
|
||||||
"task_code": str(task["task_code"]),
|
"task_code": str(task["task_code"]),
|
||||||
"target_id": str(task["sensor_id"]),
|
"target_id": str(task["sensor_id"]),
|
||||||
"task_params": flatten_task_metadata(task),
|
"task_params": flatten_task_metadata(task, profile, profile_path),
|
||||||
})
|
})
|
||||||
return {
|
return {
|
||||||
"schema_version": 1,
|
"schema_version": 1,
|
||||||
@@ -382,7 +404,7 @@ def main() -> int:
|
|||||||
profile = validate_profile(load_yaml(profile_path))
|
profile = validate_profile(load_yaml(profile_path))
|
||||||
|
|
||||||
if args.format == "requested_tasks":
|
if args.format == "requested_tasks":
|
||||||
output = export_requested_tasks(profile, parse_requested_tasks(args.tasks))
|
output = export_requested_tasks(profile, parse_requested_tasks(args.tasks), profile_path)
|
||||||
else:
|
else:
|
||||||
output = build_summary(profile)
|
output = build_summary(profile)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user