diff --git a/agv_calib_brain/.codex b/agv_calib_brain/.codex new file mode 100644 index 0000000..e69de29 diff --git a/agv_calib_brain/.isaac_cache/ack_m_isaac_fixed.urdf b/agv_calib_brain/.isaac_cache/ack_m_isaac_fixed.urdf new file mode 100644 index 0000000..dace892 --- /dev/null +++ b/agv_calib_brain/.isaac_cache/ack_m_isaac_fixed.urdf @@ -0,0 +1,301 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/agv_calib_brain/.isaac_cache/ack_m_isaac_fixed.usd b/agv_calib_brain/.isaac_cache/ack_m_isaac_fixed.usd new file mode 100644 index 0000000..ab778e1 --- /dev/null +++ b/agv_calib_brain/.isaac_cache/ack_m_isaac_fixed.usd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ac7fb8e48883155d1feffe880c55a972c5562b34bc340f0002ff80965d20cfac +size 75606548 diff --git a/agv_calib_brain/.isaac_cache/checkerboard.png b/agv_calib_brain/.isaac_cache/checkerboard.png new file mode 100644 index 0000000..26b3c06 Binary files /dev/null and b/agv_calib_brain/.isaac_cache/checkerboard.png differ diff --git a/agv_calib_brain/.isaac_cache/down_camera_charuco.png b/agv_calib_brain/.isaac_cache/down_camera_charuco.png new file mode 100644 index 0000000..c7d2c92 Binary files /dev/null and b/agv_calib_brain/.isaac_cache/down_camera_charuco.png differ diff --git a/agv_calib_brain/.isaac_cache/workshop_scene_manifest.json b/agv_calib_brain/.isaac_cache/workshop_scene_manifest.json new file mode 100644 index 0000000..36dd910 --- /dev/null +++ b/agv_calib_brain/.isaac_cache/workshop_scene_manifest.json @@ -0,0 +1,840 @@ +{ + "vehicle": { + "vehicle_id": "demo_agv_001", + "vehicle_source": "urdf_direct", + "source_urdf_path": "/home/nvidia/study/AutoCalib-Workshop/agv_calib_brain/src/simulation/isaac_workshop_sim/assets/urdf/ackermann_front_steer_rear_drive.urdf", + "urdf_path": "/home/nvidia/study/AutoCalib-Workshop/agv_calib_brain/src/simulation/isaac_workshop_sim/assets/urdf/ackermann_front_steer_rear_drive.urdf", + "usd_path": null, + "stage_prim_path": "/ackermann_front_steer_rear_drive", + "drive_wheels": [ + "rear_left_wheel_link", + "rear_right_wheel_link" + ], + "steer_wheels": [ + "front_left_wheel_link", + "front_right_wheel_link" + ], + "initial_pose": { + "x_m": 0.0, + "y_m": 0.0, + "z_m": 0.0, + "yaw_rad": 0.0 + } + }, + "workshop": { + "workcell_zone_id": "isaac_workcell_zone_a", + "room_length_m": 10.0, + "room_width_m": 6.0, + "room_height_m": 3.5, + "checkerboard": { + "rows": 6, + "cols": 9, + "square_size_m": 0.12, + "texture_path": "/home/nvidia/study/AutoCalib-Workshop/agv_calib_brain/.isaac_cache/checkerboard.png", + "reserved_wall_zones": { + "front": [ + { + "axis_min": -2.82, + "axis_max": -0.9199999999999999, + "z_min": 0.0, + "z_max": 3.5, + "reason": "2d_lidar_extrinsic_corner_bay" + } + ], + "right": [ + { + "axis_min": 2.9200000000000004, + "axis_max": 4.82, + "z_min": 0.0, + "z_max": 3.5, + "reason": "2d_lidar_extrinsic_corner_bay" + } + ] + }, + "layout": [ + { + "id": "front_wall_r00_c01", + "wall": "front", + "mount": "flush", + "purpose": "wall_checkerboard_array", + "position": [ + 4.985, + 0.0, + 1.2100000000000002 + ], + "rotation_deg": [ + 90, + 0, + 90 + ] + }, + { + "id": "front_wall_r00_c02", + "wall": "front", + "mount": "flush", + "purpose": "wall_checkerboard_array", + "position": [ + 4.985, + 1.4999999999999998, + 1.2100000000000002 + ], + "rotation_deg": [ + 90, + 0, + 90 + ] + }, + { + "id": "front_wall_r01_c01", + "wall": "front", + "mount": "flush", + "purpose": "wall_checkerboard_array", + "position": [ + 4.985, + 0.0, + 2.33 + ], + "rotation_deg": [ + 90, + 0, + 90 + ] + }, + { + "id": "front_wall_r01_c02", + "wall": "front", + "mount": "flush", + "purpose": "wall_checkerboard_array", + "position": [ + 4.985, + 1.4999999999999998, + 2.33 + ], + "rotation_deg": [ + 90, + 0, + 90 + ] + }, + { + "id": "back_wall_r00_c00", + "wall": "back", + "mount": "flush", + "purpose": "wall_checkerboard_array", + "position": [ + -4.985, + -1.4999999999999998, + 1.2100000000000002 + ], + "rotation_deg": [ + 90, + 0, + -90 + ] + }, + { + "id": "back_wall_r00_c01", + "wall": "back", + "mount": "flush", + "purpose": "wall_checkerboard_array", + "position": [ + -4.985, + 0.0, + 1.2100000000000002 + ], + "rotation_deg": [ + 90, + 0, + -90 + ] + }, + { + "id": "back_wall_r00_c02", + "wall": "back", + "mount": "flush", + "purpose": "wall_checkerboard_array", + "position": [ + -4.985, + 1.4999999999999998, + 1.2100000000000002 + ], + "rotation_deg": [ + 90, + 0, + -90 + ] + }, + { + "id": "back_wall_r01_c00", + "wall": "back", + "mount": "flush", + "purpose": "wall_checkerboard_array", + "position": [ + -4.985, + -1.4999999999999998, + 2.33 + ], + "rotation_deg": [ + 90, + 0, + -90 + ] + }, + { + "id": "back_wall_r01_c01", + "wall": "back", + "mount": "flush", + "purpose": "wall_checkerboard_array", + "position": [ + -4.985, + 0.0, + 2.33 + ], + "rotation_deg": [ + 90, + 0, + -90 + ] + }, + { + "id": "back_wall_r01_c02", + "wall": "back", + "mount": "flush", + "purpose": "wall_checkerboard_array", + "position": [ + -4.985, + 1.4999999999999998, + 2.33 + ], + "rotation_deg": [ + 90, + 0, + -90 + ] + }, + { + "id": "left_wall_r00_c00", + "wall": "left", + "mount": "flush", + "purpose": "wall_checkerboard_array", + "position": [ + -3.749999999999999, + 2.985, + 1.2100000000000002 + ], + "rotation_deg": [ + 90, + 0, + 0 + ] + }, + { + "id": "left_wall_r00_c01", + "wall": "left", + "mount": "flush", + "purpose": "wall_checkerboard_array", + "position": [ + -2.249999999999999, + 2.985, + 1.2100000000000002 + ], + "rotation_deg": [ + 90, + 0, + 0 + ] + }, + { + "id": "left_wall_r00_c02", + "wall": "left", + "mount": "flush", + "purpose": "wall_checkerboard_array", + "position": [ + -0.7499999999999996, + 2.985, + 1.2100000000000002 + ], + "rotation_deg": [ + 90, + 0, + 0 + ] + }, + { + "id": "left_wall_r00_c03", + "wall": "left", + "mount": "flush", + "purpose": "wall_checkerboard_array", + "position": [ + 0.75, + 2.985, + 1.2100000000000002 + ], + "rotation_deg": [ + 90, + 0, + 0 + ] + }, + { + "id": "left_wall_r00_c04", + "wall": "left", + "mount": "flush", + "purpose": "wall_checkerboard_array", + "position": [ + 2.25, + 2.985, + 1.2100000000000002 + ], + "rotation_deg": [ + 90, + 0, + 0 + ] + }, + { + "id": "left_wall_r00_c05", + "wall": "left", + "mount": "flush", + "purpose": "wall_checkerboard_array", + "position": [ + 3.75, + 2.985, + 1.2100000000000002 + ], + "rotation_deg": [ + 90, + 0, + 0 + ] + }, + { + "id": "left_wall_r01_c00", + "wall": "left", + "mount": "flush", + "purpose": "wall_checkerboard_array", + "position": [ + -3.749999999999999, + 2.985, + 2.33 + ], + "rotation_deg": [ + 90, + 0, + 0 + ] + }, + { + "id": "left_wall_r01_c01", + "wall": "left", + "mount": "flush", + "purpose": "wall_checkerboard_array", + "position": [ + -2.249999999999999, + 2.985, + 2.33 + ], + "rotation_deg": [ + 90, + 0, + 0 + ] + }, + { + "id": "left_wall_r01_c02", + "wall": "left", + "mount": "flush", + "purpose": "wall_checkerboard_array", + "position": [ + -0.7499999999999996, + 2.985, + 2.33 + ], + "rotation_deg": [ + 90, + 0, + 0 + ] + }, + { + "id": "left_wall_r01_c03", + "wall": "left", + "mount": "flush", + "purpose": "wall_checkerboard_array", + "position": [ + 0.75, + 2.985, + 2.33 + ], + "rotation_deg": [ + 90, + 0, + 0 + ] + }, + { + "id": "left_wall_r01_c04", + "wall": "left", + "mount": "flush", + "purpose": "wall_checkerboard_array", + "position": [ + 2.25, + 2.985, + 2.33 + ], + "rotation_deg": [ + 90, + 0, + 0 + ] + }, + { + "id": "left_wall_r01_c05", + "wall": "left", + "mount": "flush", + "purpose": "wall_checkerboard_array", + "position": [ + 3.75, + 2.985, + 2.33 + ], + "rotation_deg": [ + 90, + 0, + 0 + ] + }, + { + "id": "right_wall_r00_c00", + "wall": "right", + "mount": "flush", + "purpose": "wall_checkerboard_array", + "position": [ + -3.749999999999999, + -2.985, + 1.2100000000000002 + ], + "rotation_deg": [ + 90, + 0, + 180 + ] + }, + { + "id": "right_wall_r00_c01", + "wall": "right", + "mount": "flush", + "purpose": "wall_checkerboard_array", + "position": [ + -2.249999999999999, + -2.985, + 1.2100000000000002 + ], + "rotation_deg": [ + 90, + 0, + 180 + ] + }, + { + "id": "right_wall_r00_c02", + "wall": "right", + "mount": "flush", + "purpose": "wall_checkerboard_array", + "position": [ + -0.7499999999999996, + -2.985, + 1.2100000000000002 + ], + "rotation_deg": [ + 90, + 0, + 180 + ] + }, + { + "id": "right_wall_r00_c03", + "wall": "right", + "mount": "flush", + "purpose": "wall_checkerboard_array", + "position": [ + 0.75, + -2.985, + 1.2100000000000002 + ], + "rotation_deg": [ + 90, + 0, + 180 + ] + }, + { + "id": "right_wall_r00_c04", + "wall": "right", + "mount": "flush", + "purpose": "wall_checkerboard_array", + "position": [ + 2.25, + -2.985, + 1.2100000000000002 + ], + "rotation_deg": [ + 90, + 0, + 180 + ] + }, + { + "id": "right_wall_r01_c00", + "wall": "right", + "mount": "flush", + "purpose": "wall_checkerboard_array", + "position": [ + -3.749999999999999, + -2.985, + 2.33 + ], + "rotation_deg": [ + 90, + 0, + 180 + ] + }, + { + "id": "right_wall_r01_c01", + "wall": "right", + "mount": "flush", + "purpose": "wall_checkerboard_array", + "position": [ + -2.249999999999999, + -2.985, + 2.33 + ], + "rotation_deg": [ + 90, + 0, + 180 + ] + }, + { + "id": "right_wall_r01_c02", + "wall": "right", + "mount": "flush", + "purpose": "wall_checkerboard_array", + "position": [ + -0.7499999999999996, + -2.985, + 2.33 + ], + "rotation_deg": [ + 90, + 0, + 180 + ] + }, + { + "id": "right_wall_r01_c03", + "wall": "right", + "mount": "flush", + "purpose": "wall_checkerboard_array", + "position": [ + 0.75, + -2.985, + 2.33 + ], + "rotation_deg": [ + 90, + 0, + 180 + ] + }, + { + "id": "right_wall_r01_c04", + "wall": "right", + "mount": "flush", + "purpose": "wall_checkerboard_array", + "position": [ + 2.25, + -2.985, + 2.33 + ], + "rotation_deg": [ + 90, + 0, + 180 + ] + } + ] + }, + "down_camera_intrinsic_target": { + "enabled": true, + "texture_path": "/home/nvidia/study/AutoCalib-Workshop/agv_calib_brain/.isaac_cache/down_camera_charuco.png", + "pattern": "charuco", + "squares_x": 30, + "squares_y": 10, + "square_size_x_m": 0.09000000000000001, + "square_size_y_m": 0.09, + "layout": [ + { + "id": "down_camera_3d_charuco_ramp", + "target_type": "down_camera_intrinsic", + "pattern": "charuco", + "purpose": "intrinsic_calibration_with_depth_and_pose_gradient", + "center": [ + 0.0, + -2.15, + 0.028499999999999998 + ], + "length_m": 2.7, + "width_m": 0.9, + "squares_x": 30, + "squares_y": 10, + "square_size_m": 0.09, + "min_height_m": 0.012, + "max_height_m": 0.045, + "recommended_drive_axis": "+x", + "recommended_speed_mps": 0.1, + "surfaces": [ + { + "id": "low_flat_charuco", + "type": "flat", + "x_range_m": [ + -1.35, + -0.756 + ], + "z_range_m": [ + 0.012, + 0.012 + ] + }, + { + "id": "continuous_slope_charuco", + "type": "continuous_slope", + "x_range_m": [ + -0.756, + 0.6479999999999999 + ], + "z_range_m": [ + 0.012, + 0.045 + ] + }, + { + "id": "high_flat_charuco", + "type": "flat", + "x_range_m": [ + 0.6479999999999999, + 1.35 + ], + "z_range_m": [ + 0.045, + 0.045 + ] + } + ] + } + ] + } + }, + "ros_topics": { + "cmd_vel": "/vehicle/demo_agv_001/actuator/cmd_vel", + "camera_image": "/AutoCalib_Workshop/camera/image_raw", + "lidar_prefix": "/AutoCalib_Workshop/lidar", + "front_camera_image": "/sensor/front_camera/image_raw", + "down_camera_image": "/sensor/down_camera/image_raw", + "lidar_3d_pointcloud": "/sensor/lidar_3d/pointcloud", + "lidar_2d_scan": "/sensor/lidar_2d/scan", + "imu": "/sensor/imu/data", + "external_telemetry": "/isaac/external_localization/telemetry", + "chassis_telemetry": "/chassis/telemetry", + "control_telemetry": "/control/telemetry", + "sensor_telemetry": "/sensor_calibration/telemetry" + }, + "chassis_calibration": { + "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_track_m": 0.52, + "wheel_base_m": 0.8 + }, + "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" + }, + "external_truth": { + "localization_source_id": "isaac_sim_truth_source", + "reference_source_name": "isaac_sim_truth_source", + "workcell_zone_id": "isaac_workcell_zone_a", + "expected_position_stddev_m": 0.01, + "expected_yaw_stddev_rad": 0.01, + "expected_time_sync_offset_ms": 2.0 + }, + "sensors": [ + { + "sensor_id": "demo_front_camera", + "sensor_type": "front_camera", + "frame_id": "front_camera_link", + "image_topic": "/sensor/front_camera/image_raw", + "telemetry_topic": "/sensor_calibration/telemetry", + "mount_pose": { + "x_m": 0.82, + "y_m": 0.0, + "z_m": 0.32, + "roll_rad": 0.0, + "pitch_rad": -1.5707963267948966, + "yaw_rad": 0.0 + } + }, + { + "sensor_id": "demo_down_camera", + "sensor_type": "down_camera", + "frame_id": "down_camera_link", + "image_topic": "/sensor/down_camera/image_raw", + "mount_pose": { + "x_m": 0.4, + "y_m": 0.0, + "z_m": 0.3, + "roll_rad": 0.0, + "pitch_rad": 0.0, + "yaw_rad": 0.0 + } + }, + { + "sensor_id": "demo_lidar_3d", + "sensor_type": "lidar_3d", + "frame_id": "lidar_3d_link", + "pointcloud_topic": "/sensor/lidar_3d/pointcloud", + "mount_pose": { + "x_m": 0.55, + "y_m": 0.0, + "z_m": 0.46, + "roll_rad": 0.0, + "pitch_rad": 0.0, + "yaw_rad": 0.0 + } + }, + { + "sensor_id": "demo_lidar_2d", + "sensor_type": "lidar_2d", + "frame_id": "lidar_2d_link", + "scan_topic": "/sensor/lidar_2d/scan", + "mount_pose": { + "x_m": 0.7, + "y_m": 0.0, + "z_m": 0.24, + "roll_rad": 0.0, + "pitch_rad": 0.0, + "yaw_rad": 0.0 + } + }, + { + "sensor_id": "demo_imu", + "sensor_type": "imu", + "frame_id": "imu_link", + "imu_topic": "/sensor/imu/data", + "mount_pose": { + "x_m": 0.4, + "y_m": 0.0, + "z_m": 0.26, + "roll_rad": 0.0, + "pitch_rad": 0.0, + "yaw_rad": 0.0 + } + } + ], + "lidar_2d_calibration_targets": { + "enabled": true, + "target_width_m": 0.82, + "target_height_m": 1.6, + "target_thickness_m": 0.04, + "target_bottom_z_m": 0.08, + "slope_angle_deg": 45.0, + "targets": [ + { + "id": "front_vertical_reference_panel", + "wall": "front", + "type": "vertical_reference", + "position": [ + 4.920000000000001, + -2.34, + 0.88 + ], + "scale": [ + 0.04, + 0.82, + 1.6 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "nominal_plane": "x = room_length/2 - standoff - thickness" + }, + { + "id": "front_45deg_height_encoding_panel", + "wall": "front", + "type": "height_encoding_slope", + "position": [ + 4.1000000000000005, + -1.4, + 0.88 + ], + "scale": [ + 0.04, + 0.82, + 2.262741699796952 + ], + "rotation_deg": [ + 0.0, + -45.0, + 0.0 + ], + "slope_angle_deg": 45.0, + "height_to_range_sign": "higher_scan_plane_farther_from_front_wall" + }, + { + "id": "right_vertical_reference_panel", + "wall": "right", + "type": "vertical_reference", + "position": [ + 3.87, + -2.92, + 0.88 + ], + "scale": [ + 0.82, + 0.04, + 1.6 + ], + "rotation_deg": [ + 0.0, + 0.0, + 0.0 + ], + "nominal_plane": "y = -room_width/2 + standoff + thickness" + } + ] + }, + "orchestrator_session_config_hint": { + "vehicle_id": "demo_agv_001", + "localization_source_id": "isaac_sim_truth_source", + "workcell_zone_id": "isaac_workcell_zone_a", + "reference_target_id": "isaac_external_truth" + } +} diff --git a/agv_calib_brain/README.md b/agv_calib_brain/README.md new file mode 100644 index 0000000..e896c6b --- /dev/null +++ b/agv_calib_brain/README.md @@ -0,0 +1,134 @@ +# AGV 自动标定车间 - 总述 + +本项目是 AGV(自动导引车)自动化标定车间的软件系统,用于在车间环境中对 AGV 进行底盘参数标定、运控参数标定、传感器内参/外参标定,以及手眼标定。 + +## 项目概述 + +### 核心能力 + +| 标定类型 | 说明 | 状态 | +|---------|------|------| +| 外部真值接入 | 通过外部定位系统(如 OptiTrack、激光雷达定位)获取车辆位姿真值 | ✅ 已验证 | +| 底盘参数标定 | 标定轮距、轴距、转向角等底盘几何参数 | ✅ 已验证(模板) | +| 运控参数标定 | 标定横向/纵向控制器的 PID/MPC/LQR 参数 | ✅ 已验证(模板) | +| 传感器内参标定 | 相机(前视/下视)、IMU 的内参标定 | ✅ 已验证(模板) | +| 传感器外参标定 | 相机、雷达等传感器相对于车体坐标系的外参标定 | 🚧 待完善 | +| 手眼标定 | 机械臂与相机之间的变换关系标定 | 🚧 待开发 | + +### 技术架构 + +``` +┌─────────────────────────────────────────────────────────────────┐ +│ 车间工控机(Ubuntu) │ +│ ┌─────────────────┐ ┌──────────────┐ ┌─────────────────────┐ │ +│ │ workshop_ │ │ 底盘标定服务 │ │ 传感器标定服务 │ │ +│ │ orchestrator_v2 │ │ chassis_ │ │ sensor_calibration_ │ │ +│ │ (总控) │ │ calibration │ │ service │ │ +│ └────────┬────────┘ └──────────────┘ └─────────────────────┘ │ +│ │ │ +│ ┌────────▼────────┐ ┌──────────────┐ ┌─────────────────────┐ │ +│ │ vehicle_agent_ │ │ 运控标定服务 │ │ 外部位姿服务 │ │ +│ │ gateway │ │ control_ │ │ external_ │ │ +│ │ (WiFi6/TCP网关) │ │ calibration │ │ localization_ │ │ +│ └─────────────────┘ └──────────────┘ └─────────────────────┘ │ +└─────────────────────────────────────────────────────────────────┘ + │ WiFi6/TCP + ▼ +┌─────────────────────────────────────────────────────────────────┐ +│ 车端电脑(Windows/Linux) │ +│ ┌─────────────────┐ ┌──────────────┐ ┌─────────────────────┐ │ +│ │ 车端 Agent │ │ 底盘控制器 │ │ 传感器驱动 │ │ +│ │ (TCP<->CAN/PLC) │ │ (CAN/PLC) │ │ (相机/雷达/IMU) │ │ +│ └─────────────────┘ └──────────────┘ └─────────────────────┘ │ +└─────────────────────────────────────────────────────────────────┘ +``` + +## 目录结构 + +``` +agv_calib_brain/ +├── src/ +│ ├── core/ # 核心标定逻辑 +│ │ ├── workshop_orchestrator/ # 车间总控编排器 +│ │ ├── chassis_calibration_service/ # 底盘标定服务 +│ │ ├── control_calibration_service/ # 运控标定服务 +│ │ ├── sensor_calibration_service/ # 传感器标定服务 +│ │ ├── external_localization_service/ # 外部位姿服务 +│ │ └── vehicle_profile_manager/ # 车辆画像管理 +│ │ +│ ├── communication/ # 通信层 +│ │ ├── win_ubuntu_bridge/ # 车间<->车端通信桥 +│ │ └── interfaces/ # ROS2 接口定义 +│ │ +│ ├── simulation/ # 仿真验证 +│ │ ├── isaac_workshop_sim/ # Isaac Sim 标定车间 +│ │ ├── vehicle_agent_sim/ # 仿真车端 Agent +│ │ ├── vehicle_sensor_agent_sim/ # 仿真传感器 Agent +│ │ └── tools/ # 仿真工具脚本 +│ │ +│ ├── deployment/ # 部署配置 +│ │ └── profiles/ # 仿真/部署配置文件 +│ │ +│ ├── site_deployment/ # 真实现场部署 +│ │ └── (真实车辆 SDK、PLC/CAN 适配) +│ │ +│ └── apps/ # 操作员工具 +│ +├── run_isaac_real_sim_test.sh # 一键启动 Isaac 仿真测试 +├── stop_isaac_real_sim_stack.sh # 停止仿真栈 +├── SIMULATION_GUIDE.md # 仿真环境启动指南 +└── README.md # 本文件 +``` + +## 快速开始 + +### 1. 编译 + +```bash +cd /home/nvidia/study/AutoCalib-Workshop/agv_calib_brain +colcon build +source install/setup.bash +``` + +### 2. 一键启动完整仿真 + +```bash +# 启动 Isaac + 车端 Agent + 车间总控 + 执行验收 +./run_isaac_real_sim_test.sh --headless +``` + +### 3. 查看详细指南 + +- **仿真启动指南**: [SIMULATION_GUIDE.md](./SIMULATION_GUIDE.md) - 如何单独启动各组件 +- **源码目录说明**: [src/README.md](./src/README.md) - 源码组织结构 + +## 主要验证场景 + +| 场景 | 启动方式 | 验证内容 | +|------|---------|---------| +| 快速闭环验证 | `launch_sim_stack.py --component vehicle-agent` + `minimal_workshop_demo.launch.py` | 车间总控能创建 session、生成阶段计划、调用车端接口 | +| Isaac topic 检查 | `run_isaac_real_sim_test.sh --headless`(前半段) | Isaac 能发布传感器和真值 topic | +| 完整 Isaac 仿真闭环 | `run_isaac_real_sim_test.sh --headless` | 场景、agent、桥接、总控一起运行 | +| 传感器转发链路 | `--component vehicle-agent,sensor-ingest` | 车端能订阅 Isaac topic,车间端能拉取数据 | + +## 四条核心通信链路 + +| 链路 | 方向 | 功能 | 组件 | +|------|------|------|------| +| 外部真值链路 | 外部系统 → 车端 | 位姿、速度、时间戳 | `external-pose-bridge` | +| 底盘链路 | 车间 → 车端 | 下发标定动作、读取遥测 | `vehicle-agent-gateway` | +| 运控链路 | 车间 → 车端 | 下发控制任务、读取误差 | `vehicle-agent-gateway` | +| 传感器链路 | 车端 → 车间 | 相机、雷达、IMU 数据 | `sensor-ingest` | + +## 开发状态 + +- ✅ **已完成**: 车间总控编排器、四类标定服务框架、车辆画像管理、Isaac 仿真环境、WiFi6/TCP 通信链路、端到端验收脚本 +- 🚧 **进行中**: 传感器外参标定采样链路、真实算法参数收敛 +- ⏳ **待开发**: 手眼标定、真实现场部署验证 + +## 关键设计原则 + +1. **主控只编排流程** - `workshop_orchestrator_v2` 负责阶段调度,具体标定由独立 ROS2 包实现 +2. **仿真与现场分离** - Isaac 代码只在 `simulation/`,真实车辆代码只在 `site_deployment/` +3. **统一通信协议** - 车间与车端通过 WiFi6/TCP 通信,协议由 `communication/` 定义 +4. **车辆画像驱动** - 车辆能力、传感器配置通过 `vehicle_profile` 描述,支持多车型 diff --git a/agv_calib_brain/SIMULATION_GUIDE.md b/agv_calib_brain/SIMULATION_GUIDE.md new file mode 100644 index 0000000..f41f66b --- /dev/null +++ b/agv_calib_brain/SIMULATION_GUIDE.md @@ -0,0 +1,150 @@ +# 仿真环境启动指南 + +本文档说明如何单独启动 Isaac 仿真环境的各个组件。 + +## 前置条件 + +```bash +# 先 source ROS2 环境 +source install/setup.bash +``` + +## 单独启动组件 + +### 1. Isaac 仿真环境 + +```bash +# 带可视化界面 +python3 src/simulation/tools/launch_sim_stack.py --component isaac + +# headless 模式(无界面) +python3 src/simulation/tools/launch_sim_stack.py --component isaac --headless +``` + +### 2. 仿真车端统一 Agent + +包含底盘、运控、传感器数据转发: + +```bash +python3 src/simulation/tools/launch_sim_stack.py --component vehicle-agent +``` + +### 3. 外部位姿桥 + +将 Isaac 真值转发给车端: + +```bash +python3 src/simulation/tools/launch_sim_stack.py --component external-pose-bridge +``` + +### 4. 车间侧传感器 Ingest + +从车端轮询传感器数据: + +```bash +python3 src/simulation/tools/launch_sim_stack.py --component sensor-ingest +``` + +### 5. 车间 Gateway + +ROS2 节点,对接底盘/运控: + +```bash +python3 src/simulation/tools/launch_sim_stack.py --component gateway +``` + +## 启动完整仿真链路 + +### 方式一:一键启动所有仿真组件(不含车间总控) + +```bash +python3 src/simulation/tools/launch_sim_stack.py --component all +``` + +### 方式二:分步启动(推荐用于调试) + +终端 1 - 启动 Isaac: +```bash +python3 src/simulation/tools/launch_sim_stack.py --component isaac +``` + +终端 2 - 启动车端 Agent: +```bash +python3 src/simulation/tools/launch_sim_stack.py --component vehicle-agent +``` + +终端 3 - 启动外部位姿桥: +```bash +python3 src/simulation/tools/launch_sim_stack.py --component external-pose-bridge +``` + +终端 4 - 启动传感器 Ingest: +```bash +python3 src/simulation/tools/launch_sim_stack.py --component sensor-ingest +``` + +终端 5 - 启动车间 Gateway: +```bash +python3 src/simulation/tools/launch_sim_stack.py --component gateway +``` + +终端 6 - 启动车间总控: +```bash +ros2 launch win_ubuntu_bridge minimal_workshop_demo.launch.py \ + use_gateway:=true \ + chassis_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 +``` + +## 验证仿真环境 + +启动后检查 topic: + +```bash +# 查看 Isaac 发布的传感器数据 +ros2 topic list | grep isaac + +# 检查各传感器数据频率 +ros2 topic hz /sensor/front_camera/image_raw +ros2 topic hz /sensor/down_camera/image_raw +ros2 topic hz /sensor/lidar_3d/pointcloud +ros2 topic hz /sensor/lidar_2d/scan +ros2 topic hz /sensor/imu/data + +# 检查外部位姿 +ros2 topic hz /isaac/external_localization/telemetry +``` + +## 执行端到端验收测试 + +仿真环境启动后,执行验收: + +```bash +python3 src/simulation/tools/smoke_test_workshop_orchestrator.py \ + --verbose-feedback \ + --no-publish-fake-external-telemetry +``` + +## 停止仿真环境 + +按 `Ctrl+C` 停止各组件,或使用: + +```bash +./stop_isaac_real_sim_stack.sh +``` + +## 快捷脚本 + +也可以使用一键测试脚本(启动所有组件并执行验收): + +```bash +# 完整测试(启动 + 验收 + 停止) +./run_isaac_real_sim_test.sh + +# 保持运行(不执行验收) +./run_isaac_real_sim_test.sh --no-smoke --keep-running + +# headless 模式 +./run_isaac_real_sim_test.sh --headless +``` diff --git a/agv_calib_brain/run_isaac_real_sim_test.sh b/agv_calib_brain/run_isaac_real_sim_test.sh new file mode 100755 index 0000000..ceae4a7 --- /dev/null +++ b/agv_calib_brain/run_isaac_real_sim_test.sh @@ -0,0 +1,499 @@ +#!/usr/bin/env bash +# 一键启动 Isaac 真实仿真闭环测试。 +# +# 默认流程: +# 1. 使用 conda 环境 AutoCalib_Workshop 启动 Isaac 标定车间; +# 2. 启动仿真车端统一 agent、外部位姿桥和车间侧传感器 ingest; +# 3. 启动车间总控 demo; +# 4. 使用 Isaac 真实发布的真值/传感器数据执行 orchestrator 端到端验收; +# 5. 验收结束后自动停止后台进程。 + +set -Eeuo pipefail + +WORKSPACE_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +CONDA_ENV_NAME="${CONDA_ENV_NAME:-AutoCalib_Workshop}" +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)}" + +HEADLESS=0 +DO_BUILD=0 +RUN_SMOKE=1 +KEEP_RUNNING=0 +WITH_SENSOR_INGEST=1 +WAIT_FOR_ISAAC_TOPICS=1 +ISAAC_WAIT_SEC="${ISAAC_WAIT_SEC:-180}" +WORKSHOP_WARMUP_SEC="${WORKSHOP_WARMUP_SEC:-5}" +START_GRACE_SEC="${START_GRACE_SEC:-2}" +SMOKE_TASKS="" + +PIDS=() +NAMES=() +LOGS=() +CLEANED=0 + +usage() { + cat <<'EOF' +用法: + ./run_isaac_real_sim_test.sh [选项] + +默认会验证: + 1. Isaac 发布外部真值、前视相机、下视相机、3D 雷达、2D 雷达、IMU topic。 + 2. 仿真车端统一 agent、外部位姿桥、车间侧传感器 ingest 都能启动并通信。 + 3. 车间总控能创建 session,并默认执行 external,chassis,control,sensor_intrinsic 阶段。 + 4. 传感器数据能从 Isaac -> 车端 -> WiFi6/TCP -> 车间侧链路流动。 + +当前不验证: + 1. 真实底盘参数是否已经收敛。 + 2. 真实相机内参、传感器外参是否已经被算法正确求解。 + 3. 棋盘格角点数量、点云几何质量、时间同步精度等采样质量。 + +选项: + --headless 以 headless 模式启动 Isaac。 + --build 启动前先执行 colcon build。 + --no-smoke 只启动仿真链路,不执行 orchestrator 验收。 + --keep-running 验收结束后保持仿真链路运行,按 Ctrl+C 停止。 + --no-sensor-ingest 不启动车间侧传感器 ingest 轮询进程。 + --skip-topic-check 不等待 Isaac 真值/传感器 topic,直接继续启动后续链路。 + --isaac-wait-sec SEC 等待 Isaac topic 的最长时间,默认 180 秒。 + --warmup-sec SEC 总控 demo 启动后等待采集真值历史的时间,默认 5 秒。 + --tasks LIST 覆盖 smoke_test_workshop_orchestrator.py 的任务列表。 + 例: external,chassis,control,sensor_intrinsic + --conda-env NAME Isaac 进程使用的 conda 环境,默认 AutoCalib_Workshop。 + -h, --help 显示帮助。 + +环境变量: + CONDA_ENV_NAME 同 --conda-env。 + ROS_LOG_DIR ROS 日志目录,默认 /tmp/roslog。 + RUN_LOG_ROOT 本脚本各后台进程日志目录。 + +示例: + ./run_isaac_real_sim_test.sh --headless + ./run_isaac_real_sim_test.sh --headless --keep-running + ./run_isaac_real_sim_test.sh --no-smoke +EOF +} + +log() { + echo "[INFO] $*" +} + +warn() { + echo "[WARN] $*" >&2 +} + +die() { + echo "[ERROR] $*" >&2 + exit 1 +} + +while [[ $# -gt 0 ]]; do + case "$1" in + --headless) + HEADLESS=1 + shift + ;; + --build) + DO_BUILD=1 + shift + ;; + --no-smoke) + RUN_SMOKE=0 + KEEP_RUNNING=1 + shift + ;; + --keep-running) + KEEP_RUNNING=1 + shift + ;; + --no-sensor-ingest) + WITH_SENSOR_INGEST=0 + shift + ;; + --skip-topic-check) + WAIT_FOR_ISAAC_TOPICS=0 + shift + ;; + --isaac-wait-sec) + [[ $# -ge 2 ]] || die "--isaac-wait-sec 需要参数" + ISAAC_WAIT_SEC="$2" + shift 2 + ;; + --warmup-sec) + [[ $# -ge 2 ]] || die "--warmup-sec 需要参数" + WORKSHOP_WARMUP_SEC="$2" + shift 2 + ;; + --tasks) + [[ $# -ge 2 ]] || die "--tasks 需要参数" + SMOKE_TASKS="$2" + shift 2 + ;; + --conda-env) + [[ $# -ge 2 ]] || die "--conda-env 需要参数" + CONDA_ENV_NAME="$2" + shift 2 + ;; + -h|--help) + usage + exit 0 + ;; + *) + die "未知参数: $1" + ;; + esac +done + +resolve_conda_sh() { + if [[ -n "${CONDA_SH:-}" && -f "${CONDA_SH}" ]]; then + echo "${CONDA_SH}" + return 0 + fi + + local candidate + for candidate in \ + "${HOME}/anaconda3/etc/profile.d/conda.sh" \ + "${HOME}/miniconda3/etc/profile.d/conda.sh" \ + "/opt/conda/etc/profile.d/conda.sh" + do + if [[ -f "${candidate}" ]]; then + echo "${candidate}" + return 0 + fi + done + + if command -v conda >/dev/null 2>&1; then + local base + base="$(conda info --base 2>/dev/null || true)" + if [[ -n "${base}" && -f "${base}/etc/profile.d/conda.sh" ]]; then + echo "${base}/etc/profile.d/conda.sh" + return 0 + fi + fi + + return 1 +} + +source_ros_env() { + set +u + if [[ -f /opt/ros/humble/setup.bash ]]; then + source /opt/ros/humble/setup.bash + fi + source "${WORKSPACE_DIR}/install/setup.bash" + set -u +} + +cleanup() { + if [[ "${CLEANED}" -eq 1 ]]; then + return + fi + CLEANED=1 + + if [[ "${#PIDS[@]}" -eq 0 ]]; then + return + fi + + log "正在停止后台进程..." + local i pid name + for ((i=${#PIDS[@]}-1; i>=0; i--)); do + pid="${PIDS[$i]}" + name="${NAMES[$i]}" + if kill -0 "${pid}" >/dev/null 2>&1; then + log "发送 SIGINT: ${name} pid=${pid}" + kill -INT "${pid}" >/dev/null 2>&1 || true + fi + done + + sleep 3 + for ((i=${#PIDS[@]}-1; i>=0; i--)); do + pid="${PIDS[$i]}" + name="${NAMES[$i]}" + if kill -0 "${pid}" >/dev/null 2>&1; then + warn "${name} 未退出,发送 SIGTERM: pid=${pid}" + kill -TERM "${pid}" >/dev/null 2>&1 || true + fi + done + + sleep 2 + for ((i=${#PIDS[@]}-1; i>=0; i--)); do + pid="${PIDS[$i]}" + name="${NAMES[$i]}" + if kill -0 "${pid}" >/dev/null 2>&1; then + warn "${name} 仍未退出,发送 SIGKILL: pid=${pid}" + kill -KILL "${pid}" >/dev/null 2>&1 || true + fi + done +} + +on_signal() { + trap - INT TERM + cleanup + exit 130 +} + +trap cleanup EXIT +trap on_signal INT TERM + +start_bg() { + local name="$1" + shift + local logfile="${RUN_LOG_ROOT}/${name}.log" + + log "启动 ${name},日志: ${logfile}" + "$@" >"${logfile}" 2>&1 & + local pid=$! + PIDS+=("${pid}") + NAMES+=("${name}") + LOGS+=("${logfile}") + + sleep "${START_GRACE_SEC}" + if ! kill -0 "${pid}" >/dev/null 2>&1; then + warn "${name} 启动后立即退出,最近日志如下:" + tail -n 120 "${logfile}" >&2 || true + exit 1 + fi +} + +wait_for_tcp_port() { + local host="$1" + local port="$2" + local timeout_sec="$3" + local name="$4" + local deadline=$((SECONDS + timeout_sec)) + + log "等待 ${name} TCP ${host}:${port}" + while (( SECONDS < deadline )); do + if timeout 1 bash -c "/dev/null 2>&1; then + log "${name} 已就绪" + return 0 + fi + sleep 1 + done + + warn "等待 ${name} 超时" + return 1 +} + +wait_for_ros_topic_once() { + local topic="$1" + local timeout_sec="$2" + local deadline=$((SECONDS + timeout_sec)) + local echo_args=(--qos-reliability best_effort --once "${topic}") + + case "${topic}" in + /sensor/front_camera/*|/sensor/down_camera/*|/sensor/lidar_3d/*) + echo_args=(--qos-reliability best_effort --once "${topic}" --field header) + ;; + esac + + log "等待 Isaac topic: ${topic}" + while (( SECONDS < deadline )); do + if timeout 8s ros2 topic echo "${echo_args[@]}" >/dev/null 2>&1; then + log "已收到 topic: ${topic}" + return 0 + fi + sleep 2 + done + + warn "等待 topic 超时: ${topic}" + warn "可检查 Isaac 日志: ${RUN_LOG_ROOT}/isaac.log" + if [[ -f "${RUN_LOG_ROOT}/isaac.log" ]]; then + warn "Isaac 最近日志:" + tail -n 120 "${RUN_LOG_ROOT}/isaac.log" >&2 || true + fi + return 1 +} + +wait_for_ros_service() { + local service="$1" + local timeout_sec="$2" + local deadline=$((SECONDS + timeout_sec)) + + log "等待 ROS service: ${service}" + while (( SECONDS < deadline )); do + if ros2 service list 2>/dev/null | grep -qx "${service}"; then + log "service 已就绪: ${service}" + return 0 + fi + sleep 1 + done + + warn "等待 service 超时: ${service}" + return 1 +} + +check_no_existing_workshop_orchestrator() { + local nodes + nodes="$(timeout 5s ros2 node list 2>/dev/null || true)" + if [[ -z "${nodes}" ]]; then + return + fi + + local count + count="$(printf '%s\n' "${nodes}" | grep -xc "/workshop_orchestrator_v2" || true)" + if [[ "${count}" -gt 0 ]]; then + die "检测到已有 /workshop_orchestrator_v2 节点。请先关闭旧的 workshop-demo/仿真脚本,或执行 ./stop_isaac_real_sim_stack.sh --kill 清理遗留进程,否则 create_session 和 execute_session 可能连到不同实例。" + fi +} + +mkdir -p "${ROS_LOG_DIR}" "${RUN_LOG_ROOT}" + +if [[ "${DO_BUILD}" -eq 1 ]]; then + log "执行 colcon build" + ( + cd "${WORKSPACE_DIR}" + set +u + if [[ -f /opt/ros/humble/setup.bash ]]; then + source /opt/ros/humble/setup.bash + fi + set -u + colcon build + ) +fi + +[[ -f "${WORKSPACE_DIR}/install/setup.bash" ]] || die "找不到 install/setup.bash,请先执行 colcon build,或使用 --build" + +CONDA_SH_PATH="$(resolve_conda_sh || true)" +[[ -n "${CONDA_SH_PATH}" ]] || die "找不到 conda.sh,无法激活 conda 环境 ${CONDA_ENV_NAME}" + +source_ros_env + +log "工作空间: ${WORKSPACE_DIR}" +log "ROS_LOG_DIR: ${ROS_LOG_DIR}" +log "进程日志目录: ${RUN_LOG_ROOT}" +log "Isaac conda 环境: ${CONDA_ENV_NAME}" + +export PYTHONUNBUFFERED=1 +check_no_existing_workshop_orchestrator + +ISAAC_HEADLESS_ARG="" +if [[ "${HEADLESS}" -eq 1 ]]; then + ISAAC_HEADLESS_ARG="--headless" +fi + +start_bg "isaac" bash -lc " + set -Eeuo pipefail + cd '${WORKSPACE_DIR}' + set +u + source '${CONDA_SH_PATH}' + conda activate '${CONDA_ENV_NAME}' + if [[ -f /opt/ros/humble/setup.bash ]]; then + source /opt/ros/humble/setup.bash + fi + source '${WORKSPACE_DIR}/install/setup.bash' + set -u + export ROS_LOG_DIR='${ROS_LOG_DIR}' + exec python3 src/simulation/tools/launch_sim_stack.py --component isaac ${ISAAC_HEADLESS_ARG} +" + +if [[ "${WAIT_FOR_ISAAC_TOPICS}" -eq 1 ]]; then + wait_for_ros_topic_once "/isaac/external_localization/telemetry" "${ISAAC_WAIT_SEC}" || exit 1 + wait_for_ros_topic_once "/sensor/front_camera/image_raw" "${ISAAC_WAIT_SEC}" || exit 1 + wait_for_ros_topic_once "/sensor/down_camera/image_raw" "${ISAAC_WAIT_SEC}" || exit 1 + wait_for_ros_topic_once "/sensor/lidar_3d/pointcloud" "${ISAAC_WAIT_SEC}" || exit 1 + wait_for_ros_topic_once "/sensor/imu/data" "${ISAAC_WAIT_SEC}" || exit 1 + wait_for_ros_topic_once "/sensor/lidar_2d/scan" "${ISAAC_WAIT_SEC}" || exit 1 +fi + +start_bg "vehicle-agent" bash -lc " + set -Eeuo pipefail + cd '${WORKSPACE_DIR}' + set +u + if [[ -f /opt/ros/humble/setup.bash ]]; then + source /opt/ros/humble/setup.bash + fi + source '${WORKSPACE_DIR}/install/setup.bash' + set -u + export ROS_LOG_DIR='${ROS_LOG_DIR}' + exec python3 src/simulation/tools/launch_sim_stack.py --component vehicle-agent +" +wait_for_tcp_port "127.0.0.1" 9000 30 "vehicle-agent unified WiFi6" || exit 1 + +start_bg "external-pose-bridge" bash -lc " + set -Eeuo pipefail + cd '${WORKSPACE_DIR}' + set +u + if [[ -f /opt/ros/humble/setup.bash ]]; then + source /opt/ros/humble/setup.bash + fi + source '${WORKSPACE_DIR}/install/setup.bash' + set -u + export ROS_LOG_DIR='${ROS_LOG_DIR}' + exec python3 src/simulation/tools/launch_sim_stack.py --component external-pose-bridge +" + +if [[ "${WITH_SENSOR_INGEST}" -eq 1 ]]; then + start_bg "sensor-ingest" bash -lc " + set -Eeuo pipefail + cd '${WORKSPACE_DIR}' + set +u + if [[ -f /opt/ros/humble/setup.bash ]]; then + source /opt/ros/humble/setup.bash + fi + source '${WORKSPACE_DIR}/install/setup.bash' + set -u + export ROS_LOG_DIR='${ROS_LOG_DIR}' + exec python3 src/simulation/tools/launch_sim_stack.py --component sensor-ingest + " +fi + +start_bg "workshop-demo" bash -lc " + set -Eeuo pipefail + cd '${WORKSPACE_DIR}' + set +u + if [[ -f /opt/ros/humble/setup.bash ]]; then + source /opt/ros/humble/setup.bash + fi + source '${WORKSPACE_DIR}/install/setup.bash' + set -u + export ROS_LOG_DIR='${ROS_LOG_DIR}' + exec ros2 launch win_ubuntu_bridge minimal_workshop_demo.launch.py \ + use_gateway:=true \ + chassis_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 +" + +wait_for_ros_service "/workshop_v2/create_session" 60 || exit 1 + +log "等待 ${WORKSHOP_WARMUP_SEC}s,让 external_localization_service 累积 Isaac 真值历史" +sleep "${WORKSHOP_WARMUP_SEC}" + +SMOKE_STATUS=0 +if [[ "${RUN_SMOKE}" -eq 1 ]]; then + SMOKE_ARGS=( + --verbose-feedback + --no-publish-fake-external-telemetry + ) + if [[ -n "${SMOKE_TASKS}" ]]; then + SMOKE_ARGS+=(--tasks "${SMOKE_TASKS}") + fi + + SMOKE_LOG="${RUN_LOG_ROOT}/smoke.log" + log "执行 orchestrator 端到端验收,日志: ${SMOKE_LOG}" + set +e + ( + cd "${WORKSPACE_DIR}" + source_ros_env + export ROS_LOG_DIR="${ROS_LOG_DIR}" + python3 src/simulation/tools/smoke_test_workshop_orchestrator.py "${SMOKE_ARGS[@]}" + ) 2>&1 | tee "${SMOKE_LOG}" + SMOKE_STATUS=${PIPESTATUS[0]} + set -e + + if [[ "${SMOKE_STATUS}" -eq 0 ]]; then + log "仿真闭环验收通过" + else + warn "仿真闭环验收失败,退出码: ${SMOKE_STATUS}" + fi +else + log "已按 --no-smoke 跳过 orchestrator 验收" +fi + +if [[ "${KEEP_RUNNING}" -eq 1 ]]; then + log "仿真链路保持运行中。按 Ctrl+C 停止。" + while true; do + sleep 3600 + done +fi + +exit "${SMOKE_STATUS}" diff --git a/agv_calib_brain/src/README.md b/agv_calib_brain/src/README.md index 7ed7d72..818ced4 100644 --- a/agv_calib_brain/src/README.md +++ b/agv_calib_brain/src/README.md @@ -1,141 +1,59 @@ -# 🧠 AGV 标定中央大脑 +# 源码目录结构 -**环境**: Ubuntu 22.04 + ROS 2 Humble | **语言**: C++ | **通信**: gRPC over Wi-Fi 6 +`src` 按“仿真、核心逻辑、通信、现场部署”的边界组织,而不是按临时实验文件组织。 -> 标定车间的"发令大脑",通过局域网跨平台遥控 Windows 车端执行动作并拉取遥测数据。 +## 目录说明 ---- +- `apps/` + 面向操作人员的工具和界面原型。 -## 📋 目录 +- `communication/` + ROS 2 接口包、TCP 帧协议、车间工控机到车端电脑的 gateway。这个层应该同时服务于仿真和现场部署。 -1. [系统依赖安装](#1-系统依赖一键安装) -2. [VS Code 插件配置](#2-vs-code-核心插件配置) -3. [解决 IntelliSense 报错](#3-解决-vs-code-红色波浪线) -4. [编译与运行](#4-编译与运行) +- `core/` + 标定流程和标定算法,包括 `workshop_orchestrator`、底盘标定、运控标定、传感器标定、车辆参数管理等可复用核心逻辑。 ---- +- `simulation/` + 部署前仿真验证代码。Isaac 车间、仿真车辆、仿真车端 agent、仿真传感器、仿真标定靶和旧版仿真包都放在这里。 -## 1. 系统依赖一键安装 +- `deployment/` + 部署 profile 和从仿真迁移到现场前的检查清单。这里放配置基准,不放算法实现。 -在 Ubuntu 22.04 终端执行以下命令: +- `docs/` + 源码树内的设计说明、边界说明和迁移规则。 + +- `site_deployment/` + 真实现场部署代码,例如真实车端电脑适配器、真实车辆 SDK、PLC/CAN 或厂商控制器对接代码。 + +## 边界规则 + +- Isaac API 只放在 `simulation/`。 +- 真实车辆 SDK、PLC、CAN、厂商控制器相关代码只放在 `site_deployment/`。 +- ROS 2 接口、TCP 协议和 gateway 放在 `communication/`。 +- 编排流程和标定算法放在 `core/`。 +- 部署 profile 放在 `deployment/`。 +- 设计说明和迁移边界说明放在 `docs/`。 + +## 主要入口 + +Isaac 车间仿真: ```bash -sudo apt update -sudo apt install -y build-essential cmake pkg-config gdb -sudo apt install -y protobuf-compiler-grpc libgrpc++-dev libprotobuf-dev protobuf-compiler +python3 src/simulation/isaac_workshop_sim/scripts/build_calibration_room.py ``` -> ⚠️ **警告**: 严禁自行去 GitHub 源码编译 gRPC,直接使用 Ubuntu 官方 APT 源即可,避免浪费时间与报错。 - ---- - -## 2. VS Code 核心插件配置 - -打开 VS Code → 扩展商店 (Extensions),**必须安装**以下 4 个插件: - -| 插件名称 | 开发者 | 用途 | -|---------|--------|------| -| **C/C++** | Microsoft | 代码补全与 GDB 调试 | -| **CMake Tools** | Microsoft | 底部快速构建状态栏 | -| **ROS** | Microsoft | 自动识别 `colcon` 工作空间 | -| **vscode-proto3** | zxh404 | `.proto` 文件语法高亮 | - ---- - -## 3. 解决 VS Code 红色波浪线 (IntelliSense 报错) - -**问题原因**: gRPC 生成的 `.pb.h` 文件在 `colcon build` 阶段动态生成于 `build/` 目录,VS Code 初始无法识别。 - -**修复步骤**: - -1. 按 `Ctrl+Shift+P` → 输入 `C/C++: Edit Configurations (JSON)` -2. 确保 `c_cpp_properties.json` 包含以下配置: - -```json -{ - "configurations": [ - { - "name": "ROS2", - "includePath": [ - "${workspaceFolder}/**", - "/opt/ros/humble/include/**", - "${workspaceFolder}/build/agv_calib_brain/grpc_gen/**" - ], - "compilerPath": "/usr/bin/gcc", - "cStandard": "c17", - "cppStandard": "c++17", - "intelliSenseMode": "linux-gcc-x64" - } - ] -} -``` - -> 💡 **提示**: `grpc_gen` 是 CMakeLists 中配置的自动生成源码路径,请根据实际情况微调。 - ---- - -## 4. 编译与运行 (CMake 自动化) - -### 4.0 最小联调闭环 - -当前仓库已经补齐了一个最小可运行闭环: - -- `vehicle_profile_manager`:提供默认车辆画像 -- `external_localization_service`:提供外部真值校核的最小执行端 -- `workshop_orchestrator_v2`:负责编排会话、计划和报告 - -启动顺序: +仿真车端 agent: ```bash -source /opt/ros/humble/setup.bash -source install/setup.bash -ros2 launch win_ubuntu_bridge minimal_workshop_demo.launch.py +python3 src/simulation/vehicle_agent_sim/scripts/isaac_vehicle_agent_sim.py ``` -如果只想单独跑编排器: +按 profile 启动完整仿真链路: ```bash -ros2 launch workshop_orchestrator_v2 workshop_orchestrator_v2.launch.py +python3 src/simulation/tools/launch_sim_stack.py ``` -可先调用这些接口做联调: +这条链路中,车间工控机与车端电脑之间的底盘、运控、外部真值位姿和传感器数据都通过 TCP/WiFi6 仿真边界传输;Isaac topic 只留在仿真内部。 -- `/vehicle_profile_manager/get_vehicle_profile` -- `/vehicle_profile_manager/evaluate_vehicle_calibration_applicability` -- `/external_localization/get_readiness` -- `/external_localization/execute_task` -- `/workshop_v2/create_session` -- `/workshop_v2/execute_session` -- `/workshop_v2/get_report` - -最小会话建议至少包含: - -- `session.config.localization_source_id = demo_vehicle_001` -- `session.config.workcell_zone_id = demo_workcell` -- 一个 `requested_tasks`,其中 `stage_type = EXTERNAL_REFERENCE_READY_CHECK_STAGE` -- 该任务的 `task_params` 至少包含: - - `external.static_sample_count` - - `external.dynamic_sample_count` - - `external.max_position_stddev_m` - - `external.max_yaw_stddev_rad` - - `external.max_tracking_loss_ratio` - - `external.max_time_sync_offset_ms` - - `external.timeout_sec` - - -> ✨ **无需手动执行 `protoc`** —— CMakeLists.txt 已配置自动化脚本,编译时自动生成 C++ 网络源码。 - -### 4.1 编译 - -```bash -# 回到工作空间根目录(如 ~/agv_ws) -source /opt/ros/humble/setup.bash -colcon build --packages-select agv_calib_brain --symlink-install -``` - -### 4.2 运行 - -```bash -source install/setup.bash -ros2 run agv_calib_brain brain_node -``` \ No newline at end of file +车间 gateway 和 `workshop_orchestrator` 仍然按 ROS 2 包名启动;源码分别在 `communication/` 和 `core/` 下。 diff --git a/agv_calib_brain/src/agv_calib_core/chassis_calibration_service/src/differential_chassis_algorithm.cpp b/agv_calib_brain/src/agv_calib_core/chassis_calibration_service/src/differential_chassis_algorithm.cpp deleted file mode 100644 index 2ff4629..0000000 --- a/agv_calib_brain/src/agv_calib_core/chassis_calibration_service/src/differential_chassis_algorithm.cpp +++ /dev/null @@ -1,24 +0,0 @@ -#include "chassis_calibration_service/chassis_calibration_common.hpp" - -namespace chassis_calibration_service -{ - -bool DifferentialChassisAlgorithm::run( - const ChassisCalibrationInput & input, - ChassisCalibrationOutput & output, - std::string & failure_reason) const -{ - (void)failure_reason; - - // TODO: 在这里填写差速底盘标定算法。 - // 算法工程师应从这里读取并计算: - // - 任务请求:input.request(request_id、任务目的、selected_primitive、straight_line 参数、timeout 等) - // - 底盘反馈:由上层扩展到 ChassisCalibrationInput 中的实时数据(轮速、左右电机反馈、IMU、里程计、定位等) - // - 输出:output.response.result(success、error_code、validation_summary、estimated_params、artifacts 等) - fill_common_result(input, output); - output.response.result.message = "differential chassis template executed."; - output.response.result.recommended_parameter_version = "differential_template_v1"; - return true; -} - -} // namespace chassis_calibration_service diff --git a/agv_calib_brain/src/agv_calib_core/control_calibration_service/src/pure_pursuit_control_calibration_algorithm.cpp b/agv_calib_brain/src/agv_calib_core/control_calibration_service/src/pure_pursuit_control_calibration_algorithm.cpp deleted file mode 100644 index a9b20dc..0000000 --- a/agv_calib_brain/src/agv_calib_core/control_calibration_service/src/pure_pursuit_control_calibration_algorithm.cpp +++ /dev/null @@ -1,33 +0,0 @@ -#include "control_calibration_service/control_calibration_common.hpp" - -namespace control_calibration_service -{ - -bool PurePursuitControlCalibrationAlgorithm::run( - const ControlCalibrationInput & input, - ControlCalibrationOutput & output, - std::string & failure_reason) const -{ - (void)failure_reason; - - // Pure Pursuit 控制标定模板: - // 适合基于参考轨迹和前视点策略的横向控制评估。 - // 算法工程师通常会在这里处理: - // 1. 参考轨迹读取: - // - input.reference_trajectory - // - input.reference_stop_at_end / input.reference_timeout_sec - // 2. 观测输入: - // - input.control_telemetry_history 中的横向误差、航向误差、转向输出 - // - input.chassis_telemetry_history 中的速度、姿态和底盘运动状态 - // - input.truth_source_diagnostics / 真值历史,用于判断轨迹对齐是否可靠 - // 3. 参数输出: - // - 可将前视距离、速度相关增益等写入 output.response.result.estimated_parameter_set - // 4. 验收输出: - // - 将最大误差、均方误差、振荡情况、自动验收结论写入 validation_summary - fill_common_result(input, output); - output.response.result.message = "pure pursuit control calibration template executed."; - output.response.result.recommended_parameter_version = "pure_pursuit_template_v1"; - return true; -} - -} // namespace control_calibration_service diff --git a/agv_calib_brain/src/agv_calib_core/external_localization_service/src/external_localization_service_node.cpp b/agv_calib_brain/src/agv_calib_core/external_localization_service/src/external_localization_service_node.cpp deleted file mode 100644 index 02f81b6..0000000 --- a/agv_calib_brain/src/agv_calib_core/external_localization_service/src/external_localization_service_node.cpp +++ /dev/null @@ -1,120 +0,0 @@ -#include "external_localization_service/external_localization_service_node.hpp" - -#include -#include - -#include "calibration_common_interfaces/msg/error_code.hpp" -#include "calibration_common_interfaces/msg/job_state.hpp" - -namespace external_localization_service -{ - -namespace -{ -int64_t now_us() -{ - return std::chrono::duration_cast( - std::chrono::system_clock::now().time_since_epoch()) - .count(); -} -} // namespace - -using calibration_common_interfaces::msg::ErrorCode; -using calibration_common_interfaces::msg::JobState; - -ExternalLocalizationServiceNode::ExternalLocalizationServiceNode(const rclcpp::NodeOptions & options) -: Node("external_localization_service", options) -{ - readiness_service_ = create_service( - "/external_localization/get_readiness", - std::bind(&ExternalLocalizationServiceNode::handle_readiness, this, std::placeholders::_1, std::placeholders::_2)); - - execute_task_action_server_ = rclcpp_action::create_server( - this, - "/external_localization/execute_task", - std::bind(&ExternalLocalizationServiceNode::handle_goal, this, std::placeholders::_1, std::placeholders::_2), - std::bind(&ExternalLocalizationServiceNode::handle_cancel, this, std::placeholders::_1), - std::bind(&ExternalLocalizationServiceNode::handle_accepted, this, std::placeholders::_1)); -} - -void ExternalLocalizationServiceNode::handle_readiness( - const std::shared_ptr request, - std::shared_ptr response) -{ - (void)request; - - // 这里先保留最小 readiness 逻辑。 - // 后续若接入真实外部定位设备/真值源桥接程序,可在这里增加: - // - 真值源在线检查 - // - 同步状态检查 - // - 覆盖范围检查 - // - 观测质量检查 - // - 切源稳定性检查 - response->response.success = true; - response->response.error_code.code = ErrorCode::OK; - response->response.message = "external_localization_service is ready."; - response->response.agent_ready = true; - response->response.ready_for_reference_validation = true; - response->response.checked_timestamp_us = now_us(); - response->response.validation_summary.time_sync_ok = true; - response->response.validation_summary.coverage_ok = true; - response->response.validation_summary.quality_ok = true; - response->response.validation_summary.tracking_stable = true; - response->response.validation_summary.recommended_as_truth_source = true; - response->response.validation_summary.position_stddev_m = 0.0; - response->response.validation_summary.yaw_stddev_rad = 0.0; - response->response.validation_summary.tracking_loss_ratio = 0.0; - response->response.validation_summary.time_sync_offset_ms = 0.0; -} - -rclcpp_action::GoalResponse ExternalLocalizationServiceNode::handle_goal( - const rclcpp_action::GoalUUID & /*uuid*/, - std::shared_ptr goal) -{ - std::string reject_reason; - if (!executor_.validate_goal(*goal, reject_reason)) { - RCLCPP_WARN(get_logger(), "Reject external_localization goal: %s", reject_reason.c_str()); - return rclcpp_action::GoalResponse::REJECT; - } - return rclcpp_action::GoalResponse::ACCEPT_AND_EXECUTE; -} - -rclcpp_action::CancelResponse ExternalLocalizationServiceNode::handle_cancel( - const std::shared_ptr /*goal_handle*/) -{ - return rclcpp_action::CancelResponse::ACCEPT; -} - -void ExternalLocalizationServiceNode::handle_accepted( - const std::shared_ptr goal_handle) -{ - std::thread(std::bind(&ExternalLocalizationServiceNode::execute_goal, this, goal_handle)).detach(); -} - -void ExternalLocalizationServiceNode::execute_goal( - const std::shared_ptr goal_handle) -{ - auto feedback = std::make_shared(); - feedback->feedback.job_id = goal_handle->get_goal()->goal.header.request_id; - feedback->feedback.state.state = JobState::RUNNING; - feedback->feedback.progress = 0.5; - feedback->feedback.error_code.code = ErrorCode::OK; - feedback->feedback.message = "external localization task is running."; - feedback->feedback.server_timestamp_us = now_us(); - feedback->feedback.safe_to_retry = false; - goal_handle->publish_feedback(feedback); - - auto result = std::make_shared(); - std::string failure_reason; - if (!executor_.build_result(*goal_handle->get_goal(), *result, failure_reason)) { - result->result.success = false; - result->result.error_code.code = ErrorCode::INVALID_ARGUMENT; - result->result.message = failure_reason; - goal_handle->abort(result); - return; - } - - goal_handle->succeed(result); -} - -} // namespace external_localization_service diff --git a/agv_calib_brain/src/agv_calib_core/external_localization_service/src/marker_alignment_algorithm.cpp b/agv_calib_brain/src/agv_calib_core/external_localization_service/src/marker_alignment_algorithm.cpp deleted file mode 100644 index 3936365..0000000 --- a/agv_calib_brain/src/agv_calib_core/external_localization_service/src/marker_alignment_algorithm.cpp +++ /dev/null @@ -1,66 +0,0 @@ -#include "external_localization_service/external_localization_common.hpp" - -namespace external_localization_service -{ - -bool MarkerAlignmentAlgorithm::run( - const ExternalLocalizationInput & input, - ExternalLocalizationOutput & output, - std::string & failure_reason) const -{ - (void)failure_reason; - - // 标靶对齐模板: - // 【本文件负责什么】 - // - 负责标靶检测结果读取、坐标系对齐求解和残差统计相关算法实现。 - // - 后续算法工程师应主要修改本文件,不要改 node 层和模板分发层。 - // - // 【建议优先读取的输入】 - // 1. input.marker_alignment_task - // - target_board_id、min_valid_observation_count、timeout_sec 等关键约束。 - // 2. input.latest_external_localization_telemetry / input.external_localization_telemetry_history - // - 读取观测位姿、标准差、丢失率、时间同步偏差和质量评分。 - // 3. input.marker_alignment_diagnostics - // - 读取标靶检测失败、角点不足、姿态求解不稳定等问题描述。 - // 4. input.sensor_quality_diagnostics - // - 如果标靶检测依赖相机 / LiDAR 质量,可在这里读取辅助质量信息。 - // - // 【必写输出】 - // 1. output.response.result.result.workshop_to_localization - // - 这是标靶对齐最核心的输出结果。 - // 2. output.response.result.result.residual_error_m / residual_error_rad - // - 写回对齐残差,供 orchestrator 判断是否自动验收。 - // 3. output.response.result.validation_summary - // - 写位置标准差、航向标准差、时间同步偏差等摘要。 - // - // 【可选输出】 - // - output.response.result.artifacts - // 可挂标靶检测日志、可视化结果、拟合报告、残差统计文件等。 - // - // 【常见失败原因】 - // - 有效观测数不足、标靶检测失败、姿态求解不稳定、时间同步异常、质量评分过低。 - // - // 【在这里添加真实算法】 - // - 请在 fill_external_localization_common_success(...) 之前或之后补充真实对齐求解逻辑。 - // - 当前文件仅提供交付模板,不包含真实外部定位算法。 - - fill_external_localization_common_success( - input, - output, - "标靶对齐完成。", - "demo_external_marker_alignment_v1"); - - output.response.result.result.workshop_frame_id = "workshop"; - output.response.result.result.localization_frame_id = "localization"; - output.response.result.result.workshop_to_localization.z_m = 0.0; - output.response.result.result.position_repeatability_m = 0.0; - output.response.result.result.yaw_repeatability_rad = 0.0; - output.response.result.result.residual_error_m = 0.0; - output.response.result.result.residual_error_rad = 0.0; - output.response.result.result.tracking_loss_ratio = 0.0; - output.response.result.result.time_sync_offset_ms = 0.0; - output.response.result.result.validated_as_truth_source = true; - return true; -} - -} // namespace external_localization_service diff --git a/agv_calib_brain/src/agv_calib_core/external_localization_service/src/reference_pose_collection_algorithm.cpp b/agv_calib_brain/src/agv_calib_core/external_localization_service/src/reference_pose_collection_algorithm.cpp deleted file mode 100644 index eb45138..0000000 --- a/agv_calib_brain/src/agv_calib_core/external_localization_service/src/reference_pose_collection_algorithm.cpp +++ /dev/null @@ -1,65 +0,0 @@ -#include "external_localization_service/external_localization_common.hpp" - -namespace external_localization_service -{ - -bool ReferencePoseCollectionAlgorithm::run( - const ExternalLocalizationInput & input, - ExternalLocalizationOutput & output, - std::string & failure_reason) const -{ - (void)failure_reason; - - // 参考位姿采集模板: - // 【本文件负责什么】 - // - 负责参考位姿采集与静态重复性分析相关算法实现。 - // - 后续算法工程师应主要修改本文件,不要改 node 层和模板分发层。 - // - // 【建议优先读取的输入】 - // 1. input.reference_pose_collection_task - // - sample_count、require_vehicle_static、timeout_sec、min_sample_interval_sec 等采样约束。 - // 2. input.latest_external_localization_telemetry / input.external_localization_telemetry_history - // - 每帧外部定位位姿、位置标准差、航向标准差、时间同步偏差、质量评分。 - // 3. input.latest_chassis_telemetry / input.chassis_telemetry_history - // - 用于判断车辆是否真实静止,避免采入无效参考位姿。 - // 4. input.truth_source_diagnostics / input.acquisition_diagnostics - // - 记录时间同步异常、观测缺失、采样不足、落盘失败等问题。 - // - // 【必写输出】 - // 1. output.response.result.validation_summary - // - 写位置标准差、航向标准差、时间同步偏差、是否推荐为真值源等摘要。 - // 2. output.response.result.result - // - 写 workshop_frame_id / localization_frame_id / repeatability / residual 等结果。 - // 3. output.response.result.suitable_for_commit - // - 明确当前采集结果是否建议进入下一阶段。 - // - // 【可选输出】 - // - output.response.result.artifacts - // 可挂采样日志、原始位姿文件、统计报告等文件引用。 - // - // 【常见失败原因】 - // - 车辆未静止、有效样本不足、时间同步超标、外部定位观测丢失、采样频率不足。 - // - // 【在这里添加真实算法】 - // - 请在 fill_external_localization_common_success(...) 之前或之后补充真实采样与统计逻辑。 - // - 当前文件仅提供交付模板,不包含真实外部定位算法。 - - fill_external_localization_common_success( - input, - output, - "参考位姿采集完成。", - "demo_external_reference_pose_collection_v1"); - - output.response.result.result.workshop_frame_id = "workshop"; - output.response.result.result.localization_frame_id = "localization"; - output.response.result.result.position_repeatability_m = 0.0; - output.response.result.result.yaw_repeatability_rad = 0.0; - output.response.result.result.residual_error_m = 0.0; - output.response.result.result.residual_error_rad = 0.0; - output.response.result.result.tracking_loss_ratio = 0.0; - output.response.result.result.time_sync_offset_ms = 0.0; - output.response.result.result.validated_as_truth_source = true; - return true; -} - -} // namespace external_localization_service diff --git a/agv_calib_brain/src/agv_calib_core/external_localization_service/src/truth_source_validation_algorithm.cpp b/agv_calib_brain/src/agv_calib_core/external_localization_service/src/truth_source_validation_algorithm.cpp deleted file mode 100644 index 6987fc9..0000000 --- a/agv_calib_brain/src/agv_calib_core/external_localization_service/src/truth_source_validation_algorithm.cpp +++ /dev/null @@ -1,77 +0,0 @@ -#include "external_localization_service/external_localization_common.hpp" - -namespace external_localization_service -{ - -bool TruthSourceValidationAlgorithm::run( - const ExternalLocalizationInput & input, - ExternalLocalizationOutput & output, - std::string & failure_reason) const -{ - (void)failure_reason; - - // 真值源验证模板: - // 【本文件负责什么】 - // - 负责静态重复性、动态稳定性、时间同步与丢失率等真值源验证算法实现。 - // - 后续算法工程师应主要修改本文件,不要改 node 层和模板分发层。 - // - // 【建议优先读取的输入】 - // 1. input.truth_source_validation_task - // - static_sample_count、dynamic_sample_count、require_short_motion_segment 等任务要求。 - // 2. input.external_localization_telemetry_history - // - 外部定位历史观测窗口,是稳定性、同步性、重复性分析的核心输入。 - // 3. input.chassis_telemetry_history / input.control_telemetry_history - // - 如果要求短运动段验证,需要结合车辆实际运动状态和控制输出做时序对齐。 - // 4. input.sensor_telemetry_history - // - 用于判断辅助传感器质量是否影响外部定位观测可信度。 - // 5. input.truth_source_diagnostics - // - 记录时间同步超标、观测丢失、切源异常等问题。 - // - // 【必写输出】 - // 1. output.response.result.validation_summary - // - 这是 orchestrator 自动验收最关键的摘要区域。 - // 2. output.response.result.result - // - 写 repeatability、tracking_loss_ratio、time_sync_offset_ms 等核心结果。 - // 3. output.response.result.data_quality_passed / suitable_for_commit - // - 明确当前真值源是否可进入后续标定闭环。 - // - // 【可选输出】 - // - output.response.result.artifacts - // 可挂稳定性分析报告、同步统计图、丢失率分析文件等。 - // - // 【常见失败原因】 - // - 动态窗口不足、时间同步偏差超阈值、观测丢失率过高、重复性不满足要求。 - // - // 【在这里添加真实算法】 - // - 请在 fill_external_localization_common_success(...) 之前或之后补充真实验证逻辑。 - // - 当前文件仅提供交付模板,不包含真实外部定位算法。 - - fill_external_localization_common_success( - input, - output, - "真值源验证完成。", - "demo_external_truth_source_validation_v1"); - - output.response.result.validation_summary.time_sync_ok = true; - output.response.result.validation_summary.coverage_ok = true; - output.response.result.validation_summary.quality_ok = true; - output.response.result.validation_summary.tracking_stable = true; - output.response.result.validation_summary.recommended_as_truth_source = true; - output.response.result.validation_summary.position_stddev_m = 0.0; - output.response.result.validation_summary.yaw_stddev_rad = 0.0; - output.response.result.validation_summary.tracking_loss_ratio = 0.0; - output.response.result.validation_summary.time_sync_offset_ms = 0.0; - - output.response.result.result.workshop_frame_id = "workshop"; - output.response.result.result.localization_frame_id = "localization"; - output.response.result.result.position_repeatability_m = 0.0; - output.response.result.result.yaw_repeatability_rad = 0.0; - output.response.result.result.residual_error_m = 0.0; - output.response.result.result.residual_error_rad = 0.0; - output.response.result.result.tracking_loss_ratio = 0.0; - output.response.result.result.time_sync_offset_ms = 0.0; - output.response.result.result.validated_as_truth_source = true; - return true; -} - -} // namespace external_localization_service diff --git a/agv_calib_brain/src/agv_calib_core/sensor_calibration_service/src/sensor_calibration_service_node.cpp b/agv_calib_brain/src/agv_calib_core/sensor_calibration_service/src/sensor_calibration_service_node.cpp deleted file mode 100644 index 624f09b..0000000 --- a/agv_calib_brain/src/agv_calib_core/sensor_calibration_service/src/sensor_calibration_service_node.cpp +++ /dev/null @@ -1,167 +0,0 @@ -#include "sensor_calibration_service/sensor_calibration_service_node.hpp" - -#include -#include - -#include "calibration_common_interfaces/msg/error_code.hpp" -#include "calibration_common_interfaces/msg/job_state.hpp" -#include "calibration_sensor_interfaces/msg/sensor_calibration_job_result.hpp" -#include "calibration_sensor_interfaces/msg/sensor_readiness_response.hpp" - -namespace sensor_calibration_service -{ - -namespace -{ -int64_t now_us() -{ - return std::chrono::duration_cast( - std::chrono::system_clock::now().time_since_epoch()) - .count(); -} -} // namespace - -using calibration_common_interfaces::msg::ErrorCode; -using calibration_common_interfaces::msg::JobState; - -SensorCalibrationServiceNode::SensorCalibrationServiceNode(const rclcpp::NodeOptions & options) -: Node("sensor_calibration_service", options) -{ - readiness_service_ = create_service( - "/sensor_calibration/get_readiness", - std::bind(&SensorCalibrationServiceNode::handle_readiness, this, std::placeholders::_1, std::placeholders::_2)); - - execute_task_action_server_ = rclcpp_action::create_server( - this, - "/sensor_calibration/execute_task", - std::bind(&SensorCalibrationServiceNode::handle_goal, this, std::placeholders::_1, std::placeholders::_2), - std::bind(&SensorCalibrationServiceNode::handle_cancel, this, std::placeholders::_1), - std::bind(&SensorCalibrationServiceNode::handle_accepted, this, std::placeholders::_1)); -} - -void SensorCalibrationServiceNode::handle_readiness( - const std::shared_ptr request, - std::shared_ptr response) -{ - (void)request; - response->response.success = true; - response->response.error_code.code = ErrorCode::OK; - response->response.message = "sensor_calibration_service is ready."; - response->response.agent_ready = true; - response->response.capture_pipeline_ready = true; - response->response.storage_ready = true; - response->response.telemetry_ready = true; - response->response.vehicle_safe_to_move = true; - response->response.arm_ready = true; - response->response.ready_sensor_ids.push_back("demo_sensor_001"); - response->response.checked_timestamp_us = now_us(); -} - -rclcpp_action::GoalResponse SensorCalibrationServiceNode::handle_goal( - const rclcpp_action::GoalUUID & /*uuid*/, - std::shared_ptr goal) -{ - if (goal->goal.header.request_id.empty()) { - return rclcpp_action::GoalResponse::REJECT; - } - return rclcpp_action::GoalResponse::ACCEPT_AND_EXECUTE; -} - -rclcpp_action::CancelResponse SensorCalibrationServiceNode::handle_cancel( - const std::shared_ptr /*goal_handle*/) -{ - return rclcpp_action::CancelResponse::ACCEPT; -} - -void SensorCalibrationServiceNode::handle_accepted( - const std::shared_ptr goal_handle) -{ - std::thread(std::bind(&SensorCalibrationServiceNode::execute_goal, this, goal_handle)).detach(); -} - -void SensorCalibrationServiceNode::execute_goal( - const std::shared_ptr goal_handle) -{ - // 先回一帧 RUNNING feedback,告诉 orchestrator 当前任务已经进入执行阶段。 - auto feedback = std::make_shared(); - feedback->feedback.state.state = JobState::RUNNING; - goal_handle->publish_feedback(feedback); - - // ===== 组装算法输入上下文 ===== - // 当前模板阶段先把“算法最常用的任务侧输入”显式展开。 - // 后续如果要接真实车辆画像、已生效参数查询、历史遥测缓存、外部定位缓存, - // 也应继续在这里补齐并写入 SensorCalibrationInput。 - SensorCalibrationAlgorithmTemplate::Input input; - input.request = *goal_handle->get_goal(); - input.task_type = goal_handle->get_goal()->goal.selected_task; - input.task_subtype = goal_handle->get_goal()->goal.task_subtype; - input.target_sensor_id = resolve_target_sensor_id(*goal_handle->get_goal()); - input.camera_intrinsic_task = goal_handle->get_goal()->goal.camera_intrinsic; - input.imu_intrinsic_task = goal_handle->get_goal()->goal.imu_intrinsic; - input.sensor_to_base_extrinsic_task = goal_handle->get_goal()->goal.sensor_to_base_extrinsic; - input.hand_eye_task = goal_handle->get_goal()->goal.hand_eye; - input.required_image_count = goal_handle->get_goal()->goal.camera_intrinsic.required_image_count; - input.required_static_segment_count = goal_handle->get_goal()->goal.imu_intrinsic.required_static_segment_count; - input.required_motion_segment_count = goal_handle->get_goal()->goal.imu_intrinsic.required_motion_segment_count; - input.required_sample_count = goal_handle->get_goal()->goal.sensor_to_base_extrinsic.required_sample_count; - input.required_pose_count = goal_handle->get_goal()->goal.hand_eye.required_pose_count; - switch (goal_handle->get_goal()->goal.selected_task.value) { - case TaskType::CAMERA_INTRINSIC: - input.reference_timeout_sec = goal_handle->get_goal()->goal.camera_intrinsic.timeout_sec; - break; - case TaskType::IMU_INTRINSIC: - input.reference_timeout_sec = goal_handle->get_goal()->goal.imu_intrinsic.timeout_sec; - break; - case TaskType::SENSOR_TO_BASE_EXTRINSIC: - input.reference_timeout_sec = goal_handle->get_goal()->goal.sensor_to_base_extrinsic.timeout_sec; - break; - case TaskType::HAND_EYE: - input.reference_timeout_sec = goal_handle->get_goal()->goal.hand_eye.timeout_sec; - break; - default: - input.reference_timeout_sec = 0.0; - break; - } - input.reference_board_id = goal_handle->get_goal()->goal.camera_intrinsic.target_board_id; - input.reference_base_frame_id = goal_handle->get_goal()->goal.sensor_to_base_extrinsic.base_frame_id; - input.reference_arm_id = goal_handle->get_goal()->goal.hand_eye.arm_id; - input.sensor_history_available = false; - input.chassis_history_available = false; - input.control_history_available = false; - input.truth_history_available = false; - - SensorCalibrationAlgorithmTemplate::Output output; - std::string failure_reason; - if (!algorithm_.run(input, output, failure_reason)) { - auto result = std::make_shared(); - result->result.success = false; - result->result.error_code.code = ErrorCode::INVALID_STATE; - result->result.message = failure_reason; - result->result.job_id = goal_handle->get_goal()->goal.header.request_id; - result->result.data_quality_passed = false; - result->result.suitable_for_commit = false; - goal_handle->abort(result); - return; - } - - auto result = std::make_shared(output.response); - goal_handle->succeed(result); -} - -std::string SensorCalibrationServiceNode::resolve_target_sensor_id(const ExecuteTask::Goal & goal) const -{ - switch (goal.goal.selected_task.value) { - case TaskType::CAMERA_INTRINSIC: - return goal.goal.camera_intrinsic.sensor_id; - case TaskType::IMU_INTRINSIC: - return goal.goal.imu_intrinsic.sensor_id; - case TaskType::SENSOR_TO_BASE_EXTRINSIC: - return goal.goal.sensor_to_base_extrinsic.sensor_id; - case TaskType::HAND_EYE: - return goal.goal.hand_eye.sensor_id; - default: - return ""; - } -} - -} // namespace sensor_calibration_service diff --git a/agv_calib_brain/src/agv_calib_core/sensor_calibration_service/src/sensor_to_base_extrinsic_calibration_algorithm.cpp b/agv_calib_brain/src/agv_calib_core/sensor_calibration_service/src/sensor_to_base_extrinsic_calibration_algorithm.cpp deleted file mode 100644 index 246fec8..0000000 --- a/agv_calib_brain/src/agv_calib_core/sensor_calibration_service/src/sensor_to_base_extrinsic_calibration_algorithm.cpp +++ /dev/null @@ -1,76 +0,0 @@ -#include "sensor_calibration_service/sensor_calibration_common.hpp" - -namespace sensor_calibration_service -{ - -bool FrontCameraExtrinsicCalibrationAlgorithm::run( - const SensorCalibrationInput & input, - SensorCalibrationOutput & output, - std::string & failure_reason) const -{ - (void)failure_reason; - - // 前视相机到 base_link 外参标定模板。 - fill_common_result(input, output); - output.response.result.message = "front camera extrinsic calibration template executed."; - output.response.result.recommended_parameter_version = "front_camera_extrinsic_template_v1"; - return true; -} - -bool DownwardCameraExtrinsicCalibrationAlgorithm::run( - const SensorCalibrationInput & input, - SensorCalibrationOutput & output, - std::string & failure_reason) const -{ - (void)failure_reason; - - // 下视相机到 base_link 外参标定模板。 - fill_common_result(input, output); - output.response.result.message = "downward camera extrinsic calibration template executed."; - output.response.result.recommended_parameter_version = "downward_camera_extrinsic_template_v1"; - return true; -} - -bool ImuExtrinsicCalibrationAlgorithm::run( - const SensorCalibrationInput & input, - SensorCalibrationOutput & output, - std::string & failure_reason) const -{ - (void)failure_reason; - - // IMU 到 base_link 外参标定模板。 - fill_common_result(input, output); - output.response.result.message = "imu extrinsic calibration template executed."; - output.response.result.recommended_parameter_version = "imu_extrinsic_template_v1"; - return true; -} - -bool Lidar2DExtrinsicCalibrationAlgorithm::run( - const SensorCalibrationInput & input, - SensorCalibrationOutput & output, - std::string & failure_reason) const -{ - (void)failure_reason; - - // 2D 激光雷达到 base_link 外参标定模板。 - fill_common_result(input, output); - output.response.result.message = "2d lidar extrinsic calibration template executed."; - output.response.result.recommended_parameter_version = "lidar_2d_extrinsic_template_v1"; - return true; -} - -bool Lidar3DExtrinsicCalibrationAlgorithm::run( - const SensorCalibrationInput & input, - SensorCalibrationOutput & output, - std::string & failure_reason) const -{ - (void)failure_reason; - - // 3D 激光雷达到 base_link 外参标定模板。 - fill_common_result(input, output); - output.response.result.message = "3d lidar extrinsic calibration template executed."; - output.response.result.recommended_parameter_version = "lidar_3d_extrinsic_template_v1"; - return true; -} - -} // namespace sensor_calibration_service diff --git a/agv_calib_brain/src/agv_calib_core/vehicle_profile_manager/src/vehicle_profile_manager_node.cpp b/agv_calib_brain/src/agv_calib_core/vehicle_profile_manager/src/vehicle_profile_manager_node.cpp deleted file mode 100644 index 96e9f6a..0000000 --- a/agv_calib_brain/src/agv_calib_core/vehicle_profile_manager/src/vehicle_profile_manager_node.cpp +++ /dev/null @@ -1,180 +0,0 @@ -#include "vehicle_profile_manager/vehicle_profile_manager_node.hpp" - -#include - -#include "calibration_common_interfaces/msg/error_code.hpp" -#include "calibration_vehicle_profile_interfaces/msg/chassis_type.hpp" -#include "calibration_vehicle_profile_interfaces/msg/workflow_stage_type.hpp" -#include "rclcpp_components/register_node_macro.hpp" - -namespace vehicle_profile_manager -{ - -using calibration_common_interfaces::msg::ErrorCode; -using calibration_vehicle_profile_interfaces::msg::ChassisType; -using calibration_vehicle_profile_interfaces::msg::WorkflowStageType; - -VehicleProfileManagerNode::VehicleProfileManagerNode(const rclcpp::NodeOptions & options) -: Node("vehicle_profile_manager", options) -{ - get_profile_service_ = create_service( - "/vehicle_profile_manager/get_vehicle_profile", - std::bind( - &VehicleProfileManagerNode::handle_get_profile, this, - std::placeholders::_1, std::placeholders::_2)); - - register_service_ = create_service( - "/vehicle_profile_manager/register_or_update_vehicle_profile", - std::bind( - &VehicleProfileManagerNode::handle_register, this, - std::placeholders::_1, std::placeholders::_2)); - - applicability_service_ = create_service( - "/vehicle_profile_manager/evaluate_vehicle_calibration_applicability", - std::bind( - &VehicleProfileManagerNode::handle_applicability, this, - std::placeholders::_1, std::placeholders::_2)); - - heartbeat_service_ = create_service( - "/vehicle_profile_manager/heartbeat", - std::bind( - &VehicleProfileManagerNode::handle_heartbeat, this, - std::placeholders::_1, std::placeholders::_2)); - - load_demo_profile(); - - RCLCPP_INFO(get_logger(), "VehicleProfileManagerNode 启动,已预载 demo 车辆画像。"); -} - -void VehicleProfileManagerNode::handle_get_profile( - const std::shared_ptr request, - std::shared_ptr response) -{ - const auto & vehicle_id = request->request.vehicle_id; - auto it = profiles_.find(vehicle_id); - if (it == profiles_.end()) { - response->response.success = false; - response->response.error_code.code = ErrorCode::INVALID_ARGUMENT; - response->response.message = "找不到 vehicle_id=[" + vehicle_id + "] 的车辆画像。"; - return; - } - response->response.success = true; - response->response.error_code.code = ErrorCode::OK; - response->response.message = "查询成功。"; - response->response.profile = it->second; -} - -void VehicleProfileManagerNode::handle_register( - const std::shared_ptr request, - std::shared_ptr response) -{ - const auto & vehicle_id = request->request.profile.base_info.vehicle_id; - if (vehicle_id.empty()) { - response->response.success = false; - response->response.error_code.code = ErrorCode::INVALID_ARGUMENT; - response->response.message = "vehicle_id 不能为空。"; - return; - } - profiles_[vehicle_id] = request->request.profile; - RCLCPP_INFO(get_logger(), "已注册/更新车辆画像 vehicle_id=[%s]", vehicle_id.c_str()); - response->response.success = true; - response->response.error_code.code = ErrorCode::OK; - response->response.message = "注册/更新成功。"; -} - -void VehicleProfileManagerNode::handle_applicability( - const std::shared_ptr request, - std::shared_ptr response) -{ - const auto & profile = request->request.profile_snapshot; - auto stages = evaluate_supported_stages(profile); - - response->response.success = true; - response->response.error_code.code = ErrorCode::OK; - response->response.message = "适用性评估完成。"; - response->response.overall_supported = !stages.empty(); - response->response.recommended_workflow_stages = stages; -} - -void VehicleProfileManagerNode::handle_heartbeat( - const std::shared_ptr /*request*/, - std::shared_ptr response) -{ - const auto now_us = std::chrono::duration_cast( - std::chrono::system_clock::now().time_since_epoch()).count(); - response->response.success = true; - response->response.error_code.code = ErrorCode::OK; - response->response.message = "vehicle_profile_manager 在线。"; - response->response.server_timestamp_us = now_us; - response->response.vehicle_ready = true; -} - -std::vector -VehicleProfileManagerNode::evaluate_supported_stages(const VehicleProfile & profile) const -{ - std::vector stages; - - auto make_stage = [](uint8_t v) { - WorkflowStageType s; - s.value = v; - return s; - }; - - // 预检和画像校验始终支持。 - stages.push_back(make_stage(WorkflowStageType::PROFILE_VALIDATION_STAGE)); - stages.push_back(make_stage(WorkflowStageType::WORKSHOP_PRECHECK_STAGE)); - - // 底盘标定:底盘类型已指定时支持。 - if (profile.chassis_type.value != ChassisType::CHASSIS_TYPE_UNSPECIFIED) { - stages.push_back(make_stage(WorkflowStageType::CHASSIS_CALIBRATION_STAGE)); - stages.push_back(make_stage(WorkflowStageType::CONTROL_CALIBRATION_STAGE)); - } - - // 传感器标定:有传感器配置时支持。 - if (!profile.sensors.empty()) { - stages.push_back(make_stage(WorkflowStageType::SENSOR_INTRINSIC_CALIBRATION_STAGE)); - stages.push_back(make_stage(WorkflowStageType::SENSOR_EXTRINSIC_CALIBRATION_STAGE)); - } - - // 手眼标定:有机械臂且有传感器时支持。 - if (profile.arm_profile.has_mechanical_arm && !profile.sensors.empty()) { - stages.push_back(make_stage(WorkflowStageType::HAND_EYE_CALIBRATION_STAGE)); - } - - // 最终阶段始终加入。 - stages.push_back(make_stage(WorkflowStageType::PARAMETER_COMMIT_STAGE)); - stages.push_back(make_stage(WorkflowStageType::REPORT_ARCHIVE_STAGE)); - - return stages; -} - -void VehicleProfileManagerNode::load_demo_profile() -{ - VehicleProfile demo; - - // 基础信息 - demo.base_info.vehicle_id = "demo_agv_001"; - demo.base_info.vehicle_name = "Demo AGV"; - demo.base_info.model_name = "DemoModel-X1"; - demo.base_info.manufacturer = "Demo Manufacturer"; - - // 底盘类型:差速 - demo.chassis_type.value = ChassisType::DIFFERENTIAL; - - // base_link - demo.base_link_frame = "base_link"; - - // 画像版本 - demo.profile_version = "demo_v1"; - - // 启用的工作流阶段 - demo.enabled_workflow_stages = evaluate_supported_stages(demo); - - profiles_[demo.base_info.vehicle_id] = demo; - RCLCPP_INFO(get_logger(), "已预载 demo 车辆画像 vehicle_id=[%s]", - demo.base_info.vehicle_id.c_str()); -} - -} // namespace vehicle_profile_manager - -RCLCPP_COMPONENTS_REGISTER_NODE(vehicle_profile_manager::VehicleProfileManagerNode) diff --git a/agv_calib_brain/src/agv_calib_core/workshop_orchestrator/launch/workshop_orchestrator_v2.launch.py b/agv_calib_brain/src/agv_calib_core/workshop_orchestrator/launch/workshop_orchestrator_v2.launch.py deleted file mode 100644 index 35a8946..0000000 --- a/agv_calib_brain/src/agv_calib_core/workshop_orchestrator/launch/workshop_orchestrator_v2.launch.py +++ /dev/null @@ -1,13 +0,0 @@ -from launch import LaunchDescription -from launch_ros.actions import Node - - -def generate_launch_description(): - return LaunchDescription([ - Node( - package="workshop_orchestrator_v2", - executable="workshop_orchestrator_v2_node", - name="workshop_orchestrator_v2", - output="screen", - ) - ]) diff --git a/agv_calib_brain/src/agv_calib_core/workshop_orchestrator/src/precheck_runner.cpp b/agv_calib_brain/src/agv_calib_core/workshop_orchestrator/src/precheck_runner.cpp deleted file mode 100644 index e568d9b..0000000 --- a/agv_calib_brain/src/agv_calib_core/workshop_orchestrator/src/precheck_runner.cpp +++ /dev/null @@ -1,153 +0,0 @@ -#include "workshop_orchestrator_v2/precheck_runner.hpp" - -#include "calibration_workshop_orchestration_interfaces/msg/precheck_item.hpp" - -namespace workshop_orchestrator_v2 -{ - -bool PrecheckRunner::has_metadata_key(const StagePlan & stage, const std::string & key) const -{ - for (const auto & kv : stage.metadata) { - if (kv.key == key) { - return true; - } - } - return false; -} - -bool PrecheckRunner::has_metadata_prefix(const StagePlan & stage, const std::string & prefix) const -{ - for (const auto & kv : stage.metadata) { - if (kv.key.rfind(prefix, 0) == 0) { - return true; - } - } - return false; -} - -WorkshopPrecheckResponse PrecheckRunner::run(const WorkshopSession & session) const -{ - WorkshopPrecheckResponse response; - response.success = true; - response.error_code = make_error_code(ErrorCode::OK); - response.checked_timestamp_us = now_us(); - - auto append_item = [&](const std::string & code, - const std::string & name, - bool passed, - const std::string & message, - uint8_t stage_type, - uint8_t module_type) { - calibration_workshop_orchestration_interfaces::msg::PrecheckItem item; - item.item_code = code; - item.display_name = name; - item.passed = passed; - item.blocking = true; - item.error_code = make_error_code(passed ? ErrorCode::OK : ErrorCode::INVALID_STATE); - item.message = message; - item.related_stage_type = make_stage_type(stage_type); - item.related_module_type = make_module_type(module_type); - response.items.push_back(item); - if (!passed) { - response.blocking_issue_count += 1; - response.all_passed = false; - } - }; - - response.all_passed = true; - response.blocking_issue_count = 0; - - append_item( - "plan_not_empty", - "Execution plan exists", - !session.stage_plan.empty(), - session.stage_plan.empty() ? "Session has no executable stages." : "Session has executable stages.", - WorkflowStageType::WORKFLOW_STAGE_UNSPECIFIED, - CalibrationModuleType::CALIBRATION_MODULE_UNSPECIFIED); - - for (const auto & stage : session.stage_plan) { - if (stage.module_type.value == CalibrationModuleType::CHASSIS_MODULE) { - const bool passed = - has_metadata_key(stage, metadata_keys::CHASSIS_PRIMITIVE_TYPE) && - has_metadata_key(stage, metadata_keys::CHASSIS_STRAIGHT_LINE_DISTANCE_M) && - has_metadata_key(stage, metadata_keys::CHASSIS_STRAIGHT_LINE_SPEED_MS); - append_item( - stage.stage_id + ".metadata", - stage.display_name + " metadata", - passed, - passed ? "底盘阶段输入完整。" : "底盘阶段缺少 primitive_type 或直线动作参数。", - stage.stage_type.value, - stage.module_type.value); - } else if (stage.module_type.value == CalibrationModuleType::CONTROL_MODULE) { - const bool passed = - has_metadata_key(stage, metadata_keys::CONTROL_TASK_TYPE) && - has_metadata_prefix(stage, metadata_keys::CONTROL_TRAJECTORY_PREFIX); - append_item( - stage.stage_id + ".metadata", - stage.display_name + " metadata", - passed, - passed ? "运控阶段输入完整。" : "运控阶段缺少 control.task_type 或轨迹点输入。", - stage.stage_type.value, - stage.module_type.value); - } else if ( - stage.module_type.value == CalibrationModuleType::SENSOR_INTRINSIC_MODULE || - stage.module_type.value == CalibrationModuleType::SENSOR_EXTRINSIC_MODULE || - stage.module_type.value == CalibrationModuleType::HAND_EYE_MODULE) { - bool passed = - has_metadata_key(stage, metadata_keys::SENSOR_ID) && - has_metadata_key(stage, metadata_keys::SENSOR_TASK_SUBTYPE); - std::string message = passed ? "传感器阶段基础输入完整。" : "传感器阶段缺少 sensor.sensor_id 或 sensor.task_subtype。"; - - if (passed && stage.module_type.value == CalibrationModuleType::SENSOR_INTRINSIC_MODULE) { - const bool has_image_count = has_metadata_key(stage, metadata_keys::CAMERA_INTRINSIC_REQUIRED_IMAGE_COUNT); - const bool has_board = has_metadata_key(stage, metadata_keys::CAMERA_INTRINSIC_TARGET_BOARD_ID); - passed = has_image_count || has_board; - message = passed ? "传感器内参阶段输入完整。" : "传感器内参阶段缺少图像数或标定板信息。"; - } - - if (passed && stage.module_type.value == CalibrationModuleType::SENSOR_EXTRINSIC_MODULE) { - passed = - has_metadata_key(stage, metadata_keys::SENSOR_EXTRINSIC_BASE_FRAME_ID) && - has_metadata_key(stage, metadata_keys::SENSOR_EXTRINSIC_REQUIRED_SAMPLE_COUNT); - message = passed ? "传感器外参阶段输入完整。" : "传感器外参阶段缺少 base_frame_id 或 required_sample_count。"; - } - - if (passed && stage.module_type.value == CalibrationModuleType::HAND_EYE_MODULE) { - passed = - has_metadata_key(stage, metadata_keys::HAND_EYE_ARM_ID) && - has_metadata_key(stage, metadata_keys::HAND_EYE_REQUIRED_POSE_COUNT); - message = passed ? "手眼阶段输入完整。" : "手眼阶段缺少 arm_id 或 required_pose_count。"; - } - - append_item( - stage.stage_id + ".metadata", - stage.display_name + " metadata", - passed, - message, - stage.stage_type.value, - stage.module_type.value); - } else if (stage.module_type.value == CalibrationModuleType::EXTERNAL_LOCALIZATION_MODULE) { - const bool passed = - has_metadata_key(stage, metadata_keys::EXTERNAL_STATIC_SAMPLE_COUNT) && - has_metadata_key(stage, metadata_keys::EXTERNAL_DYNAMIC_SAMPLE_COUNT) && - has_metadata_key(stage, metadata_keys::EXTERNAL_MAX_POSITION_STDDEV_M) && - has_metadata_key(stage, metadata_keys::EXTERNAL_MAX_YAW_STDDEV_RAD) && - has_metadata_key(stage, metadata_keys::EXTERNAL_MAX_TRACKING_LOSS_RATIO) && - has_metadata_key(stage, metadata_keys::EXTERNAL_MAX_TIME_SYNC_OFFSET_MS) && - has_metadata_key(stage, metadata_keys::EXTERNAL_TIMEOUT_SEC); - append_item( - stage.stage_id + ".metadata", - stage.display_name + " metadata", - passed, - passed ? "external 阶段输入完整。" : "external 阶段缺少真值校核阈值配置。", - stage.stage_type.value, - stage.module_type.value); - } - } - - response.ready_for_start = response.all_passed; - response.message = response.ready_for_start ? "Precheck passed." : "Precheck failed."; - return response; -} - -} // namespace workshop_orchestrator_v2 diff --git a/agv_calib_brain/src/UI/README.md b/agv_calib_brain/src/apps/operator_ui/README.md similarity index 95% rename from agv_calib_brain/src/UI/README.md rename to agv_calib_brain/src/apps/operator_ui/README.md index e922f36..de21973 100644 --- a/agv_calib_brain/src/UI/README.md +++ b/agv_calib_brain/src/apps/operator_ui/README.md @@ -1,5 +1,4 @@ - -# workshop_ui_pyside6_config_aligned +# 操作员界面原型 这版 PySide6 原型的目标不是单纯展示界面,而是: diff --git a/agv_calib_brain/src/UI/main.py b/agv_calib_brain/src/apps/operator_ui/main.py similarity index 100% rename from agv_calib_brain/src/UI/main.py rename to agv_calib_brain/src/apps/operator_ui/main.py diff --git a/agv_calib_brain/src/UI/requirements.txt b/agv_calib_brain/src/apps/operator_ui/requirements.txt similarity index 100% rename from agv_calib_brain/src/UI/requirements.txt rename to agv_calib_brain/src/apps/operator_ui/requirements.txt diff --git a/agv_calib_brain/src/UI/standalone_dashboard.py b/agv_calib_brain/src/apps/operator_ui/standalone_dashboard.py similarity index 100% rename from agv_calib_brain/src/UI/standalone_dashboard.py rename to agv_calib_brain/src/apps/operator_ui/standalone_dashboard.py diff --git a/agv_calib_brain/src/communication/vehicle_internal_interfaces/CMakeLists.txt b/agv_calib_brain/src/communication/vehicle_internal_interfaces/CMakeLists.txt new file mode 100644 index 0000000..0af7709 --- /dev/null +++ b/agv_calib_brain/src/communication/vehicle_internal_interfaces/CMakeLists.txt @@ -0,0 +1,25 @@ +cmake_minimum_required(VERSION 3.8) +project(vehicle_internal_interfaces) + +find_package(ament_cmake REQUIRED) +find_package(rosidl_default_generators REQUIRED) +find_package(calibration_vehicle_profile_interfaces REQUIRED) + +set(msg_files + "msg/VehicleControlMode.msg" + "msg/AckermannDriveCommand.msg" + "msg/VehicleSafetyCommand.msg" + "msg/AckermannActuatorState.msg" + "msg/VehicleInternalState.msg" + "msg/VehicleHealthStatus.msg" + "msg/VehicleTimeSyncStatus.msg" + "msg/SensorLinkStatus.msg" +) + +rosidl_generate_interfaces(${PROJECT_NAME} + ${msg_files} + DEPENDENCIES calibration_vehicle_profile_interfaces +) + +ament_export_dependencies(rosidl_default_runtime) +ament_package() diff --git a/agv_calib_brain/src/communication/vehicle_internal_interfaces/README.md b/agv_calib_brain/src/communication/vehicle_internal_interfaces/README.md new file mode 100644 index 0000000..8a3f940 --- /dev/null +++ b/agv_calib_brain/src/communication/vehicle_internal_interfaces/README.md @@ -0,0 +1,17 @@ +# 车辆内部接口 + +这个包定义车端电脑和车辆本体之间的内部 ROS2 消息。 + +它不直接作为车间电脑和车端电脑之间的 WiFi6 协议。WiFi6 对外协议仍由 `calibration_*_interfaces/proto` 定义。这个包用于把真实 Windows 小车的 CAN、串口、厂商 SDK 或仿真 Isaac topic 统一映射成车端内部语义。 + +当前消息包括: + +- `AckermannDriveCommand`:阿克曼底盘速度、转角、制动、超时命令。 +- `VehicleSafetyCommand`:上使能、下使能、急停、清故障、标定低速模式。 +- `AckermannActuatorState`:速度、转角、轮速、电机电流、制动/油门反馈。 +- `VehicleInternalState`:车辆模式、安全状态、底盘执行器状态、电源与运动状态。 +- `VehicleHealthStatus`:控制器在线、总线状态、通信质量和车端资源状态。 +- `VehicleTimeSyncStatus`:车端、传感器、外部真值之间的时间同步状态。 +- `SensorLinkStatus`:车载传感器在线状态、帧率、丢帧和延迟。 + +仿真中,`vehicle_agent_sim` 会把车间电脑下发的控制请求转换为 `AckermannDriveCommand`,同时继续发布 Isaac 当前需要的 `cmd_vel`。真实部署时,Windows 车端应把这些内部消息映射到实际车辆接口。 diff --git a/agv_calib_brain/src/communication/vehicle_internal_interfaces/msg/AckermannActuatorState.msg b/agv_calib_brain/src/communication/vehicle_internal_interfaces/msg/AckermannActuatorState.msg new file mode 100644 index 0000000..c055ec3 --- /dev/null +++ b/agv_calib_brain/src/communication/vehicle_internal_interfaces/msg/AckermannActuatorState.msg @@ -0,0 +1,35 @@ +# ========================================================= +# 阿克曼底盘内部执行器状态 +# 发送方:车辆控制器 / Isaac 底盘执行器适配层 +# 接收方:车端电脑 +# ========================================================= + +# 状态时间戳 +int64 hardware_timestamp_us + +# 实际纵向速度 +float64 actual_speed_ms +# 实际纵向加速度 +float64 actual_accel_ms2 +# 实际前轮等效转角 +float64 actual_steering_angle_rad +# 实际转向角速度 +float64 actual_steering_rate_rads + +# 左后驱动轮速度 +float64 rear_left_wheel_speed_ms +# 右后驱动轮速度 +float64 rear_right_wheel_speed_ms +# 左前轮等效转角 +float64 front_left_steering_angle_rad +# 右前轮等效转角 +float64 front_right_steering_angle_rad + +# 驱动电机电流 +float64 drive_motor_current_amp +# 转向电机电流 +float64 steering_motor_current_amp +# 制动压力或制动比例,范围 [0, 1] +float64 brake_pressure +# 驱动控制输出,范围 [0, 1] +float64 throttle_output diff --git a/agv_calib_brain/src/communication/vehicle_internal_interfaces/msg/AckermannDriveCommand.msg b/agv_calib_brain/src/communication/vehicle_internal_interfaces/msg/AckermannDriveCommand.msg new file mode 100644 index 0000000..4043f27 --- /dev/null +++ b/agv_calib_brain/src/communication/vehicle_internal_interfaces/msg/AckermannDriveCommand.msg @@ -0,0 +1,33 @@ +# ========================================================= +# 阿克曼底盘内部控制命令 +# 发送方:车端电脑 +# 接收方:车辆运动控制器 / Isaac 底盘执行器适配层 +# ========================================================= + +# 命令时间戳 +int64 command_timestamp_us +# 命令 ID,用于追踪和去重 +string command_id +# 命令来源,例如 vehicle_agent_sim / real_vehicle_agent +string source +# 控制模式 +vehicle_internal_interfaces/VehicleControlMode control_mode + +# 目标纵向速度 +float64 target_speed_ms +# 目标纵向加速度;0 表示由车辆控制器默认限幅 +float64 target_accel_ms2 +# 目标前轮等效转角,左正右负 +float64 target_steering_angle_rad +# 目标转向角速度;0 表示由车辆控制器默认限幅 +float64 target_steering_rate_rads + +# 制动命令,范围 [0, 1] +float64 brake_command +# 油门 / 驱动命令,范围 [0, 1];仿真可选 +float64 throttle_command +# 命令超时时间 +float64 command_timeout_sec + +# 是否要求控制器在超时或任务结束后停车 +bool stop_when_timeout diff --git a/agv_calib_brain/src/communication/vehicle_internal_interfaces/msg/SensorLinkStatus.msg b/agv_calib_brain/src/communication/vehicle_internal_interfaces/msg/SensorLinkStatus.msg new file mode 100644 index 0000000..9b66414 --- /dev/null +++ b/agv_calib_brain/src/communication/vehicle_internal_interfaces/msg/SensorLinkStatus.msg @@ -0,0 +1,33 @@ +# ========================================================= +# 车端传感器链路状态 +# 发送方:车端传感器代理 +# 接收方:车端电脑 / 车间电脑桥接层 +# ========================================================= + +# 状态时间戳 +int64 hardware_timestamp_us + +# 传感器 ID +string sensor_id +# 传感器类型 +calibration_vehicle_profile_interfaces/SensorType sensor_type +# 传感器 frame +string frame_id +# 车端订阅或驱动 topic / 通道名 +string source_channel + +# 是否在线 +bool online +# 当前帧率 +float64 frame_rate_hz +# 最近一帧年龄 +float64 latest_frame_age_ms +# 累计帧数 +uint64 frame_count +# 丢帧比例 +float64 dropped_frame_ratio +# 最近一帧传输延迟 +float64 latest_transport_latency_ms + +# 当前链路状态说明 +string status_message diff --git a/agv_calib_brain/src/communication/vehicle_internal_interfaces/msg/VehicleControlMode.msg b/agv_calib_brain/src/communication/vehicle_internal_interfaces/msg/VehicleControlMode.msg new file mode 100644 index 0000000..edb284f --- /dev/null +++ b/agv_calib_brain/src/communication/vehicle_internal_interfaces/msg/VehicleControlMode.msg @@ -0,0 +1,15 @@ +# ========================================================= +# 车辆内部控制模式 +# 作用:车端电脑与车辆控制器之间约定当前车辆控制状态 +# ========================================================= + +uint8 MODE_UNSPECIFIED=0 +uint8 DISABLED=1 +uint8 MANUAL=2 +uint8 AUTO=3 +uint8 CALIBRATION=4 +uint8 ESTOP=5 +uint8 FAULT=6 + +# 当前模式 +uint8 value diff --git a/agv_calib_brain/src/communication/vehicle_internal_interfaces/msg/VehicleHealthStatus.msg b/agv_calib_brain/src/communication/vehicle_internal_interfaces/msg/VehicleHealthStatus.msg new file mode 100644 index 0000000..3e081dd --- /dev/null +++ b/agv_calib_brain/src/communication/vehicle_internal_interfaces/msg/VehicleHealthStatus.msg @@ -0,0 +1,40 @@ +# ========================================================= +# 车辆内部健康诊断状态 +# 发送方:车端电脑 / 车辆控制器 +# 接收方:车端电脑内部监控或车间电脑桥接层 +# ========================================================= + +# 状态时间戳 +int64 hardware_timestamp_us + +# 车端电脑进程是否在线 +bool vehicle_agent_online +# 车辆主控制器是否在线 +bool vehicle_controller_online +# 驱动控制器是否在线 +bool drive_controller_online +# 转向控制器是否在线 +bool steering_controller_online +# 传感器总线是否在线 +bool sensor_bus_online +# CAN 或厂商控制链路是否在线 +bool vehicle_bus_online +# 外部真值链路是否在线 +bool external_truth_link_online + +# 通信质量 +float64 vehicle_bus_rx_hz +float64 vehicle_bus_drop_ratio +float64 command_latency_ms +float64 telemetry_latency_ms + +# 车端电脑资源 +float64 cpu_load_ratio +float64 memory_used_ratio +float64 disk_used_ratio +float64 temperature_c + +# 诊断摘要 +bool healthy +string diagnostic_code +string diagnostic_message diff --git a/agv_calib_brain/src/communication/vehicle_internal_interfaces/msg/VehicleInternalState.msg b/agv_calib_brain/src/communication/vehicle_internal_interfaces/msg/VehicleInternalState.msg new file mode 100644 index 0000000..6d71723 --- /dev/null +++ b/agv_calib_brain/src/communication/vehicle_internal_interfaces/msg/VehicleInternalState.msg @@ -0,0 +1,35 @@ +# ========================================================= +# 车辆内部综合状态 +# 发送方:车辆控制器 / Isaac 底盘执行器适配层 +# 接收方:车端电脑 +# ========================================================= + +# 状态时间戳 +int64 hardware_timestamp_us +# 当前控制模式 +vehicle_internal_interfaces/VehicleControlMode current_mode + +# 车辆是否已上使能 +bool vehicle_enabled +# 急停是否触发 +bool estop_engaged +# 是否处于低速标定模式 +bool calibration_low_speed_mode +# 是否存在故障 +bool fault_active +# 主故障码 +string primary_fault_code +# 主故障说明 +string primary_fault_message + +# 当前底盘状态 +vehicle_internal_interfaces/AckermannActuatorState ackermann_state + +# 电源状态 +float64 battery_voltage_v +float64 battery_current_amp +float64 battery_soc + +# 车辆运动状态 +float64 yaw_rate_rads +float64 lateral_accel_ms2 diff --git a/agv_calib_brain/src/communication/vehicle_internal_interfaces/msg/VehicleSafetyCommand.msg b/agv_calib_brain/src/communication/vehicle_internal_interfaces/msg/VehicleSafetyCommand.msg new file mode 100644 index 0000000..c92d5e2 --- /dev/null +++ b/agv_calib_brain/src/communication/vehicle_internal_interfaces/msg/VehicleSafetyCommand.msg @@ -0,0 +1,30 @@ +# ========================================================= +# 车辆内部安全命令 +# 发送方:车端电脑 +# 接收方:车辆安全控制器 / 底盘控制器 +# ========================================================= + +# 命令时间戳 +int64 command_timestamp_us +# 命令 ID +string command_id +# 命令来源 +string source + +# 上使能车辆 +bool enable_vehicle +# 下使能车辆 +bool disable_vehicle +# 触发急停 +bool engage_estop +# 解除急停 +bool release_estop +# 清除可恢复故障 +bool clear_faults +# 切换到标定低速安全模式 +bool enter_calibration_low_speed_mode +# 退出标定低速安全模式 +bool exit_calibration_low_speed_mode + +# 操作原因 +string reason diff --git a/agv_calib_brain/src/communication/vehicle_internal_interfaces/msg/VehicleTimeSyncStatus.msg b/agv_calib_brain/src/communication/vehicle_internal_interfaces/msg/VehicleTimeSyncStatus.msg new file mode 100644 index 0000000..075ebac --- /dev/null +++ b/agv_calib_brain/src/communication/vehicle_internal_interfaces/msg/VehicleTimeSyncStatus.msg @@ -0,0 +1,27 @@ +# ========================================================= +# 车辆内部时间同步状态 +# 发送方:车端电脑 +# 接收方:车间电脑桥接层 / 车端诊断 +# ========================================================= + +# 状态时间戳 +int64 hardware_timestamp_us + +# 同步源,例如 ptp / ntp / external_truth / sim_clock +string sync_source +# 是否认为已同步 +bool synchronized + +# 车端系统时钟相对传感器硬件时钟偏差 +float64 system_to_sensor_offset_ms +# 车端系统时钟相对外部真值时钟偏差 +float64 system_to_external_truth_offset_ms +# 同步抖动 +float64 jitter_ms +# 近期最大时间同步误差 +float64 max_offset_ms +# 时间同步链路延迟 +float64 sync_transport_latency_ms + +# 说明 +string status_message diff --git a/agv_calib_brain/src/communication/vehicle_internal_interfaces/package.xml b/agv_calib_brain/src/communication/vehicle_internal_interfaces/package.xml new file mode 100644 index 0000000..b73288b --- /dev/null +++ b/agv_calib_brain/src/communication/vehicle_internal_interfaces/package.xml @@ -0,0 +1,21 @@ + + + vehicle_internal_interfaces + 0.0.1 + ROS 2 interfaces for vehicle-computer to vehicle-controller internal communication. + + user + Proprietary + + ament_cmake + rosidl_default_generators + + calibration_vehicle_profile_interfaces + + rosidl_default_runtime + + rosidl_interface_packages + + ament_cmake + + diff --git a/agv_calib_brain/src/win_ubuntu_bridge/calibration_chassis_interfaces/CMakeLists.txt b/agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_chassis_interfaces/CMakeLists.txt similarity index 100% rename from agv_calib_brain/src/win_ubuntu_bridge/calibration_chassis_interfaces/CMakeLists.txt rename to agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_chassis_interfaces/CMakeLists.txt diff --git a/agv_calib_brain/src/win_ubuntu_bridge/calibration_chassis_interfaces/FILE_TREE.txt b/agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_chassis_interfaces/FILE_TREE.txt similarity index 100% rename from agv_calib_brain/src/win_ubuntu_bridge/calibration_chassis_interfaces/FILE_TREE.txt rename to agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_chassis_interfaces/FILE_TREE.txt diff --git a/agv_calib_brain/src/win_ubuntu_bridge/calibration_chassis_interfaces/action/ExecuteMotionPrimitive.action b/agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_chassis_interfaces/action/ExecuteMotionPrimitive.action similarity index 100% rename from agv_calib_brain/src/win_ubuntu_bridge/calibration_chassis_interfaces/action/ExecuteMotionPrimitive.action rename to agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_chassis_interfaces/action/ExecuteMotionPrimitive.action diff --git a/agv_calib_brain/src/win_ubuntu_bridge/calibration_chassis_interfaces/msg/AckermannCalibrationParams.msg b/agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_chassis_interfaces/msg/AckermannCalibrationParams.msg similarity index 100% rename from agv_calib_brain/src/win_ubuntu_bridge/calibration_chassis_interfaces/msg/AckermannCalibrationParams.msg rename to agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_chassis_interfaces/msg/AckermannCalibrationParams.msg diff --git a/agv_calib_brain/src/win_ubuntu_bridge/calibration_chassis_interfaces/msg/AppliedChassisCalibrationParametersResponse.msg b/agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_chassis_interfaces/msg/AppliedChassisCalibrationParametersResponse.msg similarity index 100% rename from agv_calib_brain/src/win_ubuntu_bridge/calibration_chassis_interfaces/msg/AppliedChassisCalibrationParametersResponse.msg rename to agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_chassis_interfaces/msg/AppliedChassisCalibrationParametersResponse.msg diff --git a/agv_calib_brain/src/win_ubuntu_bridge/calibration_chassis_interfaces/msg/ArcCommand.msg b/agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_chassis_interfaces/msg/ArcCommand.msg similarity index 100% rename from agv_calib_brain/src/win_ubuntu_bridge/calibration_chassis_interfaces/msg/ArcCommand.msg rename to agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_chassis_interfaces/msg/ArcCommand.msg diff --git a/agv_calib_brain/src/win_ubuntu_bridge/calibration_chassis_interfaces/msg/ChassisCalibrationParameterSet.msg b/agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_chassis_interfaces/msg/ChassisCalibrationParameterSet.msg similarity index 100% rename from agv_calib_brain/src/win_ubuntu_bridge/calibration_chassis_interfaces/msg/ChassisCalibrationParameterSet.msg rename to agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_chassis_interfaces/msg/ChassisCalibrationParameterSet.msg diff --git a/agv_calib_brain/src/win_ubuntu_bridge/calibration_chassis_interfaces/msg/ChassisCapabilityRequest.msg b/agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_chassis_interfaces/msg/ChassisCapabilityRequest.msg similarity index 100% rename from agv_calib_brain/src/win_ubuntu_bridge/calibration_chassis_interfaces/msg/ChassisCapabilityRequest.msg rename to agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_chassis_interfaces/msg/ChassisCapabilityRequest.msg diff --git a/agv_calib_brain/src/win_ubuntu_bridge/calibration_chassis_interfaces/msg/ChassisCapabilityResponse.msg b/agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_chassis_interfaces/msg/ChassisCapabilityResponse.msg similarity index 100% rename from agv_calib_brain/src/win_ubuntu_bridge/calibration_chassis_interfaces/msg/ChassisCapabilityResponse.msg rename to agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_chassis_interfaces/msg/ChassisCapabilityResponse.msg diff --git a/agv_calib_brain/src/win_ubuntu_bridge/calibration_chassis_interfaces/msg/ChassisJobResult.msg b/agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_chassis_interfaces/msg/ChassisJobResult.msg similarity index 100% rename from agv_calib_brain/src/win_ubuntu_bridge/calibration_chassis_interfaces/msg/ChassisJobResult.msg rename to agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_chassis_interfaces/msg/ChassisJobResult.msg diff --git a/agv_calib_brain/src/win_ubuntu_bridge/calibration_chassis_interfaces/msg/ChassisMotionPrimitiveType.msg b/agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_chassis_interfaces/msg/ChassisMotionPrimitiveType.msg similarity index 100% rename from agv_calib_brain/src/win_ubuntu_bridge/calibration_chassis_interfaces/msg/ChassisMotionPrimitiveType.msg rename to agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_chassis_interfaces/msg/ChassisMotionPrimitiveType.msg diff --git a/agv_calib_brain/src/win_ubuntu_bridge/calibration_chassis_interfaces/msg/ChassisReadinessResponse.msg b/agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_chassis_interfaces/msg/ChassisReadinessResponse.msg similarity index 100% rename from agv_calib_brain/src/win_ubuntu_bridge/calibration_chassis_interfaces/msg/ChassisReadinessResponse.msg rename to agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_chassis_interfaces/msg/ChassisReadinessResponse.msg diff --git a/agv_calib_brain/src/win_ubuntu_bridge/calibration_chassis_interfaces/msg/ChassisSpecificParamsType.msg b/agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_chassis_interfaces/msg/ChassisSpecificParamsType.msg similarity index 100% rename from agv_calib_brain/src/win_ubuntu_bridge/calibration_chassis_interfaces/msg/ChassisSpecificParamsType.msg rename to agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_chassis_interfaces/msg/ChassisSpecificParamsType.msg diff --git a/agv_calib_brain/src/win_ubuntu_bridge/calibration_chassis_interfaces/msg/ChassisTelemetry.msg b/agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_chassis_interfaces/msg/ChassisTelemetry.msg similarity index 100% rename from agv_calib_brain/src/win_ubuntu_bridge/calibration_chassis_interfaces/msg/ChassisTelemetry.msg rename to agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_chassis_interfaces/msg/ChassisTelemetry.msg diff --git a/agv_calib_brain/src/win_ubuntu_bridge/calibration_chassis_interfaces/msg/ChassisValidationSummary.msg b/agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_chassis_interfaces/msg/ChassisValidationSummary.msg similarity index 100% rename from agv_calib_brain/src/win_ubuntu_bridge/calibration_chassis_interfaces/msg/ChassisValidationSummary.msg rename to agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_chassis_interfaces/msg/ChassisValidationSummary.msg diff --git a/agv_calib_brain/src/win_ubuntu_bridge/calibration_chassis_interfaces/msg/ChassisWorkMode.msg b/agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_chassis_interfaces/msg/ChassisWorkMode.msg similarity index 100% rename from agv_calib_brain/src/win_ubuntu_bridge/calibration_chassis_interfaces/msg/ChassisWorkMode.msg rename to agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_chassis_interfaces/msg/ChassisWorkMode.msg diff --git a/agv_calib_brain/src/win_ubuntu_bridge/calibration_chassis_interfaces/msg/ChassisWorkModeRequest.msg b/agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_chassis_interfaces/msg/ChassisWorkModeRequest.msg similarity index 100% rename from agv_calib_brain/src/win_ubuntu_bridge/calibration_chassis_interfaces/msg/ChassisWorkModeRequest.msg rename to agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_chassis_interfaces/msg/ChassisWorkModeRequest.msg diff --git a/agv_calib_brain/src/win_ubuntu_bridge/calibration_chassis_interfaces/msg/CommitChassisCalibrationParametersRequest.msg b/agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_chassis_interfaces/msg/CommitChassisCalibrationParametersRequest.msg similarity index 100% rename from agv_calib_brain/src/win_ubuntu_bridge/calibration_chassis_interfaces/msg/CommitChassisCalibrationParametersRequest.msg rename to agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_chassis_interfaces/msg/CommitChassisCalibrationParametersRequest.msg diff --git a/agv_calib_brain/src/win_ubuntu_bridge/calibration_chassis_interfaces/msg/CommonChassisCalibrationParams.msg b/agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_chassis_interfaces/msg/CommonChassisCalibrationParams.msg similarity index 100% rename from agv_calib_brain/src/win_ubuntu_bridge/calibration_chassis_interfaces/msg/CommonChassisCalibrationParams.msg rename to agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_chassis_interfaces/msg/CommonChassisCalibrationParams.msg diff --git a/agv_calib_brain/src/win_ubuntu_bridge/calibration_chassis_interfaces/msg/CoordinatedSteeringCommand.msg b/agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_chassis_interfaces/msg/CoordinatedSteeringCommand.msg similarity index 100% rename from agv_calib_brain/src/win_ubuntu_bridge/calibration_chassis_interfaces/msg/CoordinatedSteeringCommand.msg rename to agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_chassis_interfaces/msg/CoordinatedSteeringCommand.msg diff --git a/agv_calib_brain/src/win_ubuntu_bridge/calibration_chassis_interfaces/msg/DiagonalMotionCommand.msg b/agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_chassis_interfaces/msg/DiagonalMotionCommand.msg similarity index 100% rename from agv_calib_brain/src/win_ubuntu_bridge/calibration_chassis_interfaces/msg/DiagonalMotionCommand.msg rename to agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_chassis_interfaces/msg/DiagonalMotionCommand.msg diff --git a/agv_calib_brain/src/win_ubuntu_bridge/calibration_chassis_interfaces/msg/DifferentialCalibrationParams.msg b/agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_chassis_interfaces/msg/DifferentialCalibrationParams.msg similarity index 100% rename from agv_calib_brain/src/win_ubuntu_bridge/calibration_chassis_interfaces/msg/DifferentialCalibrationParams.msg rename to agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_chassis_interfaces/msg/DifferentialCalibrationParams.msg diff --git a/agv_calib_brain/src/win_ubuntu_bridge/calibration_chassis_interfaces/msg/GetAppliedChassisCalibrationParametersRequest.msg b/agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_chassis_interfaces/msg/GetAppliedChassisCalibrationParametersRequest.msg similarity index 100% rename from agv_calib_brain/src/win_ubuntu_bridge/calibration_chassis_interfaces/msg/GetAppliedChassisCalibrationParametersRequest.msg rename to agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_chassis_interfaces/msg/GetAppliedChassisCalibrationParametersRequest.msg diff --git a/agv_calib_brain/src/win_ubuntu_bridge/calibration_chassis_interfaces/msg/InPlaceRotationCommand.msg b/agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_chassis_interfaces/msg/InPlaceRotationCommand.msg similarity index 100% rename from agv_calib_brain/src/win_ubuntu_bridge/calibration_chassis_interfaces/msg/InPlaceRotationCommand.msg rename to agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_chassis_interfaces/msg/InPlaceRotationCommand.msg diff --git a/agv_calib_brain/src/win_ubuntu_bridge/calibration_chassis_interfaces/msg/LateralTranslationCommand.msg b/agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_chassis_interfaces/msg/LateralTranslationCommand.msg similarity index 100% rename from agv_calib_brain/src/win_ubuntu_bridge/calibration_chassis_interfaces/msg/LateralTranslationCommand.msg rename to agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_chassis_interfaces/msg/LateralTranslationCommand.msg diff --git a/agv_calib_brain/src/win_ubuntu_bridge/calibration_chassis_interfaces/msg/ModuleAlignmentCheckCommand.msg b/agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_chassis_interfaces/msg/ModuleAlignmentCheckCommand.msg similarity index 100% rename from agv_calib_brain/src/win_ubuntu_bridge/calibration_chassis_interfaces/msg/ModuleAlignmentCheckCommand.msg rename to agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_chassis_interfaces/msg/ModuleAlignmentCheckCommand.msg diff --git a/agv_calib_brain/src/win_ubuntu_bridge/calibration_chassis_interfaces/msg/MotionPrimitiveRequest.msg b/agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_chassis_interfaces/msg/MotionPrimitiveRequest.msg similarity index 100% rename from agv_calib_brain/src/win_ubuntu_bridge/calibration_chassis_interfaces/msg/MotionPrimitiveRequest.msg rename to agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_chassis_interfaces/msg/MotionPrimitiveRequest.msg diff --git a/agv_calib_brain/src/win_ubuntu_bridge/calibration_chassis_interfaces/msg/MotionPrimitiveTaskPurpose.msg b/agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_chassis_interfaces/msg/MotionPrimitiveTaskPurpose.msg similarity index 100% rename from agv_calib_brain/src/win_ubuntu_bridge/calibration_chassis_interfaces/msg/MotionPrimitiveTaskPurpose.msg rename to agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_chassis_interfaces/msg/MotionPrimitiveTaskPurpose.msg diff --git a/agv_calib_brain/src/win_ubuntu_bridge/calibration_chassis_interfaces/msg/MultiSteerWheelCalibrationParams.msg b/agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_chassis_interfaces/msg/MultiSteerWheelCalibrationParams.msg similarity index 100% rename from agv_calib_brain/src/win_ubuntu_bridge/calibration_chassis_interfaces/msg/MultiSteerWheelCalibrationParams.msg rename to agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_chassis_interfaces/msg/MultiSteerWheelCalibrationParams.msg diff --git a/agv_calib_brain/src/win_ubuntu_bridge/calibration_chassis_interfaces/msg/SingleSteerWheelCalibrationParams.msg b/agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_chassis_interfaces/msg/SingleSteerWheelCalibrationParams.msg similarity index 100% rename from agv_calib_brain/src/win_ubuntu_bridge/calibration_chassis_interfaces/msg/SingleSteerWheelCalibrationParams.msg rename to agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_chassis_interfaces/msg/SingleSteerWheelCalibrationParams.msg diff --git a/agv_calib_brain/src/win_ubuntu_bridge/calibration_chassis_interfaces/msg/SteeringModuleCalibrationParam.msg b/agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_chassis_interfaces/msg/SteeringModuleCalibrationParam.msg similarity index 100% rename from agv_calib_brain/src/win_ubuntu_bridge/calibration_chassis_interfaces/msg/SteeringModuleCalibrationParam.msg rename to agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_chassis_interfaces/msg/SteeringModuleCalibrationParam.msg diff --git a/agv_calib_brain/src/win_ubuntu_bridge/calibration_chassis_interfaces/msg/SteeringSweepCommand.msg b/agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_chassis_interfaces/msg/SteeringSweepCommand.msg similarity index 100% rename from agv_calib_brain/src/win_ubuntu_bridge/calibration_chassis_interfaces/msg/SteeringSweepCommand.msg rename to agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_chassis_interfaces/msg/SteeringSweepCommand.msg diff --git a/agv_calib_brain/src/win_ubuntu_bridge/calibration_chassis_interfaces/msg/StraightLineCommand.msg b/agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_chassis_interfaces/msg/StraightLineCommand.msg similarity index 100% rename from agv_calib_brain/src/win_ubuntu_bridge/calibration_chassis_interfaces/msg/StraightLineCommand.msg rename to agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_chassis_interfaces/msg/StraightLineCommand.msg diff --git a/agv_calib_brain/src/win_ubuntu_bridge/calibration_chassis_interfaces/msg/StreamChassisTelemetryRequest.msg b/agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_chassis_interfaces/msg/StreamChassisTelemetryRequest.msg similarity index 100% rename from agv_calib_brain/src/win_ubuntu_bridge/calibration_chassis_interfaces/msg/StreamChassisTelemetryRequest.msg rename to agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_chassis_interfaces/msg/StreamChassisTelemetryRequest.msg diff --git a/agv_calib_brain/src/win_ubuntu_bridge/calibration_chassis_interfaces/msg/WheelModuleState.msg b/agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_chassis_interfaces/msg/WheelModuleState.msg similarity index 100% rename from agv_calib_brain/src/win_ubuntu_bridge/calibration_chassis_interfaces/msg/WheelModuleState.msg rename to agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_chassis_interfaces/msg/WheelModuleState.msg diff --git a/agv_calib_brain/src/win_ubuntu_bridge/calibration_chassis_interfaces/package.xml b/agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_chassis_interfaces/package.xml similarity index 100% rename from agv_calib_brain/src/win_ubuntu_bridge/calibration_chassis_interfaces/package.xml rename to agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_chassis_interfaces/package.xml diff --git a/agv_calib_brain/src/win_ubuntu_bridge/calibration_chassis_interfaces/proto/chassis_calibration.proto b/agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_chassis_interfaces/proto/chassis_calibration.proto similarity index 100% rename from agv_calib_brain/src/win_ubuntu_bridge/calibration_chassis_interfaces/proto/chassis_calibration.proto rename to agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_chassis_interfaces/proto/chassis_calibration.proto diff --git a/agv_calib_brain/src/win_ubuntu_bridge/calibration_chassis_interfaces/srv/CancelChassisJob.srv b/agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_chassis_interfaces/srv/CancelChassisJob.srv similarity index 100% rename from agv_calib_brain/src/win_ubuntu_bridge/calibration_chassis_interfaces/srv/CancelChassisJob.srv rename to agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_chassis_interfaces/srv/CancelChassisJob.srv diff --git a/agv_calib_brain/src/win_ubuntu_bridge/calibration_chassis_interfaces/srv/CommitChassisCalibrationParameters.srv b/agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_chassis_interfaces/srv/CommitChassisCalibrationParameters.srv similarity index 100% rename from agv_calib_brain/src/win_ubuntu_bridge/calibration_chassis_interfaces/srv/CommitChassisCalibrationParameters.srv rename to agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_chassis_interfaces/srv/CommitChassisCalibrationParameters.srv diff --git a/agv_calib_brain/src/win_ubuntu_bridge/calibration_chassis_interfaces/srv/EmergencyBrake.srv b/agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_chassis_interfaces/srv/EmergencyBrake.srv similarity index 100% rename from agv_calib_brain/src/win_ubuntu_bridge/calibration_chassis_interfaces/srv/EmergencyBrake.srv rename to agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_chassis_interfaces/srv/EmergencyBrake.srv diff --git a/agv_calib_brain/src/win_ubuntu_bridge/calibration_chassis_interfaces/srv/GetAppliedChassisCalibrationParameters.srv b/agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_chassis_interfaces/srv/GetAppliedChassisCalibrationParameters.srv similarity index 100% rename from agv_calib_brain/src/win_ubuntu_bridge/calibration_chassis_interfaces/srv/GetAppliedChassisCalibrationParameters.srv rename to agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_chassis_interfaces/srv/GetAppliedChassisCalibrationParameters.srv diff --git a/agv_calib_brain/src/win_ubuntu_bridge/calibration_chassis_interfaces/srv/GetChassisCapability.srv b/agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_chassis_interfaces/srv/GetChassisCapability.srv similarity index 100% rename from agv_calib_brain/src/win_ubuntu_bridge/calibration_chassis_interfaces/srv/GetChassisCapability.srv rename to agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_chassis_interfaces/srv/GetChassisCapability.srv diff --git a/agv_calib_brain/src/win_ubuntu_bridge/calibration_chassis_interfaces/srv/GetChassisJobResult.srv b/agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_chassis_interfaces/srv/GetChassisJobResult.srv similarity index 100% rename from agv_calib_brain/src/win_ubuntu_bridge/calibration_chassis_interfaces/srv/GetChassisJobResult.srv rename to agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_chassis_interfaces/srv/GetChassisJobResult.srv diff --git a/agv_calib_brain/src/win_ubuntu_bridge/calibration_chassis_interfaces/srv/GetChassisJobStatus.srv b/agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_chassis_interfaces/srv/GetChassisJobStatus.srv similarity index 100% rename from agv_calib_brain/src/win_ubuntu_bridge/calibration_chassis_interfaces/srv/GetChassisJobStatus.srv rename to agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_chassis_interfaces/srv/GetChassisJobStatus.srv diff --git a/agv_calib_brain/src/win_ubuntu_bridge/calibration_chassis_interfaces/srv/GetChassisReadiness.srv b/agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_chassis_interfaces/srv/GetChassisReadiness.srv similarity index 100% rename from agv_calib_brain/src/win_ubuntu_bridge/calibration_chassis_interfaces/srv/GetChassisReadiness.srv rename to agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_chassis_interfaces/srv/GetChassisReadiness.srv diff --git a/agv_calib_brain/src/win_ubuntu_bridge/calibration_chassis_interfaces/srv/Heartbeat.srv b/agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_chassis_interfaces/srv/Heartbeat.srv similarity index 100% rename from agv_calib_brain/src/win_ubuntu_bridge/calibration_chassis_interfaces/srv/Heartbeat.srv rename to agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_chassis_interfaces/srv/Heartbeat.srv diff --git a/agv_calib_brain/src/win_ubuntu_bridge/calibration_chassis_interfaces/srv/SetChassisWorkMode.srv b/agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_chassis_interfaces/srv/SetChassisWorkMode.srv similarity index 100% rename from agv_calib_brain/src/win_ubuntu_bridge/calibration_chassis_interfaces/srv/SetChassisWorkMode.srv rename to agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_chassis_interfaces/srv/SetChassisWorkMode.srv diff --git a/agv_calib_brain/src/win_ubuntu_bridge/calibration_chassis_interfaces/srv/StartMotionPrimitive.srv b/agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_chassis_interfaces/srv/StartMotionPrimitive.srv similarity index 100% rename from agv_calib_brain/src/win_ubuntu_bridge/calibration_chassis_interfaces/srv/StartMotionPrimitive.srv rename to agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_chassis_interfaces/srv/StartMotionPrimitive.srv diff --git a/agv_calib_brain/src/win_ubuntu_bridge/calibration_chassis_interfaces/srv/StreamChassisTelemetry.srv b/agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_chassis_interfaces/srv/StreamChassisTelemetry.srv similarity index 100% rename from agv_calib_brain/src/win_ubuntu_bridge/calibration_chassis_interfaces/srv/StreamChassisTelemetry.srv rename to agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_chassis_interfaces/srv/StreamChassisTelemetry.srv diff --git a/agv_calib_brain/src/win_ubuntu_bridge/calibration_common_interfaces/CMakeLists.txt b/agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_common_interfaces/CMakeLists.txt similarity index 100% rename from agv_calib_brain/src/win_ubuntu_bridge/calibration_common_interfaces/CMakeLists.txt rename to agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_common_interfaces/CMakeLists.txt diff --git a/agv_calib_brain/src/win_ubuntu_bridge/calibration_common_interfaces/action/MonitorJob.action b/agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_common_interfaces/action/MonitorJob.action similarity index 100% rename from agv_calib_brain/src/win_ubuntu_bridge/calibration_common_interfaces/action/MonitorJob.action rename to agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_common_interfaces/action/MonitorJob.action diff --git a/agv_calib_brain/src/win_ubuntu_bridge/calibration_common_interfaces/msg/AgentReadinessRequest.msg b/agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_common_interfaces/msg/AgentReadinessRequest.msg similarity index 100% rename from agv_calib_brain/src/win_ubuntu_bridge/calibration_common_interfaces/msg/AgentReadinessRequest.msg rename to agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_common_interfaces/msg/AgentReadinessRequest.msg diff --git a/agv_calib_brain/src/win_ubuntu_bridge/calibration_common_interfaces/msg/ErrorCode.msg b/agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_common_interfaces/msg/ErrorCode.msg similarity index 100% rename from agv_calib_brain/src/win_ubuntu_bridge/calibration_common_interfaces/msg/ErrorCode.msg rename to agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_common_interfaces/msg/ErrorCode.msg diff --git a/agv_calib_brain/src/win_ubuntu_bridge/calibration_common_interfaces/msg/FileDigest.msg b/agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_common_interfaces/msg/FileDigest.msg similarity index 100% rename from agv_calib_brain/src/win_ubuntu_bridge/calibration_common_interfaces/msg/FileDigest.msg rename to agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_common_interfaces/msg/FileDigest.msg diff --git a/agv_calib_brain/src/win_ubuntu_bridge/calibration_common_interfaces/msg/FileReference.msg b/agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_common_interfaces/msg/FileReference.msg similarity index 100% rename from agv_calib_brain/src/win_ubuntu_bridge/calibration_common_interfaces/msg/FileReference.msg rename to agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_common_interfaces/msg/FileReference.msg diff --git a/agv_calib_brain/src/win_ubuntu_bridge/calibration_common_interfaces/msg/HeartbeatRequest.msg b/agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_common_interfaces/msg/HeartbeatRequest.msg similarity index 100% rename from agv_calib_brain/src/win_ubuntu_bridge/calibration_common_interfaces/msg/HeartbeatRequest.msg rename to agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_common_interfaces/msg/HeartbeatRequest.msg diff --git a/agv_calib_brain/src/win_ubuntu_bridge/calibration_common_interfaces/msg/HeartbeatResponse.msg b/agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_common_interfaces/msg/HeartbeatResponse.msg similarity index 100% rename from agv_calib_brain/src/win_ubuntu_bridge/calibration_common_interfaces/msg/HeartbeatResponse.msg rename to agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_common_interfaces/msg/HeartbeatResponse.msg diff --git a/agv_calib_brain/src/win_ubuntu_bridge/calibration_common_interfaces/msg/JobAccepted.msg b/agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_common_interfaces/msg/JobAccepted.msg similarity index 100% rename from agv_calib_brain/src/win_ubuntu_bridge/calibration_common_interfaces/msg/JobAccepted.msg rename to agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_common_interfaces/msg/JobAccepted.msg diff --git a/agv_calib_brain/src/win_ubuntu_bridge/calibration_common_interfaces/msg/JobQuery.msg b/agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_common_interfaces/msg/JobQuery.msg similarity index 100% rename from agv_calib_brain/src/win_ubuntu_bridge/calibration_common_interfaces/msg/JobQuery.msg rename to agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_common_interfaces/msg/JobQuery.msg diff --git a/agv_calib_brain/src/win_ubuntu_bridge/calibration_common_interfaces/msg/JobState.msg b/agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_common_interfaces/msg/JobState.msg similarity index 100% rename from agv_calib_brain/src/win_ubuntu_bridge/calibration_common_interfaces/msg/JobState.msg rename to agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_common_interfaces/msg/JobState.msg diff --git a/agv_calib_brain/src/win_ubuntu_bridge/calibration_common_interfaces/msg/JobStatus.msg b/agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_common_interfaces/msg/JobStatus.msg similarity index 100% rename from agv_calib_brain/src/win_ubuntu_bridge/calibration_common_interfaces/msg/JobStatus.msg rename to agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_common_interfaces/msg/JobStatus.msg diff --git a/agv_calib_brain/src/win_ubuntu_bridge/calibration_common_interfaces/msg/KeyValuePair.msg b/agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_common_interfaces/msg/KeyValuePair.msg similarity index 100% rename from agv_calib_brain/src/win_ubuntu_bridge/calibration_common_interfaces/msg/KeyValuePair.msg rename to agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_common_interfaces/msg/KeyValuePair.msg diff --git a/agv_calib_brain/src/win_ubuntu_bridge/calibration_common_interfaces/msg/Pose3D.msg b/agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_common_interfaces/msg/Pose3D.msg similarity index 100% rename from agv_calib_brain/src/win_ubuntu_bridge/calibration_common_interfaces/msg/Pose3D.msg rename to agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_common_interfaces/msg/Pose3D.msg diff --git a/agv_calib_brain/src/win_ubuntu_bridge/calibration_common_interfaces/msg/ReadinessIssue.msg b/agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_common_interfaces/msg/ReadinessIssue.msg similarity index 100% rename from agv_calib_brain/src/win_ubuntu_bridge/calibration_common_interfaces/msg/ReadinessIssue.msg rename to agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_common_interfaces/msg/ReadinessIssue.msg diff --git a/agv_calib_brain/src/win_ubuntu_bridge/calibration_common_interfaces/msg/RequestHeader.msg b/agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_common_interfaces/msg/RequestHeader.msg similarity index 100% rename from agv_calib_brain/src/win_ubuntu_bridge/calibration_common_interfaces/msg/RequestHeader.msg rename to agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_common_interfaces/msg/RequestHeader.msg diff --git a/agv_calib_brain/src/win_ubuntu_bridge/calibration_common_interfaces/msg/StandardResponse.msg b/agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_common_interfaces/msg/StandardResponse.msg similarity index 100% rename from agv_calib_brain/src/win_ubuntu_bridge/calibration_common_interfaces/msg/StandardResponse.msg rename to agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_common_interfaces/msg/StandardResponse.msg diff --git a/agv_calib_brain/src/win_ubuntu_bridge/calibration_common_interfaces/msg/Vector3D.msg b/agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_common_interfaces/msg/Vector3D.msg similarity index 100% rename from agv_calib_brain/src/win_ubuntu_bridge/calibration_common_interfaces/msg/Vector3D.msg rename to agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_common_interfaces/msg/Vector3D.msg diff --git a/agv_calib_brain/src/win_ubuntu_bridge/calibration_common_interfaces/package.xml b/agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_common_interfaces/package.xml similarity index 100% rename from agv_calib_brain/src/win_ubuntu_bridge/calibration_common_interfaces/package.xml rename to agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_common_interfaces/package.xml diff --git a/agv_calib_brain/src/win_ubuntu_bridge/calibration_common_interfaces/proto/calibration_common.proto b/agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_common_interfaces/proto/calibration_common.proto similarity index 99% rename from agv_calib_brain/src/win_ubuntu_bridge/calibration_common_interfaces/proto/calibration_common.proto rename to agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_common_interfaces/proto/calibration_common.proto index 4d44308..0013f90 100644 --- a/agv_calib_brain/src/win_ubuntu_bridge/calibration_common_interfaces/proto/calibration_common.proto +++ b/agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_common_interfaces/proto/calibration_common.proto @@ -3,8 +3,8 @@ syntax = "proto3"; package agv.calibration.common; // ├── msg/ -// │ ├── Vector3d.msg -// │ ├── Pose3dEuler.msg +// │ ├── Vector3D.msg +// │ ├── Pose3D.msg // │ ├── RequestHeader.msg // │ ├── ErrorCode.msg // │ ├── StandardResponse.msg @@ -263,4 +263,4 @@ message FileReference { message KeyValuePair { string key = 1; // 键 string value = 2; // 值 -} \ No newline at end of file +} diff --git a/agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_common_interfaces/proto/transport_contract.proto b/agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_common_interfaces/proto/transport_contract.proto new file mode 100644 index 0000000..8cb6376 --- /dev/null +++ b/agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_common_interfaces/proto/transport_contract.proto @@ -0,0 +1,146 @@ +syntax = "proto3"; + +package agv.calibration.transport; + +// ========================================================= +// 文件作用:WiFi6/TCP 传输契约 +// 使用范围: +// 1) Ubuntu 车间工控机与 Windows 车端代理之间的网络边界 +// 2) 仿真车端 agent 与真实车端 agent 需要共同遵守的帧号和通道约定 +// 3) 业务消息仍由 chassis/control/sensor/external_localization proto 定义 +// 说明: +// 1) WiFi6 是承载网络,当前工程传输层使用 TCP 长/短连接 +// 2) 当前仿真实现 payload 使用 proto 字段名风格的 JSON +// 3) 现场可切换为 protobuf binary,但必须保持本文件中的通道和帧号不变 +// ========================================================= + +// ========================================================= +// WiFi6 逻辑通道 +// ========================================================= +enum Wifi6Channel { + WIFI6_CHANNEL_UNSPECIFIED = 0; + WIFI6_CHANNEL_CHASSIS = 1; // 底盘标定动作和急停 + WIFI6_CHANNEL_CONTROL = 2; // 运控参数评估、轨迹跟踪 + WIFI6_CHANNEL_SENSOR = 3; // 车端传感器原始数据 + WIFI6_CHANNEL_EXTERNAL_POSE = 4; // 外部真值位姿输入到车端 +} + +// ========================================================= +// 默认端口 +// 说明: +// 1) WiFi6 边界默认只暴露一个车端 gateway 端口 +// 2) channel 只是协议里的逻辑通道,不应该被理解成“一类功能一个物理端口” +// 3) 9001-9004 仅保留给仿真 router 背后的内部域服务或旧版兼容使用 +// ========================================================= +enum Wifi6DefaultPort { + WIFI6_DEFAULT_PORT_UNSPECIFIED = 0; + WIFI6_DEFAULT_PORT_VEHICLE_GATEWAY = 9000; + WIFI6_LEGACY_INTERNAL_PORT_CHASSIS = 9001; + WIFI6_LEGACY_INTERNAL_PORT_CONTROL = 9002; + WIFI6_LEGACY_INTERNAL_PORT_SENSOR = 9003; + WIFI6_LEGACY_INTERNAL_PORT_EXTERNAL_POSE = 9004; +} + +// ========================================================= +// TCP 载荷编码方式 +// ========================================================= +enum Wifi6PayloadEncoding { + WIFI6_PAYLOAD_ENCODING_UNSPECIFIED = 0; + WIFI6_PAYLOAD_ENCODING_JSON_PROTO_FIELD_NAMES = 1; // 当前仿真实现:JSON key 使用 proto 字段名 + WIFI6_PAYLOAD_ENCODING_PROTOBUF_BINARY = 2; // 现场高性能实现可选 +} + +// ========================================================= +// TCP 连接方向 +// ========================================================= +enum Wifi6FrameDirection { + WIFI6_FRAME_DIRECTION_UNSPECIFIED = 0; + WIFI6_FRAME_DIRECTION_WORKSHOP_TO_VEHICLE = 1; + WIFI6_FRAME_DIRECTION_VEHICLE_TO_WORKSHOP = 2; +} + +// ========================================================= +// TCP 帧号 +// 帧头格式固定为: +// uint32 little-endian msg_type +// uint32 little-endian payload_len +// payload_len bytes payload +// 注意: +// 1) 这个 8 字节帧头不是 protobuf 序列化结果,而是传输层二进制头 +// 2) payload 的业务结构由本字段注释中对应的 proto 消息定义 +// 3) 逻辑 channel 由 msg_type 映射得到;同一个 gateway 端口根据 msg_type 做路由 +// ========================================================= +enum Wifi6FrameType { + WIFI6_FRAME_TYPE_UNSPECIFIED = 0; + + // 底盘域:chassis_calibration.proto / AgvCalibChassisService + WIFI6_FRAME_CHASSIS_GET_READINESS_REQ = 1; // AgentReadinessRequest + WIFI6_FRAME_CHASSIS_GET_READINESS_RSP = 2; // ChassisReadinessResponse + WIFI6_FRAME_CHASSIS_MOTION_PRIMITIVE_REQ = 3; // MotionPrimitiveRequest + WIFI6_FRAME_CHASSIS_MOTION_PRIMITIVE_RSP = 4; // ChassisJobResult + WIFI6_FRAME_CHASSIS_EMERGENCY_BRAKE_REQ = 5; // Empty 或 EmergencyBrake 请求 + WIFI6_FRAME_CHASSIS_EMERGENCY_BRAKE_RSP = 6; // StandardResponse + + // 运控域:control_calibration.proto / AgvCalibControlService + WIFI6_FRAME_CONTROL_GET_READINESS_REQ = 11; // AgentReadinessRequest + WIFI6_FRAME_CONTROL_GET_READINESS_RSP = 12; // ControlReadinessResponse + WIFI6_FRAME_CONTROL_EVALUATION_REQ = 13; // ControllerEvaluationRequest + WIFI6_FRAME_CONTROL_EVALUATION_RSP = 14; // ControlJobResult + + // 传感器域:sensor_calibration.proto / AgvCalibSensorService + WIFI6_FRAME_SENSOR_GET_READINESS_REQ = 21; // AgentReadinessRequest + WIFI6_FRAME_SENSOR_GET_READINESS_RSP = 22; // SensorReadinessResponse + WIFI6_FRAME_SENSOR_GET_LATEST_FRAME_REQ = 23; // StreamVehicleSensorDataRequest 的轻量轮询形态 + WIFI6_FRAME_SENSOR_GET_LATEST_FRAME_RSP = 24; // VehicleSensorFrame + WIFI6_FRAME_SENSOR_LIST_SENSORS_REQ = 25; // Empty 或能力查询请求 + WIFI6_FRAME_SENSOR_LIST_SENSORS_RSP = 26; // Sensor 列表响应 + + // 外部真值位姿域:external_localization.proto / AgvCalibExternalPoseFeedService + WIFI6_FRAME_EXTERNAL_POSE_PUSH_REQ = 31; // ExternalLocalizationTelemetry + WIFI6_FRAME_EXTERNAL_POSE_PUSH_RSP = 32; // StandardResponse +} + +// ========================================================= +// TCP 帧头的 protobuf 表达 +// 说明: +// 1) 这个 message 仅用于文档、测试、代码生成时表达契约 +// 2) 实际线上帧头仍是上面注释中定义的 8 字节小端二进制结构 +// ========================================================= +message Wifi6TcpFrameHeader { + Wifi6FrameType msg_type = 1; // 对应 8 字节帧头中的 uint32 msg_type + uint32 payload_len = 2; // 对应 8 字节帧头中的 uint32 payload_len +} + +// ========================================================= +// 逻辑通道端点 +// ========================================================= +message Wifi6ChannelEndpoint { + Wifi6Channel channel = 1; + Wifi6DefaultPort default_port = 2; + Wifi6FrameDirection request_direction = 3; + string canonical_proto_file = 4; // 例如 chassis_calibration.proto + string canonical_service = 5; // 例如 AgvCalibChassisService +} + +// ========================================================= +// 请求 / 响应帧映射 +// ========================================================= +message Wifi6FrameMapping { + Wifi6Channel channel = 1; + Wifi6FrameType request_type = 2; + Wifi6FrameType response_type = 3; + Wifi6PayloadEncoding payload_encoding = 4; + string request_message = 5; // proto 消息名 + string response_message = 6; // proto 消息名 + string rpc_name = 7; // 对齐的 RPC 名称;轻量轮询可为空 +} + +// ========================================================= +// 当前工程默认契约版本 +// ========================================================= +message Wifi6TransportContract { + string contract_version = 1; // 当前为 "wifi6_tcp_v1" + Wifi6PayloadEncoding default_payload_encoding = 2; + repeated Wifi6ChannelEndpoint endpoints = 3; + repeated Wifi6FrameMapping frame_mappings = 4; +} diff --git a/agv_calib_brain/src/win_ubuntu_bridge/calibration_common_interfaces/srv/GetAgentReadiness.srv b/agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_common_interfaces/srv/GetAgentReadiness.srv similarity index 100% rename from agv_calib_brain/src/win_ubuntu_bridge/calibration_common_interfaces/srv/GetAgentReadiness.srv rename to agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_common_interfaces/srv/GetAgentReadiness.srv diff --git a/agv_calib_brain/src/win_ubuntu_bridge/calibration_common_interfaces/srv/Heartbeat.srv b/agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_common_interfaces/srv/Heartbeat.srv similarity index 100% rename from agv_calib_brain/src/win_ubuntu_bridge/calibration_common_interfaces/srv/Heartbeat.srv rename to agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_common_interfaces/srv/Heartbeat.srv diff --git a/agv_calib_brain/src/win_ubuntu_bridge/calibration_common_interfaces/srv/QueryJobStatus.srv b/agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_common_interfaces/srv/QueryJobStatus.srv similarity index 100% rename from agv_calib_brain/src/win_ubuntu_bridge/calibration_common_interfaces/srv/QueryJobStatus.srv rename to agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_common_interfaces/srv/QueryJobStatus.srv diff --git a/agv_calib_brain/src/win_ubuntu_bridge/calibration_control_interfaces/CMakeLists.txt b/agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_control_interfaces/CMakeLists.txt similarity index 100% rename from agv_calib_brain/src/win_ubuntu_bridge/calibration_control_interfaces/CMakeLists.txt rename to agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_control_interfaces/CMakeLists.txt diff --git a/agv_calib_brain/src/win_ubuntu_bridge/calibration_control_interfaces/FILE_TREE.txt b/agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_control_interfaces/FILE_TREE.txt similarity index 100% rename from agv_calib_brain/src/win_ubuntu_bridge/calibration_control_interfaces/FILE_TREE.txt rename to agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_control_interfaces/FILE_TREE.txt diff --git a/agv_calib_brain/src/win_ubuntu_bridge/calibration_control_interfaces/action/ExecuteControllerEvaluation.action b/agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_control_interfaces/action/ExecuteControllerEvaluation.action similarity index 100% rename from agv_calib_brain/src/win_ubuntu_bridge/calibration_control_interfaces/action/ExecuteControllerEvaluation.action rename to agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_control_interfaces/action/ExecuteControllerEvaluation.action diff --git a/agv_calib_brain/src/win_ubuntu_bridge/calibration_control_interfaces/msg/AccelerationDecelerationTask.msg b/agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_control_interfaces/msg/AccelerationDecelerationTask.msg similarity index 100% rename from agv_calib_brain/src/win_ubuntu_bridge/calibration_control_interfaces/msg/AccelerationDecelerationTask.msg rename to agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_control_interfaces/msg/AccelerationDecelerationTask.msg diff --git a/agv_calib_brain/src/win_ubuntu_bridge/calibration_control_interfaces/msg/ActiveControllerParametersResponse.msg b/agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_control_interfaces/msg/ActiveControllerParametersResponse.msg similarity index 100% rename from agv_calib_brain/src/win_ubuntu_bridge/calibration_control_interfaces/msg/ActiveControllerParametersResponse.msg rename to agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_control_interfaces/msg/ActiveControllerParametersResponse.msg diff --git a/agv_calib_brain/src/win_ubuntu_bridge/calibration_control_interfaces/msg/CommitControllerParametersRequest.msg b/agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_control_interfaces/msg/CommitControllerParametersRequest.msg similarity index 100% rename from agv_calib_brain/src/win_ubuntu_bridge/calibration_control_interfaces/msg/CommitControllerParametersRequest.msg rename to agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_control_interfaces/msg/CommitControllerParametersRequest.msg diff --git a/agv_calib_brain/src/win_ubuntu_bridge/calibration_control_interfaces/msg/ControlJobResult.msg b/agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_control_interfaces/msg/ControlJobResult.msg similarity index 100% rename from agv_calib_brain/src/win_ubuntu_bridge/calibration_control_interfaces/msg/ControlJobResult.msg rename to agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_control_interfaces/msg/ControlJobResult.msg diff --git a/agv_calib_brain/src/win_ubuntu_bridge/calibration_control_interfaces/msg/ControlReadinessResponse.msg b/agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_control_interfaces/msg/ControlReadinessResponse.msg similarity index 68% rename from agv_calib_brain/src/win_ubuntu_bridge/calibration_control_interfaces/msg/ControlReadinessResponse.msg rename to agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_control_interfaces/msg/ControlReadinessResponse.msg index 87dfd3c..60ad539 100644 --- a/agv_calib_brain/src/win_ubuntu_bridge/calibration_control_interfaces/msg/ControlReadinessResponse.msg +++ b/agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_control_interfaces/msg/ControlReadinessResponse.msg @@ -26,3 +26,14 @@ bool vehicle_safe_to_move calibration_common_interfaces/ReadinessIssue[] issues # 检查时间 int64 checked_timestamp_us + +# 外部真值 / 定位位姿输入是否就绪 +bool external_pose_feedback_ready +# 当前外部位姿源名称 +string external_pose_source_name +# 最近一帧外部位姿年龄 +float64 external_pose_age_ms +# 最近一帧外部位姿质量分数 +float64 external_pose_quality_score +# 外部位姿传输方式,例如 wifi6 +string external_pose_transport diff --git a/agv_calib_brain/src/win_ubuntu_bridge/calibration_control_interfaces/msg/ControlTelemetry.msg b/agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_control_interfaces/msg/ControlTelemetry.msg similarity index 87% rename from agv_calib_brain/src/win_ubuntu_bridge/calibration_control_interfaces/msg/ControlTelemetry.msg rename to agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_control_interfaces/msg/ControlTelemetry.msg index cbc74ee..fe477c2 100644 --- a/agv_calib_brain/src/win_ubuntu_bridge/calibration_control_interfaces/msg/ControlTelemetry.msg +++ b/agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_control_interfaces/msg/ControlTelemetry.msg @@ -43,3 +43,8 @@ string parameter_version calibration_vehicle_profile_interfaces/ControllerAlgorithmType active_lateral_algorithm # 当前纵向算法 calibration_vehicle_profile_interfaces/ControllerAlgorithmType active_longitudinal_algorithm + +# 本帧控制使用的位姿源名称 +string pose_source_name +# 本帧位姿是否来自外部真值 / 外部定位 +bool pose_from_external_truth diff --git a/agv_calib_brain/src/win_ubuntu_bridge/calibration_control_interfaces/msg/ControlValidationSummary.msg b/agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_control_interfaces/msg/ControlValidationSummary.msg similarity index 100% rename from agv_calib_brain/src/win_ubuntu_bridge/calibration_control_interfaces/msg/ControlValidationSummary.msg rename to agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_control_interfaces/msg/ControlValidationSummary.msg diff --git a/agv_calib_brain/src/win_ubuntu_bridge/calibration_control_interfaces/msg/ControlWorkMode.msg b/agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_control_interfaces/msg/ControlWorkMode.msg similarity index 100% rename from agv_calib_brain/src/win_ubuntu_bridge/calibration_control_interfaces/msg/ControlWorkMode.msg rename to agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_control_interfaces/msg/ControlWorkMode.msg diff --git a/agv_calib_brain/src/win_ubuntu_bridge/calibration_control_interfaces/msg/ControlWorkModeRequest.msg b/agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_control_interfaces/msg/ControlWorkModeRequest.msg similarity index 100% rename from agv_calib_brain/src/win_ubuntu_bridge/calibration_control_interfaces/msg/ControlWorkModeRequest.msg rename to agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_control_interfaces/msg/ControlWorkModeRequest.msg diff --git a/agv_calib_brain/src/win_ubuntu_bridge/calibration_control_interfaces/msg/ControllerEvaluationRequest.msg b/agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_control_interfaces/msg/ControllerEvaluationRequest.msg similarity index 100% rename from agv_calib_brain/src/win_ubuntu_bridge/calibration_control_interfaces/msg/ControllerEvaluationRequest.msg rename to agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_control_interfaces/msg/ControllerEvaluationRequest.msg diff --git a/agv_calib_brain/src/win_ubuntu_bridge/calibration_control_interfaces/msg/ControllerEvaluationTaskPurpose.msg b/agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_control_interfaces/msg/ControllerEvaluationTaskPurpose.msg similarity index 100% rename from agv_calib_brain/src/win_ubuntu_bridge/calibration_control_interfaces/msg/ControllerEvaluationTaskPurpose.msg rename to agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_control_interfaces/msg/ControllerEvaluationTaskPurpose.msg diff --git a/agv_calib_brain/src/win_ubuntu_bridge/calibration_control_interfaces/msg/ControllerEvaluationTaskType.msg b/agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_control_interfaces/msg/ControllerEvaluationTaskType.msg similarity index 100% rename from agv_calib_brain/src/win_ubuntu_bridge/calibration_control_interfaces/msg/ControllerEvaluationTaskType.msg rename to agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_control_interfaces/msg/ControllerEvaluationTaskType.msg diff --git a/agv_calib_brain/src/win_ubuntu_bridge/calibration_control_interfaces/msg/ControllerParameterKind.msg b/agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_control_interfaces/msg/ControllerParameterKind.msg similarity index 100% rename from agv_calib_brain/src/win_ubuntu_bridge/calibration_control_interfaces/msg/ControllerParameterKind.msg rename to agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_control_interfaces/msg/ControllerParameterKind.msg diff --git a/agv_calib_brain/src/win_ubuntu_bridge/calibration_control_interfaces/msg/ControllerParameterPack.msg b/agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_control_interfaces/msg/ControllerParameterPack.msg similarity index 100% rename from agv_calib_brain/src/win_ubuntu_bridge/calibration_control_interfaces/msg/ControllerParameterPack.msg rename to agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_control_interfaces/msg/ControllerParameterPack.msg diff --git a/agv_calib_brain/src/win_ubuntu_bridge/calibration_control_interfaces/msg/ControllerParameterSet.msg b/agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_control_interfaces/msg/ControllerParameterSet.msg similarity index 100% rename from agv_calib_brain/src/win_ubuntu_bridge/calibration_control_interfaces/msg/ControllerParameterSet.msg rename to agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_control_interfaces/msg/ControllerParameterSet.msg diff --git a/agv_calib_brain/src/win_ubuntu_bridge/calibration_control_interfaces/msg/GetActiveControllerParametersRequest.msg b/agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_control_interfaces/msg/GetActiveControllerParametersRequest.msg similarity index 100% rename from agv_calib_brain/src/win_ubuntu_bridge/calibration_control_interfaces/msg/GetActiveControllerParametersRequest.msg rename to agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_control_interfaces/msg/GetActiveControllerParametersRequest.msg diff --git a/agv_calib_brain/src/win_ubuntu_bridge/calibration_control_interfaces/msg/InjectControllerParametersRequest.msg b/agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_control_interfaces/msg/InjectControllerParametersRequest.msg similarity index 100% rename from agv_calib_brain/src/win_ubuntu_bridge/calibration_control_interfaces/msg/InjectControllerParametersRequest.msg rename to agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_control_interfaces/msg/InjectControllerParametersRequest.msg diff --git a/agv_calib_brain/src/win_ubuntu_bridge/calibration_control_interfaces/msg/LQRParams.msg b/agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_control_interfaces/msg/LQRParams.msg similarity index 100% rename from agv_calib_brain/src/win_ubuntu_bridge/calibration_control_interfaces/msg/LQRParams.msg rename to agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_control_interfaces/msg/LQRParams.msg diff --git a/agv_calib_brain/src/win_ubuntu_bridge/calibration_control_interfaces/msg/LateralMPCParams.msg b/agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_control_interfaces/msg/LateralMPCParams.msg similarity index 100% rename from agv_calib_brain/src/win_ubuntu_bridge/calibration_control_interfaces/msg/LateralMPCParams.msg rename to agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_control_interfaces/msg/LateralMPCParams.msg diff --git a/agv_calib_brain/src/win_ubuntu_bridge/calibration_control_interfaces/msg/LongitudinalMPCParams.msg b/agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_control_interfaces/msg/LongitudinalMPCParams.msg similarity index 100% rename from agv_calib_brain/src/win_ubuntu_bridge/calibration_control_interfaces/msg/LongitudinalMPCParams.msg rename to agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_control_interfaces/msg/LongitudinalMPCParams.msg diff --git a/agv_calib_brain/src/win_ubuntu_bridge/calibration_control_interfaces/msg/PIDParams.msg b/agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_control_interfaces/msg/PIDParams.msg similarity index 100% rename from agv_calib_brain/src/win_ubuntu_bridge/calibration_control_interfaces/msg/PIDParams.msg rename to agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_control_interfaces/msg/PIDParams.msg diff --git a/agv_calib_brain/src/win_ubuntu_bridge/calibration_control_interfaces/msg/PurePursuitParams.msg b/agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_control_interfaces/msg/PurePursuitParams.msg similarity index 100% rename from agv_calib_brain/src/win_ubuntu_bridge/calibration_control_interfaces/msg/PurePursuitParams.msg rename to agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_control_interfaces/msg/PurePursuitParams.msg diff --git a/agv_calib_brain/src/win_ubuntu_bridge/calibration_control_interfaces/msg/StopAccuracyTask.msg b/agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_control_interfaces/msg/StopAccuracyTask.msg similarity index 100% rename from agv_calib_brain/src/win_ubuntu_bridge/calibration_control_interfaces/msg/StopAccuracyTask.msg rename to agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_control_interfaces/msg/StopAccuracyTask.msg diff --git a/agv_calib_brain/src/win_ubuntu_bridge/calibration_control_interfaces/msg/StreamControlTelemetryRequest.msg b/agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_control_interfaces/msg/StreamControlTelemetryRequest.msg similarity index 100% rename from agv_calib_brain/src/win_ubuntu_bridge/calibration_control_interfaces/msg/StreamControlTelemetryRequest.msg rename to agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_control_interfaces/msg/StreamControlTelemetryRequest.msg diff --git a/agv_calib_brain/src/win_ubuntu_bridge/calibration_control_interfaces/msg/TrajectoryPoint.msg b/agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_control_interfaces/msg/TrajectoryPoint.msg similarity index 100% rename from agv_calib_brain/src/win_ubuntu_bridge/calibration_control_interfaces/msg/TrajectoryPoint.msg rename to agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_control_interfaces/msg/TrajectoryPoint.msg diff --git a/agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_control_interfaces/msg/TrajectoryTrackingTask.msg b/agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_control_interfaces/msg/TrajectoryTrackingTask.msg new file mode 100644 index 0000000..353ff30 --- /dev/null +++ b/agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_control_interfaces/msg/TrajectoryTrackingTask.msg @@ -0,0 +1,25 @@ +# ========================================================= +# 轨迹跟踪评估任务 +# 作用:让车端按当前控制参数跑一条测试轨迹 +# ========================================================= + +# 轨迹点序列 +calibration_control_interfaces/TrajectoryPoint[] path +# 结束后是否停车 +bool stop_at_end +# 超时时间 +float64 timeout_sec +# 要求使用的外部位姿源 ID +string required_external_pose_source_id +# 允许最大外部位姿延迟,0 表示使用车端默认值 +float64 max_external_pose_age_ms +# 允许最小外部位姿质量分数,0 表示使用车端默认值 +float64 min_external_pose_quality_score +# 轨迹 ID,用于分段下发时关联多段轨迹 +string trajectory_id +# 当前轨迹段序号,从 0 开始 +uint32 segment_index +# 总段数;0 或 1 表示本请求携带完整轨迹 +uint32 total_segments +# 当前段是否为最后一段,作为 total_segments 的冗余校验 +bool is_final_segment diff --git a/agv_calib_brain/src/win_ubuntu_bridge/calibration_control_interfaces/msg/VelocityStepTask.msg b/agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_control_interfaces/msg/VelocityStepTask.msg similarity index 100% rename from agv_calib_brain/src/win_ubuntu_bridge/calibration_control_interfaces/msg/VelocityStepTask.msg rename to agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_control_interfaces/msg/VelocityStepTask.msg diff --git a/agv_calib_brain/src/win_ubuntu_bridge/calibration_control_interfaces/package.xml b/agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_control_interfaces/package.xml similarity index 100% rename from agv_calib_brain/src/win_ubuntu_bridge/calibration_control_interfaces/package.xml rename to agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_control_interfaces/package.xml diff --git a/agv_calib_brain/src/win_ubuntu_bridge/win_ubuntu_bridge/proto/control_calibration.proto b/agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_control_interfaces/proto/control_calibration.proto similarity index 93% rename from agv_calib_brain/src/win_ubuntu_bridge/win_ubuntu_bridge/proto/control_calibration.proto rename to agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_control_interfaces/proto/control_calibration.proto index 61a04c4..307e21b 100644 --- a/agv_calib_brain/src/win_ubuntu_bridge/win_ubuntu_bridge/proto/control_calibration.proto +++ b/agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_control_interfaces/proto/control_calibration.proto @@ -115,6 +115,12 @@ message ControlReadinessResponse { bool vehicle_safe_to_move = 9; // 当前车辆是否允许移动 repeated .agv.calibration.common.ReadinessIssue issues = 10; // 不满足项 int64 checked_timestamp_us = 11; // 检查时间 + + bool external_pose_feedback_ready = 12; // 外部真值 / 定位位姿输入是否就绪 + string external_pose_source_name = 13; // 当前外部位姿源名称 + double external_pose_age_ms = 14; // 最近一帧外部位姿年龄 + double external_pose_quality_score = 15; // 最近一帧外部位姿质量分数 + string external_pose_transport = 16; // 外部位姿传输方式,例如 wifi6 } // ========================================================= @@ -249,6 +255,13 @@ message TrajectoryTrackingTask { repeated TrajectoryPoint path = 1; // 轨迹点序列 bool stop_at_end = 2; // 结束后是否停车 double timeout_sec = 3; // 超时时间 + string required_external_pose_source_id = 4; // 要求使用的外部位姿源 ID + double max_external_pose_age_ms = 5; // 允许最大外部位姿延迟 + double min_external_pose_quality_score = 6; // 允许最小外部位姿质量分数 + string trajectory_id = 7; // 轨迹 ID,用于分段下发时关联多段轨迹 + uint32 segment_index = 8; // 当前轨迹段序号,从 0 开始 + uint32 total_segments = 9; // 总段数;0 或 1 表示本请求携带完整轨迹 + bool is_final_segment = 10; // 当前段是否为最后一段,作为 total_segments 的冗余校验 } // ========================================================= @@ -353,6 +366,9 @@ message ControlTelemetry { string parameter_version = 15; // 当前生效参数版本 .agv.calibration.vehicle.profile.ControllerAlgorithmType active_lateral_algorithm = 16; // 当前横向算法 .agv.calibration.vehicle.profile.ControllerAlgorithmType active_longitudinal_algorithm = 17; // 当前纵向算法 + + string pose_source_name = 18; // 本帧控制使用的位姿源名称 + bool pose_from_external_truth = 19; // 本帧位姿是否来自外部真值 / 外部定位 } // ========================================================= @@ -420,4 +436,4 @@ message ControlJobResult { ControlValidationSummary validation_summary = 8; // 验证摘要 ControllerParameterSet estimated_parameter_set = 9; // 本轮估计参数集 repeated .agv.calibration.common.FileReference artifacts = 10; // 关联产物 -} \ No newline at end of file +} diff --git a/agv_calib_brain/src/win_ubuntu_bridge/calibration_control_interfaces/srv/CancelControlJob.srv b/agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_control_interfaces/srv/CancelControlJob.srv similarity index 100% rename from agv_calib_brain/src/win_ubuntu_bridge/calibration_control_interfaces/srv/CancelControlJob.srv rename to agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_control_interfaces/srv/CancelControlJob.srv diff --git a/agv_calib_brain/src/win_ubuntu_bridge/calibration_control_interfaces/srv/CommitControllerParameters.srv b/agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_control_interfaces/srv/CommitControllerParameters.srv similarity index 100% rename from agv_calib_brain/src/win_ubuntu_bridge/calibration_control_interfaces/srv/CommitControllerParameters.srv rename to agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_control_interfaces/srv/CommitControllerParameters.srv diff --git a/agv_calib_brain/src/win_ubuntu_bridge/calibration_control_interfaces/srv/EmergencyStop.srv b/agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_control_interfaces/srv/EmergencyStop.srv similarity index 100% rename from agv_calib_brain/src/win_ubuntu_bridge/calibration_control_interfaces/srv/EmergencyStop.srv rename to agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_control_interfaces/srv/EmergencyStop.srv diff --git a/agv_calib_brain/src/win_ubuntu_bridge/calibration_control_interfaces/srv/GetActiveControllerParameters.srv b/agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_control_interfaces/srv/GetActiveControllerParameters.srv similarity index 100% rename from agv_calib_brain/src/win_ubuntu_bridge/calibration_control_interfaces/srv/GetActiveControllerParameters.srv rename to agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_control_interfaces/srv/GetActiveControllerParameters.srv diff --git a/agv_calib_brain/src/win_ubuntu_bridge/calibration_control_interfaces/srv/GetControlJobResult.srv b/agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_control_interfaces/srv/GetControlJobResult.srv similarity index 100% rename from agv_calib_brain/src/win_ubuntu_bridge/calibration_control_interfaces/srv/GetControlJobResult.srv rename to agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_control_interfaces/srv/GetControlJobResult.srv diff --git a/agv_calib_brain/src/win_ubuntu_bridge/calibration_control_interfaces/srv/GetControlJobStatus.srv b/agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_control_interfaces/srv/GetControlJobStatus.srv similarity index 100% rename from agv_calib_brain/src/win_ubuntu_bridge/calibration_control_interfaces/srv/GetControlJobStatus.srv rename to agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_control_interfaces/srv/GetControlJobStatus.srv diff --git a/agv_calib_brain/src/win_ubuntu_bridge/calibration_control_interfaces/srv/GetControlReadiness.srv b/agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_control_interfaces/srv/GetControlReadiness.srv similarity index 100% rename from agv_calib_brain/src/win_ubuntu_bridge/calibration_control_interfaces/srv/GetControlReadiness.srv rename to agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_control_interfaces/srv/GetControlReadiness.srv diff --git a/agv_calib_brain/src/win_ubuntu_bridge/calibration_control_interfaces/srv/Heartbeat.srv b/agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_control_interfaces/srv/Heartbeat.srv similarity index 100% rename from agv_calib_brain/src/win_ubuntu_bridge/calibration_control_interfaces/srv/Heartbeat.srv rename to agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_control_interfaces/srv/Heartbeat.srv diff --git a/agv_calib_brain/src/win_ubuntu_bridge/calibration_control_interfaces/srv/InjectControllerParameters.srv b/agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_control_interfaces/srv/InjectControllerParameters.srv similarity index 100% rename from agv_calib_brain/src/win_ubuntu_bridge/calibration_control_interfaces/srv/InjectControllerParameters.srv rename to agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_control_interfaces/srv/InjectControllerParameters.srv diff --git a/agv_calib_brain/src/win_ubuntu_bridge/calibration_control_interfaces/srv/SetControlWorkMode.srv b/agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_control_interfaces/srv/SetControlWorkMode.srv similarity index 100% rename from agv_calib_brain/src/win_ubuntu_bridge/calibration_control_interfaces/srv/SetControlWorkMode.srv rename to agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_control_interfaces/srv/SetControlWorkMode.srv diff --git a/agv_calib_brain/src/win_ubuntu_bridge/calibration_control_interfaces/srv/StartControllerEvaluation.srv b/agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_control_interfaces/srv/StartControllerEvaluation.srv similarity index 100% rename from agv_calib_brain/src/win_ubuntu_bridge/calibration_control_interfaces/srv/StartControllerEvaluation.srv rename to agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_control_interfaces/srv/StartControllerEvaluation.srv diff --git a/agv_calib_brain/src/win_ubuntu_bridge/calibration_control_interfaces/srv/StreamControlTelemetry.srv b/agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_control_interfaces/srv/StreamControlTelemetry.srv similarity index 100% rename from agv_calib_brain/src/win_ubuntu_bridge/calibration_control_interfaces/srv/StreamControlTelemetry.srv rename to agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_control_interfaces/srv/StreamControlTelemetry.srv diff --git a/agv_calib_brain/src/win_ubuntu_bridge/calibration_external_localization_interfaces/CMakeLists.txt b/agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_external_localization_interfaces/CMakeLists.txt similarity index 100% rename from agv_calib_brain/src/win_ubuntu_bridge/calibration_external_localization_interfaces/CMakeLists.txt rename to agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_external_localization_interfaces/CMakeLists.txt diff --git a/agv_calib_brain/src/win_ubuntu_bridge/calibration_external_localization_interfaces/FILE_TREE.txt b/agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_external_localization_interfaces/FILE_TREE.txt similarity index 100% rename from agv_calib_brain/src/win_ubuntu_bridge/calibration_external_localization_interfaces/FILE_TREE.txt rename to agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_external_localization_interfaces/FILE_TREE.txt diff --git a/agv_calib_brain/src/win_ubuntu_bridge/calibration_external_localization_interfaces/action/ExecuteExternalLocalizationTask.action b/agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_external_localization_interfaces/action/ExecuteExternalLocalizationTask.action similarity index 100% rename from agv_calib_brain/src/win_ubuntu_bridge/calibration_external_localization_interfaces/action/ExecuteExternalLocalizationTask.action rename to agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_external_localization_interfaces/action/ExecuteExternalLocalizationTask.action diff --git a/agv_calib_brain/src/win_ubuntu_bridge/calibration_external_localization_interfaces/msg/AppliedExternalLocalizationResultResponse.msg b/agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_external_localization_interfaces/msg/AppliedExternalLocalizationResultResponse.msg similarity index 100% rename from agv_calib_brain/src/win_ubuntu_bridge/calibration_external_localization_interfaces/msg/AppliedExternalLocalizationResultResponse.msg rename to agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_external_localization_interfaces/msg/AppliedExternalLocalizationResultResponse.msg diff --git a/agv_calib_brain/src/win_ubuntu_bridge/calibration_external_localization_interfaces/msg/CommitExternalLocalizationResultRequest.msg b/agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_external_localization_interfaces/msg/CommitExternalLocalizationResultRequest.msg similarity index 100% rename from agv_calib_brain/src/win_ubuntu_bridge/calibration_external_localization_interfaces/msg/CommitExternalLocalizationResultRequest.msg rename to agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_external_localization_interfaces/msg/CommitExternalLocalizationResultRequest.msg diff --git a/agv_calib_brain/src/win_ubuntu_bridge/calibration_external_localization_interfaces/msg/ExternalLocalizationCalibrationResult.msg b/agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_external_localization_interfaces/msg/ExternalLocalizationCalibrationResult.msg similarity index 100% rename from agv_calib_brain/src/win_ubuntu_bridge/calibration_external_localization_interfaces/msg/ExternalLocalizationCalibrationResult.msg rename to agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_external_localization_interfaces/msg/ExternalLocalizationCalibrationResult.msg diff --git a/agv_calib_brain/src/win_ubuntu_bridge/calibration_external_localization_interfaces/msg/ExternalLocalizationCapabilityRequest.msg b/agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_external_localization_interfaces/msg/ExternalLocalizationCapabilityRequest.msg similarity index 100% rename from agv_calib_brain/src/win_ubuntu_bridge/calibration_external_localization_interfaces/msg/ExternalLocalizationCapabilityRequest.msg rename to agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_external_localization_interfaces/msg/ExternalLocalizationCapabilityRequest.msg diff --git a/agv_calib_brain/src/win_ubuntu_bridge/calibration_external_localization_interfaces/msg/ExternalLocalizationCapabilityResponse.msg b/agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_external_localization_interfaces/msg/ExternalLocalizationCapabilityResponse.msg similarity index 100% rename from agv_calib_brain/src/win_ubuntu_bridge/calibration_external_localization_interfaces/msg/ExternalLocalizationCapabilityResponse.msg rename to agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_external_localization_interfaces/msg/ExternalLocalizationCapabilityResponse.msg diff --git a/agv_calib_brain/src/win_ubuntu_bridge/calibration_external_localization_interfaces/msg/ExternalLocalizationJobResult.msg b/agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_external_localization_interfaces/msg/ExternalLocalizationJobResult.msg similarity index 100% rename from agv_calib_brain/src/win_ubuntu_bridge/calibration_external_localization_interfaces/msg/ExternalLocalizationJobResult.msg rename to agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_external_localization_interfaces/msg/ExternalLocalizationJobResult.msg diff --git a/agv_calib_brain/src/win_ubuntu_bridge/calibration_external_localization_interfaces/msg/ExternalLocalizationReadinessResponse.msg b/agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_external_localization_interfaces/msg/ExternalLocalizationReadinessResponse.msg similarity index 100% rename from agv_calib_brain/src/win_ubuntu_bridge/calibration_external_localization_interfaces/msg/ExternalLocalizationReadinessResponse.msg rename to agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_external_localization_interfaces/msg/ExternalLocalizationReadinessResponse.msg diff --git a/agv_calib_brain/src/win_ubuntu_bridge/calibration_external_localization_interfaces/msg/ExternalLocalizationTaskPayloadType.msg b/agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_external_localization_interfaces/msg/ExternalLocalizationTaskPayloadType.msg similarity index 100% rename from agv_calib_brain/src/win_ubuntu_bridge/calibration_external_localization_interfaces/msg/ExternalLocalizationTaskPayloadType.msg rename to agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_external_localization_interfaces/msg/ExternalLocalizationTaskPayloadType.msg diff --git a/agv_calib_brain/src/win_ubuntu_bridge/calibration_external_localization_interfaces/msg/ExternalLocalizationTaskPurpose.msg b/agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_external_localization_interfaces/msg/ExternalLocalizationTaskPurpose.msg similarity index 100% rename from agv_calib_brain/src/win_ubuntu_bridge/calibration_external_localization_interfaces/msg/ExternalLocalizationTaskPurpose.msg rename to agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_external_localization_interfaces/msg/ExternalLocalizationTaskPurpose.msg diff --git a/agv_calib_brain/src/win_ubuntu_bridge/calibration_external_localization_interfaces/msg/ExternalLocalizationTaskRequest.msg b/agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_external_localization_interfaces/msg/ExternalLocalizationTaskRequest.msg similarity index 100% rename from agv_calib_brain/src/win_ubuntu_bridge/calibration_external_localization_interfaces/msg/ExternalLocalizationTaskRequest.msg rename to agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_external_localization_interfaces/msg/ExternalLocalizationTaskRequest.msg diff --git a/agv_calib_brain/src/win_ubuntu_bridge/calibration_external_localization_interfaces/msg/ExternalLocalizationTelemetry.msg b/agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_external_localization_interfaces/msg/ExternalLocalizationTelemetry.msg similarity index 100% rename from agv_calib_brain/src/win_ubuntu_bridge/calibration_external_localization_interfaces/msg/ExternalLocalizationTelemetry.msg rename to agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_external_localization_interfaces/msg/ExternalLocalizationTelemetry.msg diff --git a/agv_calib_brain/src/win_ubuntu_bridge/calibration_external_localization_interfaces/msg/ExternalLocalizationValidationSummary.msg b/agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_external_localization_interfaces/msg/ExternalLocalizationValidationSummary.msg similarity index 100% rename from agv_calib_brain/src/win_ubuntu_bridge/calibration_external_localization_interfaces/msg/ExternalLocalizationValidationSummary.msg rename to agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_external_localization_interfaces/msg/ExternalLocalizationValidationSummary.msg diff --git a/agv_calib_brain/src/win_ubuntu_bridge/calibration_external_localization_interfaces/msg/ExternalLocalizationWorkMode.msg b/agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_external_localization_interfaces/msg/ExternalLocalizationWorkMode.msg similarity index 100% rename from agv_calib_brain/src/win_ubuntu_bridge/calibration_external_localization_interfaces/msg/ExternalLocalizationWorkMode.msg rename to agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_external_localization_interfaces/msg/ExternalLocalizationWorkMode.msg diff --git a/agv_calib_brain/src/win_ubuntu_bridge/calibration_external_localization_interfaces/msg/ExternalLocalizationWorkModeRequest.msg b/agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_external_localization_interfaces/msg/ExternalLocalizationWorkModeRequest.msg similarity index 100% rename from agv_calib_brain/src/win_ubuntu_bridge/calibration_external_localization_interfaces/msg/ExternalLocalizationWorkModeRequest.msg rename to agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_external_localization_interfaces/msg/ExternalLocalizationWorkModeRequest.msg diff --git a/agv_calib_brain/src/win_ubuntu_bridge/calibration_external_localization_interfaces/msg/GetAppliedExternalLocalizationResultRequest.msg b/agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_external_localization_interfaces/msg/GetAppliedExternalLocalizationResultRequest.msg similarity index 100% rename from agv_calib_brain/src/win_ubuntu_bridge/calibration_external_localization_interfaces/msg/GetAppliedExternalLocalizationResultRequest.msg rename to agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_external_localization_interfaces/msg/GetAppliedExternalLocalizationResultRequest.msg diff --git a/agv_calib_brain/src/win_ubuntu_bridge/calibration_external_localization_interfaces/msg/MarkerAlignmentTask.msg b/agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_external_localization_interfaces/msg/MarkerAlignmentTask.msg similarity index 100% rename from agv_calib_brain/src/win_ubuntu_bridge/calibration_external_localization_interfaces/msg/MarkerAlignmentTask.msg rename to agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_external_localization_interfaces/msg/MarkerAlignmentTask.msg diff --git a/agv_calib_brain/src/win_ubuntu_bridge/calibration_external_localization_interfaces/msg/ReferencePoseCollectionTask.msg b/agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_external_localization_interfaces/msg/ReferencePoseCollectionTask.msg similarity index 100% rename from agv_calib_brain/src/win_ubuntu_bridge/calibration_external_localization_interfaces/msg/ReferencePoseCollectionTask.msg rename to agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_external_localization_interfaces/msg/ReferencePoseCollectionTask.msg diff --git a/agv_calib_brain/src/win_ubuntu_bridge/calibration_external_localization_interfaces/msg/StreamExternalLocalizationTelemetryRequest.msg b/agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_external_localization_interfaces/msg/StreamExternalLocalizationTelemetryRequest.msg similarity index 100% rename from agv_calib_brain/src/win_ubuntu_bridge/calibration_external_localization_interfaces/msg/StreamExternalLocalizationTelemetryRequest.msg rename to agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_external_localization_interfaces/msg/StreamExternalLocalizationTelemetryRequest.msg diff --git a/agv_calib_brain/src/win_ubuntu_bridge/calibration_external_localization_interfaces/msg/TruthSourceValidationTask.msg b/agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_external_localization_interfaces/msg/TruthSourceValidationTask.msg similarity index 100% rename from agv_calib_brain/src/win_ubuntu_bridge/calibration_external_localization_interfaces/msg/TruthSourceValidationTask.msg rename to agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_external_localization_interfaces/msg/TruthSourceValidationTask.msg diff --git a/agv_calib_brain/src/win_ubuntu_bridge/calibration_external_localization_interfaces/package.xml b/agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_external_localization_interfaces/package.xml similarity index 100% rename from agv_calib_brain/src/win_ubuntu_bridge/calibration_external_localization_interfaces/package.xml rename to agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_external_localization_interfaces/package.xml diff --git a/agv_calib_brain/src/win_ubuntu_bridge/win_ubuntu_bridge/proto/external_localization.proto b/agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_external_localization_interfaces/proto/external_localization.proto similarity index 87% rename from agv_calib_brain/src/win_ubuntu_bridge/win_ubuntu_bridge/proto/external_localization.proto rename to agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_external_localization_interfaces/proto/external_localization.proto index 35749b7..069bb81 100644 --- a/agv_calib_brain/src/win_ubuntu_bridge/win_ubuntu_bridge/proto/external_localization.proto +++ b/agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_external_localization_interfaces/proto/external_localization.proto @@ -69,6 +69,30 @@ service AgvCalibExternalLocalizationService { returns (.agv.calibration.common.StandardResponse); } +// ========================================================= +// 服务:外部真值位姿下发服务 +// 运行位置:Ubuntu 车间电脑 / 外部真值系统桥接进程 +// 调用方:Windows 车端代理 +// 传输介质:现场通过 WiFi 6 网络承载 +// 作用: +// 1) 让车端在执行轨迹跟踪时获得车间坐标系下的实时位姿 +// 2) 与 StreamExternalLocalizationTelemetry 使用同一位姿消息结构 +// 3) 车端不得把底盘里程计当成默认全局位姿源,只可作为降级输入 +// ========================================================= +service AgvCalibExternalPoseFeedService { + // 心跳保活,用于车端确认外部真值链路在线 + rpc Heartbeat(.agv.calibration.common.HeartbeatRequest) + returns (.agv.calibration.common.HeartbeatResponse); + + // 车端订阅外部真值位姿流 + rpc StreamExternalPoseToVehicle(ExternalPoseFeedRequest) + returns (stream ExternalLocalizationTelemetry); + + // 可选:外部真值系统主动推送单帧位姿到车端 + rpc PushExternalPoseToVehicle(ExternalLocalizationTelemetry) + returns (.agv.calibration.common.StandardResponse); +} + // ========================================================= // 外部定位工作模式请求 // 作用:切换外部定位桥接服务到不同模式 @@ -238,6 +262,20 @@ message StreamExternalLocalizationTelemetryRequest { bool include_tracking_state = 6; // 是否包含跟踪状态 } +// ========================================================= +// 外部真值位姿下发请求 +// 作用:车端通过 WiFi 6 请求工控机 / 外部真值桥接进程提供实时位姿流 +// ========================================================= +message ExternalPoseFeedRequest { + .agv.calibration.common.RequestHeader header = 1; // 请求头 + string vehicle_id = 2; // 目标车辆 ID + string localization_source_id = 3; // 外部真值源 ID + string workcell_zone_id = 4; // 工位 / 区域 ID + uint32 expected_hz = 5; // 期望下发频率 + double max_pose_age_ms = 6; // 允许最大位姿延迟 + bool require_quality_metrics = 7; // 是否要求质量指标 +} + // ========================================================= // 外部定位遥测 // 作用:回传外部定位实时观测结果 @@ -248,7 +286,7 @@ message ExternalLocalizationTelemetry { int64 hardware_timestamp_us = 1; // 硬件时间戳 bool pose_valid = 2; // 位姿是否有效 - .agv.calibration.common.Pose3dEuler workshop_pose = 3; // 在车间参考系下的位姿 + .agv.calibration.common.Pose3D workshop_pose = 3; // 在车间参考系下的位姿 double position_stddev_m = 4; // 位置标准差 double yaw_stddev_rad = 5; // 航向标准差 @@ -269,7 +307,7 @@ message ExternalLocalizationCalibrationResult { string workshop_frame_id = 1; // 车间参考坐标系 ID string localization_frame_id = 2; // 外部定位坐标系 ID - .agv.calibration.common.Pose3dEuler workshop_to_localization = 3; // 车间系到定位系的位姿 + .agv.calibration.common.Pose3D workshop_to_localization = 3; // 车间系到定位系的位姿 double position_repeatability_m = 4; // 位置重复性 double yaw_repeatability_rad = 5; // 航向重复性 @@ -334,4 +372,4 @@ message ExternalLocalizationJobResult { ExternalLocalizationValidationSummary validation_summary = 9; // 真值参考验证摘要 ExternalLocalizationCalibrationResult result = 10; // 结果内容 repeated .agv.calibration.common.FileReference artifacts = 11; // 关联产物 -} \ No newline at end of file +} diff --git a/agv_calib_brain/src/win_ubuntu_bridge/calibration_external_localization_interfaces/srv/CancelExternalLocalizationJob.srv b/agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_external_localization_interfaces/srv/CancelExternalLocalizationJob.srv similarity index 100% rename from agv_calib_brain/src/win_ubuntu_bridge/calibration_external_localization_interfaces/srv/CancelExternalLocalizationJob.srv rename to agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_external_localization_interfaces/srv/CancelExternalLocalizationJob.srv diff --git a/agv_calib_brain/src/win_ubuntu_bridge/calibration_external_localization_interfaces/srv/CommitExternalLocalizationResult.srv b/agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_external_localization_interfaces/srv/CommitExternalLocalizationResult.srv similarity index 100% rename from agv_calib_brain/src/win_ubuntu_bridge/calibration_external_localization_interfaces/srv/CommitExternalLocalizationResult.srv rename to agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_external_localization_interfaces/srv/CommitExternalLocalizationResult.srv diff --git a/agv_calib_brain/src/win_ubuntu_bridge/calibration_external_localization_interfaces/srv/EmergencyStop.srv b/agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_external_localization_interfaces/srv/EmergencyStop.srv similarity index 100% rename from agv_calib_brain/src/win_ubuntu_bridge/calibration_external_localization_interfaces/srv/EmergencyStop.srv rename to agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_external_localization_interfaces/srv/EmergencyStop.srv diff --git a/agv_calib_brain/src/win_ubuntu_bridge/calibration_external_localization_interfaces/srv/GetAppliedExternalLocalizationResult.srv b/agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_external_localization_interfaces/srv/GetAppliedExternalLocalizationResult.srv similarity index 100% rename from agv_calib_brain/src/win_ubuntu_bridge/calibration_external_localization_interfaces/srv/GetAppliedExternalLocalizationResult.srv rename to agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_external_localization_interfaces/srv/GetAppliedExternalLocalizationResult.srv diff --git a/agv_calib_brain/src/win_ubuntu_bridge/calibration_external_localization_interfaces/srv/GetExternalLocalizationCapability.srv b/agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_external_localization_interfaces/srv/GetExternalLocalizationCapability.srv similarity index 100% rename from agv_calib_brain/src/win_ubuntu_bridge/calibration_external_localization_interfaces/srv/GetExternalLocalizationCapability.srv rename to agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_external_localization_interfaces/srv/GetExternalLocalizationCapability.srv diff --git a/agv_calib_brain/src/win_ubuntu_bridge/calibration_external_localization_interfaces/srv/GetExternalLocalizationJobResult.srv b/agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_external_localization_interfaces/srv/GetExternalLocalizationJobResult.srv similarity index 100% rename from agv_calib_brain/src/win_ubuntu_bridge/calibration_external_localization_interfaces/srv/GetExternalLocalizationJobResult.srv rename to agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_external_localization_interfaces/srv/GetExternalLocalizationJobResult.srv diff --git a/agv_calib_brain/src/win_ubuntu_bridge/calibration_external_localization_interfaces/srv/GetExternalLocalizationJobStatus.srv b/agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_external_localization_interfaces/srv/GetExternalLocalizationJobStatus.srv similarity index 100% rename from agv_calib_brain/src/win_ubuntu_bridge/calibration_external_localization_interfaces/srv/GetExternalLocalizationJobStatus.srv rename to agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_external_localization_interfaces/srv/GetExternalLocalizationJobStatus.srv diff --git a/agv_calib_brain/src/win_ubuntu_bridge/calibration_external_localization_interfaces/srv/GetExternalLocalizationReadiness.srv b/agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_external_localization_interfaces/srv/GetExternalLocalizationReadiness.srv similarity index 100% rename from agv_calib_brain/src/win_ubuntu_bridge/calibration_external_localization_interfaces/srv/GetExternalLocalizationReadiness.srv rename to agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_external_localization_interfaces/srv/GetExternalLocalizationReadiness.srv diff --git a/agv_calib_brain/src/win_ubuntu_bridge/calibration_external_localization_interfaces/srv/Heartbeat.srv b/agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_external_localization_interfaces/srv/Heartbeat.srv similarity index 100% rename from agv_calib_brain/src/win_ubuntu_bridge/calibration_external_localization_interfaces/srv/Heartbeat.srv rename to agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_external_localization_interfaces/srv/Heartbeat.srv diff --git a/agv_calib_brain/src/win_ubuntu_bridge/calibration_external_localization_interfaces/srv/SetExternalLocalizationWorkMode.srv b/agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_external_localization_interfaces/srv/SetExternalLocalizationWorkMode.srv similarity index 100% rename from agv_calib_brain/src/win_ubuntu_bridge/calibration_external_localization_interfaces/srv/SetExternalLocalizationWorkMode.srv rename to agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_external_localization_interfaces/srv/SetExternalLocalizationWorkMode.srv diff --git a/agv_calib_brain/src/win_ubuntu_bridge/calibration_external_localization_interfaces/srv/StartExternalLocalizationTask.srv b/agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_external_localization_interfaces/srv/StartExternalLocalizationTask.srv similarity index 100% rename from agv_calib_brain/src/win_ubuntu_bridge/calibration_external_localization_interfaces/srv/StartExternalLocalizationTask.srv rename to agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_external_localization_interfaces/srv/StartExternalLocalizationTask.srv diff --git a/agv_calib_brain/src/win_ubuntu_bridge/calibration_external_localization_interfaces/srv/StreamExternalLocalizationTelemetry.srv b/agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_external_localization_interfaces/srv/StreamExternalLocalizationTelemetry.srv similarity index 100% rename from agv_calib_brain/src/win_ubuntu_bridge/calibration_external_localization_interfaces/srv/StreamExternalLocalizationTelemetry.srv rename to agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_external_localization_interfaces/srv/StreamExternalLocalizationTelemetry.srv diff --git a/agv_calib_brain/src/win_ubuntu_bridge/calibration_sensor_interfaces/CMakeLists.txt b/agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_sensor_interfaces/CMakeLists.txt similarity index 100% rename from agv_calib_brain/src/win_ubuntu_bridge/calibration_sensor_interfaces/CMakeLists.txt rename to agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_sensor_interfaces/CMakeLists.txt diff --git a/agv_calib_brain/src/win_ubuntu_bridge/calibration_sensor_interfaces/FILE_TREE.txt b/agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_sensor_interfaces/FILE_TREE.txt similarity index 100% rename from agv_calib_brain/src/win_ubuntu_bridge/calibration_sensor_interfaces/FILE_TREE.txt rename to agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_sensor_interfaces/FILE_TREE.txt diff --git a/agv_calib_brain/src/win_ubuntu_bridge/calibration_sensor_interfaces/action/ExecuteSensorCalibrationTask.action b/agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_sensor_interfaces/action/ExecuteSensorCalibrationTask.action similarity index 100% rename from agv_calib_brain/src/win_ubuntu_bridge/calibration_sensor_interfaces/action/ExecuteSensorCalibrationTask.action rename to agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_sensor_interfaces/action/ExecuteSensorCalibrationTask.action diff --git a/agv_calib_brain/src/win_ubuntu_bridge/calibration_sensor_interfaces/msg/AppliedSensorCalibrationParametersResponse.msg b/agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_sensor_interfaces/msg/AppliedSensorCalibrationParametersResponse.msg similarity index 100% rename from agv_calib_brain/src/win_ubuntu_bridge/calibration_sensor_interfaces/msg/AppliedSensorCalibrationParametersResponse.msg rename to agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_sensor_interfaces/msg/AppliedSensorCalibrationParametersResponse.msg diff --git a/agv_calib_brain/src/win_ubuntu_bridge/calibration_sensor_interfaces/msg/ArtifactType.msg b/agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_sensor_interfaces/msg/ArtifactType.msg similarity index 100% rename from agv_calib_brain/src/win_ubuntu_bridge/calibration_sensor_interfaces/msg/ArtifactType.msg rename to agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_sensor_interfaces/msg/ArtifactType.msg diff --git a/agv_calib_brain/src/win_ubuntu_bridge/calibration_sensor_interfaces/msg/CalibrationArtifact.msg b/agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_sensor_interfaces/msg/CalibrationArtifact.msg similarity index 100% rename from agv_calib_brain/src/win_ubuntu_bridge/calibration_sensor_interfaces/msg/CalibrationArtifact.msg rename to agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_sensor_interfaces/msg/CalibrationArtifact.msg diff --git a/agv_calib_brain/src/win_ubuntu_bridge/calibration_sensor_interfaces/msg/CalibrationReferenceTarget.msg b/agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_sensor_interfaces/msg/CalibrationReferenceTarget.msg similarity index 100% rename from agv_calib_brain/src/win_ubuntu_bridge/calibration_sensor_interfaces/msg/CalibrationReferenceTarget.msg rename to agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_sensor_interfaces/msg/CalibrationReferenceTarget.msg diff --git a/agv_calib_brain/src/win_ubuntu_bridge/calibration_sensor_interfaces/msg/CameraIntrinsicCalibrationTask.msg b/agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_sensor_interfaces/msg/CameraIntrinsicCalibrationTask.msg similarity index 100% rename from agv_calib_brain/src/win_ubuntu_bridge/calibration_sensor_interfaces/msg/CameraIntrinsicCalibrationTask.msg rename to agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_sensor_interfaces/msg/CameraIntrinsicCalibrationTask.msg diff --git a/agv_calib_brain/src/win_ubuntu_bridge/calibration_sensor_interfaces/msg/CameraIntrinsics.msg b/agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_sensor_interfaces/msg/CameraIntrinsics.msg similarity index 100% rename from agv_calib_brain/src/win_ubuntu_bridge/calibration_sensor_interfaces/msg/CameraIntrinsics.msg rename to agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_sensor_interfaces/msg/CameraIntrinsics.msg diff --git a/agv_calib_brain/src/win_ubuntu_bridge/calibration_sensor_interfaces/msg/CameraModelType.msg b/agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_sensor_interfaces/msg/CameraModelType.msg similarity index 100% rename from agv_calib_brain/src/win_ubuntu_bridge/calibration_sensor_interfaces/msg/CameraModelType.msg rename to agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_sensor_interfaces/msg/CameraModelType.msg diff --git a/agv_calib_brain/src/win_ubuntu_bridge/calibration_sensor_interfaces/msg/CaptureIntentType.msg b/agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_sensor_interfaces/msg/CaptureIntentType.msg similarity index 100% rename from agv_calib_brain/src/win_ubuntu_bridge/calibration_sensor_interfaces/msg/CaptureIntentType.msg rename to agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_sensor_interfaces/msg/CaptureIntentType.msg diff --git a/agv_calib_brain/src/win_ubuntu_bridge/calibration_sensor_interfaces/msg/CommitSensorCalibrationParametersRequest.msg b/agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_sensor_interfaces/msg/CommitSensorCalibrationParametersRequest.msg similarity index 100% rename from agv_calib_brain/src/win_ubuntu_bridge/calibration_sensor_interfaces/msg/CommitSensorCalibrationParametersRequest.msg rename to agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_sensor_interfaces/msg/CommitSensorCalibrationParametersRequest.msg diff --git a/agv_calib_brain/src/win_ubuntu_bridge/calibration_sensor_interfaces/msg/GetAppliedSensorCalibrationParametersRequest.msg b/agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_sensor_interfaces/msg/GetAppliedSensorCalibrationParametersRequest.msg similarity index 100% rename from agv_calib_brain/src/win_ubuntu_bridge/calibration_sensor_interfaces/msg/GetAppliedSensorCalibrationParametersRequest.msg rename to agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_sensor_interfaces/msg/GetAppliedSensorCalibrationParametersRequest.msg diff --git a/agv_calib_brain/src/win_ubuntu_bridge/calibration_sensor_interfaces/msg/HandEyeCalibrationMode.msg b/agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_sensor_interfaces/msg/HandEyeCalibrationMode.msg similarity index 100% rename from agv_calib_brain/src/win_ubuntu_bridge/calibration_sensor_interfaces/msg/HandEyeCalibrationMode.msg rename to agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_sensor_interfaces/msg/HandEyeCalibrationMode.msg diff --git a/agv_calib_brain/src/win_ubuntu_bridge/calibration_sensor_interfaces/msg/HandEyeCalibrationTask.msg b/agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_sensor_interfaces/msg/HandEyeCalibrationTask.msg similarity index 100% rename from agv_calib_brain/src/win_ubuntu_bridge/calibration_sensor_interfaces/msg/HandEyeCalibrationTask.msg rename to agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_sensor_interfaces/msg/HandEyeCalibrationTask.msg diff --git a/agv_calib_brain/src/win_ubuntu_bridge/calibration_sensor_interfaces/msg/IMUIntrinsicCalibrationTask.msg b/agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_sensor_interfaces/msg/IMUIntrinsicCalibrationTask.msg similarity index 100% rename from agv_calib_brain/src/win_ubuntu_bridge/calibration_sensor_interfaces/msg/IMUIntrinsicCalibrationTask.msg rename to agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_sensor_interfaces/msg/IMUIntrinsicCalibrationTask.msg diff --git a/agv_calib_brain/src/win_ubuntu_bridge/calibration_sensor_interfaces/msg/IMUIntrinsics.msg b/agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_sensor_interfaces/msg/IMUIntrinsics.msg similarity index 100% rename from agv_calib_brain/src/win_ubuntu_bridge/calibration_sensor_interfaces/msg/IMUIntrinsics.msg rename to agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_sensor_interfaces/msg/IMUIntrinsics.msg diff --git a/agv_calib_brain/src/win_ubuntu_bridge/calibration_sensor_interfaces/msg/SensorCalibrationCapabilityRequest.msg b/agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_sensor_interfaces/msg/SensorCalibrationCapabilityRequest.msg similarity index 100% rename from agv_calib_brain/src/win_ubuntu_bridge/calibration_sensor_interfaces/msg/SensorCalibrationCapabilityRequest.msg rename to agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_sensor_interfaces/msg/SensorCalibrationCapabilityRequest.msg diff --git a/agv_calib_brain/src/win_ubuntu_bridge/calibration_sensor_interfaces/msg/SensorCalibrationCapabilityResponse.msg b/agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_sensor_interfaces/msg/SensorCalibrationCapabilityResponse.msg similarity index 100% rename from agv_calib_brain/src/win_ubuntu_bridge/calibration_sensor_interfaces/msg/SensorCalibrationCapabilityResponse.msg rename to agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_sensor_interfaces/msg/SensorCalibrationCapabilityResponse.msg diff --git a/agv_calib_brain/src/win_ubuntu_bridge/calibration_sensor_interfaces/msg/SensorCalibrationJobResult.msg b/agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_sensor_interfaces/msg/SensorCalibrationJobResult.msg similarity index 100% rename from agv_calib_brain/src/win_ubuntu_bridge/calibration_sensor_interfaces/msg/SensorCalibrationJobResult.msg rename to agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_sensor_interfaces/msg/SensorCalibrationJobResult.msg diff --git a/agv_calib_brain/src/win_ubuntu_bridge/calibration_sensor_interfaces/msg/SensorCalibrationParameter.msg b/agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_sensor_interfaces/msg/SensorCalibrationParameter.msg similarity index 100% rename from agv_calib_brain/src/win_ubuntu_bridge/calibration_sensor_interfaces/msg/SensorCalibrationParameter.msg rename to agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_sensor_interfaces/msg/SensorCalibrationParameter.msg diff --git a/agv_calib_brain/src/win_ubuntu_bridge/calibration_sensor_interfaces/msg/SensorCalibrationParameterSet.msg b/agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_sensor_interfaces/msg/SensorCalibrationParameterSet.msg similarity index 100% rename from agv_calib_brain/src/win_ubuntu_bridge/calibration_sensor_interfaces/msg/SensorCalibrationParameterSet.msg rename to agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_sensor_interfaces/msg/SensorCalibrationParameterSet.msg diff --git a/agv_calib_brain/src/win_ubuntu_bridge/calibration_sensor_interfaces/msg/SensorCalibrationTaskPurpose.msg b/agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_sensor_interfaces/msg/SensorCalibrationTaskPurpose.msg similarity index 100% rename from agv_calib_brain/src/win_ubuntu_bridge/calibration_sensor_interfaces/msg/SensorCalibrationTaskPurpose.msg rename to agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_sensor_interfaces/msg/SensorCalibrationTaskPurpose.msg diff --git a/agv_calib_brain/src/win_ubuntu_bridge/calibration_sensor_interfaces/msg/SensorCalibrationTaskRequest.msg b/agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_sensor_interfaces/msg/SensorCalibrationTaskRequest.msg similarity index 100% rename from agv_calib_brain/src/win_ubuntu_bridge/calibration_sensor_interfaces/msg/SensorCalibrationTaskRequest.msg rename to agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_sensor_interfaces/msg/SensorCalibrationTaskRequest.msg diff --git a/agv_calib_brain/src/win_ubuntu_bridge/calibration_sensor_interfaces/msg/SensorCalibrationTaskSubtype.msg b/agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_sensor_interfaces/msg/SensorCalibrationTaskSubtype.msg similarity index 100% rename from agv_calib_brain/src/win_ubuntu_bridge/calibration_sensor_interfaces/msg/SensorCalibrationTaskSubtype.msg rename to agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_sensor_interfaces/msg/SensorCalibrationTaskSubtype.msg diff --git a/agv_calib_brain/src/win_ubuntu_bridge/calibration_sensor_interfaces/msg/SensorCalibrationTaskType.msg b/agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_sensor_interfaces/msg/SensorCalibrationTaskType.msg similarity index 100% rename from agv_calib_brain/src/win_ubuntu_bridge/calibration_sensor_interfaces/msg/SensorCalibrationTaskType.msg rename to agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_sensor_interfaces/msg/SensorCalibrationTaskType.msg diff --git a/agv_calib_brain/src/win_ubuntu_bridge/calibration_sensor_interfaces/msg/SensorCalibrationTelemetry.msg b/agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_sensor_interfaces/msg/SensorCalibrationTelemetry.msg similarity index 100% rename from agv_calib_brain/src/win_ubuntu_bridge/calibration_sensor_interfaces/msg/SensorCalibrationTelemetry.msg rename to agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_sensor_interfaces/msg/SensorCalibrationTelemetry.msg diff --git a/agv_calib_brain/src/win_ubuntu_bridge/calibration_sensor_interfaces/msg/SensorCalibrationWorkMode.msg b/agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_sensor_interfaces/msg/SensorCalibrationWorkMode.msg similarity index 100% rename from agv_calib_brain/src/win_ubuntu_bridge/calibration_sensor_interfaces/msg/SensorCalibrationWorkMode.msg rename to agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_sensor_interfaces/msg/SensorCalibrationWorkMode.msg diff --git a/agv_calib_brain/src/win_ubuntu_bridge/calibration_sensor_interfaces/msg/SensorCalibrationWorkModeRequest.msg b/agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_sensor_interfaces/msg/SensorCalibrationWorkModeRequest.msg similarity index 100% rename from agv_calib_brain/src/win_ubuntu_bridge/calibration_sensor_interfaces/msg/SensorCalibrationWorkModeRequest.msg rename to agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_sensor_interfaces/msg/SensorCalibrationWorkModeRequest.msg diff --git a/agv_calib_brain/src/win_ubuntu_bridge/calibration_sensor_interfaces/msg/SensorExtrinsics.msg b/agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_sensor_interfaces/msg/SensorExtrinsics.msg similarity index 100% rename from agv_calib_brain/src/win_ubuntu_bridge/calibration_sensor_interfaces/msg/SensorExtrinsics.msg rename to agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_sensor_interfaces/msg/SensorExtrinsics.msg diff --git a/agv_calib_brain/src/win_ubuntu_bridge/calibration_sensor_interfaces/msg/SensorReadinessResponse.msg b/agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_sensor_interfaces/msg/SensorReadinessResponse.msg similarity index 100% rename from agv_calib_brain/src/win_ubuntu_bridge/calibration_sensor_interfaces/msg/SensorReadinessResponse.msg rename to agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_sensor_interfaces/msg/SensorReadinessResponse.msg diff --git a/agv_calib_brain/src/win_ubuntu_bridge/calibration_sensor_interfaces/msg/SensorToBaseExtrinsicCalibrationTask.msg b/agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_sensor_interfaces/msg/SensorToBaseExtrinsicCalibrationTask.msg similarity index 100% rename from agv_calib_brain/src/win_ubuntu_bridge/calibration_sensor_interfaces/msg/SensorToBaseExtrinsicCalibrationTask.msg rename to agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_sensor_interfaces/msg/SensorToBaseExtrinsicCalibrationTask.msg diff --git a/agv_calib_brain/src/win_ubuntu_bridge/calibration_sensor_interfaces/msg/SensorValidationSummary.msg b/agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_sensor_interfaces/msg/SensorValidationSummary.msg similarity index 100% rename from agv_calib_brain/src/win_ubuntu_bridge/calibration_sensor_interfaces/msg/SensorValidationSummary.msg rename to agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_sensor_interfaces/msg/SensorValidationSummary.msg diff --git a/agv_calib_brain/src/win_ubuntu_bridge/calibration_sensor_interfaces/msg/StreamSensorCalibrationTelemetryRequest.msg b/agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_sensor_interfaces/msg/StreamSensorCalibrationTelemetryRequest.msg similarity index 100% rename from agv_calib_brain/src/win_ubuntu_bridge/calibration_sensor_interfaces/msg/StreamSensorCalibrationTelemetryRequest.msg rename to agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_sensor_interfaces/msg/StreamSensorCalibrationTelemetryRequest.msg diff --git a/agv_calib_brain/src/win_ubuntu_bridge/calibration_sensor_interfaces/package.xml b/agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_sensor_interfaces/package.xml similarity index 100% rename from agv_calib_brain/src/win_ubuntu_bridge/calibration_sensor_interfaces/package.xml rename to agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_sensor_interfaces/package.xml diff --git a/agv_calib_brain/src/win_ubuntu_bridge/win_ubuntu_bridge/proto/sensor_calibration.proto b/agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_sensor_interfaces/proto/sensor_calibration.proto similarity index 70% rename from agv_calib_brain/src/win_ubuntu_bridge/win_ubuntu_bridge/proto/sensor_calibration.proto rename to agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_sensor_interfaces/proto/sensor_calibration.proto index 38d0096..a121b55 100644 --- a/agv_calib_brain/src/win_ubuntu_bridge/win_ubuntu_bridge/proto/sensor_calibration.proto +++ b/agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_sensor_interfaces/proto/sensor_calibration.proto @@ -11,7 +11,7 @@ import "vehicle_profile.proto"; // Ubuntu 车间电脑(Linux) -> Windows 车端代理 / 采集服务 // 说明: // 1) Linux 负责图像 / 点云 / 激光扫描 / IMU 数据分析与参数求解 -// 2) 车端 / 采集服务只负责组织采集、回传观测状态、写入结果 +// 2) 车端 / 采集服务负责订阅车端传感器、组织采集、回传原始数据和观测状态、写入结果 // 3) 支持相机内参、IMU 内参、传感器外参、手眼标定 // 4) 支持相机“只做内参 / 只做外参 / 内外参都做” // ========================================================= @@ -58,6 +58,10 @@ service AgvCalibSensorService { rpc StreamSensorCalibrationTelemetry(StreamSensorCalibrationTelemetryRequest) returns (stream SensorCalibrationTelemetry); + // 打开车端传感器原始数据流;用于 WiFi6 把车载相机、雷达、IMU 数据传给车间电脑 + rpc StreamVehicleSensorData(StreamVehicleSensorDataRequest) + returns (stream VehicleSensorFrame); + // 写入传感器标定参数 rpc CommitSensorCalibrationParameters(CommitSensorCalibrationParametersRequest) returns (.agv.calibration.common.StandardResponse); @@ -71,6 +75,22 @@ service AgvCalibSensorService { returns (.agv.calibration.common.StandardResponse); } +// ========================================================= +// 服务:车间电脑传感器数据接收服务 +// 运行位置:Ubuntu 车间电脑 +// 调用方:Windows 车端代理 / 仿真车端代理 +// 说明: +// 1) 当现场网络策略采用车端主动上报时使用 +// 2) 与 StreamVehicleSensorData 使用同一帧结构 +// ========================================================= +service AgvCalibWorkshopSensorIngestService { + rpc Heartbeat(.agv.calibration.common.HeartbeatRequest) + returns (.agv.calibration.common.HeartbeatResponse); + + rpc PushVehicleSensorFrame(VehicleSensorFrame) + returns (.agv.calibration.common.StandardResponse); +} + // ========================================================= // 传感器标定工作模式 // 作用:切换采集服务到不同模式 @@ -109,6 +129,10 @@ message SensorReadinessResponse { repeated string ready_sensor_ids = 10; // 已就绪的传感器 ID repeated .agv.calibration.common.ReadinessIssue issues = 11; // 不满足项 int64 checked_timestamp_us = 12; // 检查时间 + + bool sensor_data_stream_ready = 13; // 车端原始传感器数据流是否就绪 + repeated string streaming_sensor_ids = 14; // 当前可被转发到车间电脑的传感器 ID + string sensor_data_transport = 15; // 传输方式,例如 wifi6 / wifi6_sim_tcp } // ========================================================= @@ -137,6 +161,9 @@ message SensorCalibrationCapabilityResponse { bool supports_2d_lidar_extrinsic = 9; // 是否支持 2D 激光雷达外参 bool supports_camera_only_intrinsic = 10; // 是否支持仅做相机内参 bool supports_camera_only_extrinsic = 11; // 是否支持仅做相机外参 + bool supports_vehicle_sensor_data_stream = 12; // 是否支持车端原始传感器数据流 + bool supports_sensor_frame_chunking = 13; // 是否支持大帧分片传输 + bool supports_compressed_image_stream = 14; // 是否支持压缩图像流 } // ========================================================= @@ -238,8 +265,8 @@ enum SensorCalibrationTaskSubtype { IMU_EXTRINSIC = 6; LIDAR_2D_EXTRINSIC = 7; LIDAR_3D_EXTRINSIC = 8; - EYE_IN_HAND = 9; - EYE_TO_HAND = 10; + HAND_EYE_IN_HAND = 9; + HAND_EYE_TO_HAND = 10; } // ========================================================= @@ -300,6 +327,161 @@ message SensorCalibrationTelemetry { CaptureIntentType capture_intent = 9; // 当前采集意图 } +// ========================================================= +// 车端传感器原始数据流请求 +// 作用:车间电脑指定要接收哪些车端传感器数据 +// ========================================================= +message StreamVehicleSensorDataRequest { + .agv.calibration.common.RequestHeader header = 1; // 请求头 + repeated string sensor_ids = 2; // 要接收的传感器 ID;为空表示由任务决定 + repeated .agv.calibration.vehicle.profile.SensorType sensor_types = 3; // 要接收的传感器类型 + uint32 expected_hz = 4; // 期望上报频率,0 表示车端默认 + uint32 max_frame_payload_bytes = 5; // 单个分片最大 payload,0 表示车端默认 + bool allow_compressed = 6; // 是否允许压缩图像 / 点云 + bool include_camera_info = 7; // 是否随图像携带相机内参初值 + CaptureIntentType capture_intent = 8; // 数据采集意图 + string active_job_id = 9; // 关联的标定任务 ID +} + +// ========================================================= +// 传感器原始帧负载类型 +// ========================================================= +enum SensorFramePayloadType { + SENSOR_FRAME_PAYLOAD_TYPE_UNSPECIFIED = 0; + IMAGE_FRAME = 1; // 相机图像 + POINT_CLOUD_FRAME = 2; // 3D 激光雷达点云 + LASER_SCAN_FRAME = 3; // 2D 激光雷达扫描 + IMU_FRAME = 4; // IMU 数据 + CAMERA_INFO_FRAME = 5; // 相机内参 / camera_info + RAW_BYTES_FRAME = 6; // 预留给厂商私有二进制格式 +} + +// ========================================================= +// 传感器原始帧公共头 +// ========================================================= +message VehicleSensorFrameHeader { + string sensor_id = 1; // 传感器 ID,例如 demo_front_camera + .agv.calibration.vehicle.profile.SensorType sensor_type = 2; // 传感器类型 + string frame_id = 3; // ROS / 车端坐标系 frame_id + int64 hardware_timestamp_us = 4; // 硬件时间戳 + uint64 sequence_id = 5; // 单传感器递增帧号 + string active_job_id = 6; // 关联任务 ID + CaptureIntentType capture_intent = 7; // 采集意图 +} + +// ========================================================= +// 图像帧 +// 对齐 ROS sensor_msgs/Image 的核心字段 +// ========================================================= +message VehicleImageFrame { + uint32 width = 1; // 图像宽度 + uint32 height = 2; // 图像高度 + string encoding = 3; // 例如 rgb8 / bgr8 / mono8 / jpeg / png + bool is_bigendian = 4; // 字节序 + uint32 step = 5; // 每行字节数 + bytes data = 6; // 图像数据或分片数据 + string compression = 7; // none / jpeg / png + CameraIntrinsics camera_intrinsics_hint = 8; // 可选内参初值或当前参数 +} + +// ========================================================= +// PointCloud2 字段描述 +// ========================================================= +enum PointCloudFieldDataType { + POINT_CLOUD_FIELD_DATA_TYPE_UNSPECIFIED = 0; + POINT_CLOUD_FIELD_INT8 = 1; + POINT_CLOUD_FIELD_UINT8 = 2; + POINT_CLOUD_FIELD_INT16 = 3; + POINT_CLOUD_FIELD_UINT16 = 4; + POINT_CLOUD_FIELD_INT32 = 5; + POINT_CLOUD_FIELD_UINT32 = 6; + POINT_CLOUD_FIELD_FLOAT32 = 7; + POINT_CLOUD_FIELD_FLOAT64 = 8; +} + +message PointCloudField { + string name = 1; // 字段名,例如 x / y / z / intensity + uint32 offset = 2; // 字节偏移 + PointCloudFieldDataType datatype = 3; // 数据类型 + uint32 count = 4; // 元素数量 +} + +// ========================================================= +// 3D 点云帧 +// 对齐 ROS sensor_msgs/PointCloud2 的核心字段 +// ========================================================= +message VehiclePointCloudFrame { + uint32 height = 1; + uint32 width = 2; + repeated PointCloudField fields = 3; + bool is_bigendian = 4; + uint32 point_step = 5; + uint32 row_step = 6; + bytes data = 7; // 点云数据或分片数据 + bool is_dense = 8; + string compression = 9; // none / zstd / lz4 / vendor +} + +// ========================================================= +// 2D 激光雷达扫描帧 +// 对齐 ROS sensor_msgs/LaserScan 的核心字段 +// ========================================================= +message VehicleLaserScanFrame { + double angle_min_rad = 1; + double angle_max_rad = 2; + double angle_increment_rad = 3; + double time_increment_sec = 4; + double scan_time_sec = 5; + double range_min_m = 6; + double range_max_m = 7; + repeated float ranges_m = 8; + repeated float intensities = 9; +} + +// ========================================================= +// IMU 帧 +// 对齐 ROS sensor_msgs/Imu 的核心字段 +// ========================================================= +message VehicleImuFrame { + double orientation_x = 1; + double orientation_y = 2; + double orientation_z = 3; + double orientation_w = 4; + repeated double orientation_covariance = 5; + .agv.calibration.common.Vector3D angular_velocity = 6; + repeated double angular_velocity_covariance = 7; + .agv.calibration.common.Vector3D linear_acceleration = 8; + repeated double linear_acceleration_covariance = 9; +} + +// ========================================================= +// 车端传感器原始帧 +// 作用:通过 WiFi6 在车端和车间电脑之间传输相机 / 雷达 / IMU 数据 +// 说明: +// 1) fragment_index / total_fragments 用于大图像或点云分片 +// 2) total_fragments 为 0 或 1 表示本消息携带完整帧 +// 3) oneof 中的数据字段可携带完整数据,也可携带当前分片数据 +// ========================================================= +message VehicleSensorFrame { + VehicleSensorFrameHeader header = 1; // 公共帧头 + SensorFramePayloadType payload_type = 2; // 负载类型 + string stream_id = 3; // 数据流 ID + uint32 fragment_index = 4; // 当前分片序号,从 0 开始 + uint32 total_fragments = 5; // 总分片数;0 或 1 表示完整帧 + bool is_final_fragment = 6; // 当前是否最后一个分片 + uint32 uncompressed_size_bytes = 7; // 解压后总大小,用于校验 + string payload_digest = 8; // 完整帧摘要,例如 sha256:... + + oneof payload { + VehicleImageFrame image = 20; + VehiclePointCloudFrame point_cloud = 21; + VehicleLaserScanFrame laser_scan = 22; + VehicleImuFrame imu = 23; + CameraIntrinsics camera_info = 24; + bytes raw_payload = 25; + } +} + // ========================================================= // 相机模型类型 // 作用:表达相机内参使用的模型 @@ -335,8 +517,8 @@ message CameraIntrinsics { // 3) 采用强约束三维向量,不再使用 repeated double // ========================================================= message IMUIntrinsics { - .agv.calibration.common.Vector3d accel_bias = 1; // 加速度计偏置 - .agv.calibration.common.Vector3d gyro_bias = 2; // 陀螺仪偏置 + .agv.calibration.common.Vector3D accel_bias = 1; // 加速度计偏置 + .agv.calibration.common.Vector3D gyro_bias = 2; // 陀螺仪偏置 } // ========================================================= @@ -347,7 +529,7 @@ message IMUIntrinsics { message SensorExtrinsics { string parent_frame_id = 1; // 父坐标系,一般为 base_link string child_frame_id = 2; // 子坐标系,一般为 sensor frame - .agv.calibration.common.Pose3dEuler parent_to_child = 3; // 父到子的位姿 + .agv.calibration.common.Pose3D parent_to_child = 3; // 父到子的位姿 } // ========================================================= @@ -461,4 +643,4 @@ message SensorCalibrationJobResult { SensorValidationSummary validation_summary = 8; // 验证摘要 SensorCalibrationParameterSet estimated_params = 9; // 本轮估计参数 repeated CalibrationArtifact artifacts = 10; // 关联产物 -} \ No newline at end of file +} diff --git a/agv_calib_brain/src/win_ubuntu_bridge/calibration_sensor_interfaces/srv/CancelSensorCalibrationJob.srv b/agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_sensor_interfaces/srv/CancelSensorCalibrationJob.srv similarity index 100% rename from agv_calib_brain/src/win_ubuntu_bridge/calibration_sensor_interfaces/srv/CancelSensorCalibrationJob.srv rename to agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_sensor_interfaces/srv/CancelSensorCalibrationJob.srv diff --git a/agv_calib_brain/src/win_ubuntu_bridge/calibration_sensor_interfaces/srv/CommitSensorCalibrationParameters.srv b/agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_sensor_interfaces/srv/CommitSensorCalibrationParameters.srv similarity index 100% rename from agv_calib_brain/src/win_ubuntu_bridge/calibration_sensor_interfaces/srv/CommitSensorCalibrationParameters.srv rename to agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_sensor_interfaces/srv/CommitSensorCalibrationParameters.srv diff --git a/agv_calib_brain/src/win_ubuntu_bridge/calibration_sensor_interfaces/srv/EmergencyStop.srv b/agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_sensor_interfaces/srv/EmergencyStop.srv similarity index 100% rename from agv_calib_brain/src/win_ubuntu_bridge/calibration_sensor_interfaces/srv/EmergencyStop.srv rename to agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_sensor_interfaces/srv/EmergencyStop.srv diff --git a/agv_calib_brain/src/win_ubuntu_bridge/calibration_sensor_interfaces/srv/GetAppliedSensorCalibrationParameters.srv b/agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_sensor_interfaces/srv/GetAppliedSensorCalibrationParameters.srv similarity index 100% rename from agv_calib_brain/src/win_ubuntu_bridge/calibration_sensor_interfaces/srv/GetAppliedSensorCalibrationParameters.srv rename to agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_sensor_interfaces/srv/GetAppliedSensorCalibrationParameters.srv diff --git a/agv_calib_brain/src/win_ubuntu_bridge/calibration_sensor_interfaces/srv/GetSensorCalibrationCapability.srv b/agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_sensor_interfaces/srv/GetSensorCalibrationCapability.srv similarity index 100% rename from agv_calib_brain/src/win_ubuntu_bridge/calibration_sensor_interfaces/srv/GetSensorCalibrationCapability.srv rename to agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_sensor_interfaces/srv/GetSensorCalibrationCapability.srv diff --git a/agv_calib_brain/src/win_ubuntu_bridge/calibration_sensor_interfaces/srv/GetSensorCalibrationJobResult.srv b/agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_sensor_interfaces/srv/GetSensorCalibrationJobResult.srv similarity index 100% rename from agv_calib_brain/src/win_ubuntu_bridge/calibration_sensor_interfaces/srv/GetSensorCalibrationJobResult.srv rename to agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_sensor_interfaces/srv/GetSensorCalibrationJobResult.srv diff --git a/agv_calib_brain/src/win_ubuntu_bridge/calibration_sensor_interfaces/srv/GetSensorCalibrationJobStatus.srv b/agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_sensor_interfaces/srv/GetSensorCalibrationJobStatus.srv similarity index 100% rename from agv_calib_brain/src/win_ubuntu_bridge/calibration_sensor_interfaces/srv/GetSensorCalibrationJobStatus.srv rename to agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_sensor_interfaces/srv/GetSensorCalibrationJobStatus.srv diff --git a/agv_calib_brain/src/win_ubuntu_bridge/calibration_sensor_interfaces/srv/GetSensorReadiness.srv b/agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_sensor_interfaces/srv/GetSensorReadiness.srv similarity index 100% rename from agv_calib_brain/src/win_ubuntu_bridge/calibration_sensor_interfaces/srv/GetSensorReadiness.srv rename to agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_sensor_interfaces/srv/GetSensorReadiness.srv diff --git a/agv_calib_brain/src/win_ubuntu_bridge/calibration_sensor_interfaces/srv/Heartbeat.srv b/agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_sensor_interfaces/srv/Heartbeat.srv similarity index 100% rename from agv_calib_brain/src/win_ubuntu_bridge/calibration_sensor_interfaces/srv/Heartbeat.srv rename to agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_sensor_interfaces/srv/Heartbeat.srv diff --git a/agv_calib_brain/src/win_ubuntu_bridge/calibration_sensor_interfaces/srv/SetSensorCalibrationWorkMode.srv b/agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_sensor_interfaces/srv/SetSensorCalibrationWorkMode.srv similarity index 100% rename from agv_calib_brain/src/win_ubuntu_bridge/calibration_sensor_interfaces/srv/SetSensorCalibrationWorkMode.srv rename to agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_sensor_interfaces/srv/SetSensorCalibrationWorkMode.srv diff --git a/agv_calib_brain/src/win_ubuntu_bridge/calibration_sensor_interfaces/srv/StartSensorCalibrationTask.srv b/agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_sensor_interfaces/srv/StartSensorCalibrationTask.srv similarity index 100% rename from agv_calib_brain/src/win_ubuntu_bridge/calibration_sensor_interfaces/srv/StartSensorCalibrationTask.srv rename to agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_sensor_interfaces/srv/StartSensorCalibrationTask.srv diff --git a/agv_calib_brain/src/win_ubuntu_bridge/calibration_sensor_interfaces/srv/StreamSensorCalibrationTelemetry.srv b/agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_sensor_interfaces/srv/StreamSensorCalibrationTelemetry.srv similarity index 100% rename from agv_calib_brain/src/win_ubuntu_bridge/calibration_sensor_interfaces/srv/StreamSensorCalibrationTelemetry.srv rename to agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_sensor_interfaces/srv/StreamSensorCalibrationTelemetry.srv diff --git a/agv_calib_brain/src/win_ubuntu_bridge/calibration_vehicle_profile_interfaces/CMakeLists.txt b/agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_vehicle_profile_interfaces/CMakeLists.txt similarity index 100% rename from agv_calib_brain/src/win_ubuntu_bridge/calibration_vehicle_profile_interfaces/CMakeLists.txt rename to agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_vehicle_profile_interfaces/CMakeLists.txt diff --git a/agv_calib_brain/src/win_ubuntu_bridge/calibration_vehicle_profile_interfaces/FILE_TREE.txt b/agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_vehicle_profile_interfaces/FILE_TREE.txt similarity index 100% rename from agv_calib_brain/src/win_ubuntu_bridge/calibration_vehicle_profile_interfaces/FILE_TREE.txt rename to agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_vehicle_profile_interfaces/FILE_TREE.txt diff --git a/agv_calib_brain/src/win_ubuntu_bridge/calibration_vehicle_profile_interfaces/msg/ApplicabilityIssue.msg b/agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_vehicle_profile_interfaces/msg/ApplicabilityIssue.msg similarity index 100% rename from agv_calib_brain/src/win_ubuntu_bridge/calibration_vehicle_profile_interfaces/msg/ApplicabilityIssue.msg rename to agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_vehicle_profile_interfaces/msg/ApplicabilityIssue.msg diff --git a/agv_calib_brain/src/win_ubuntu_bridge/calibration_vehicle_profile_interfaces/msg/CalibrationAbilityType.msg b/agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_vehicle_profile_interfaces/msg/CalibrationAbilityType.msg similarity index 100% rename from agv_calib_brain/src/win_ubuntu_bridge/calibration_vehicle_profile_interfaces/msg/CalibrationAbilityType.msg rename to agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_vehicle_profile_interfaces/msg/CalibrationAbilityType.msg diff --git a/agv_calib_brain/src/win_ubuntu_bridge/calibration_vehicle_profile_interfaces/msg/CalibrationCapability.msg b/agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_vehicle_profile_interfaces/msg/CalibrationCapability.msg similarity index 100% rename from agv_calib_brain/src/win_ubuntu_bridge/calibration_vehicle_profile_interfaces/msg/CalibrationCapability.msg rename to agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_vehicle_profile_interfaces/msg/CalibrationCapability.msg diff --git a/agv_calib_brain/src/win_ubuntu_bridge/calibration_vehicle_profile_interfaces/msg/CalibrationModuleType.msg b/agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_vehicle_profile_interfaces/msg/CalibrationModuleType.msg similarity index 100% rename from agv_calib_brain/src/win_ubuntu_bridge/calibration_vehicle_profile_interfaces/msg/CalibrationModuleType.msg rename to agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_vehicle_profile_interfaces/msg/CalibrationModuleType.msg diff --git a/agv_calib_brain/src/win_ubuntu_bridge/calibration_vehicle_profile_interfaces/msg/CameraMountType.msg b/agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_vehicle_profile_interfaces/msg/CameraMountType.msg similarity index 97% rename from agv_calib_brain/src/win_ubuntu_bridge/calibration_vehicle_profile_interfaces/msg/CameraMountType.msg rename to agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_vehicle_profile_interfaces/msg/CameraMountType.msg index fdf7125..c8cfd45 100644 --- a/agv_calib_brain/src/win_ubuntu_bridge/calibration_vehicle_profile_interfaces/msg/CameraMountType.msg +++ b/agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_vehicle_profile_interfaces/msg/CameraMountType.msg @@ -11,4 +11,4 @@ uint8 EYE_IN_HAND=4 # 眼在手上 uint8 EYE_TO_HAND=5 # 眼在手外 # 当前枚举取值 -uint8 value \ No newline at end of file +uint8 value diff --git a/agv_calib_brain/src/win_ubuntu_bridge/calibration_vehicle_profile_interfaces/msg/ChassisType.msg b/agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_vehicle_profile_interfaces/msg/ChassisType.msg similarity index 100% rename from agv_calib_brain/src/win_ubuntu_bridge/calibration_vehicle_profile_interfaces/msg/ChassisType.msg rename to agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_vehicle_profile_interfaces/msg/ChassisType.msg diff --git a/agv_calib_brain/src/win_ubuntu_bridge/calibration_vehicle_profile_interfaces/msg/ControlAxisType.msg b/agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_vehicle_profile_interfaces/msg/ControlAxisType.msg similarity index 100% rename from agv_calib_brain/src/win_ubuntu_bridge/calibration_vehicle_profile_interfaces/msg/ControlAxisType.msg rename to agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_vehicle_profile_interfaces/msg/ControlAxisType.msg diff --git a/agv_calib_brain/src/win_ubuntu_bridge/calibration_vehicle_profile_interfaces/msg/ControllerAlgorithmType.msg b/agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_vehicle_profile_interfaces/msg/ControllerAlgorithmType.msg similarity index 100% rename from agv_calib_brain/src/win_ubuntu_bridge/calibration_vehicle_profile_interfaces/msg/ControllerAlgorithmType.msg rename to agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_vehicle_profile_interfaces/msg/ControllerAlgorithmType.msg diff --git a/agv_calib_brain/src/win_ubuntu_bridge/calibration_vehicle_profile_interfaces/msg/ControllerProfile.msg b/agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_vehicle_profile_interfaces/msg/ControllerProfile.msg similarity index 100% rename from agv_calib_brain/src/win_ubuntu_bridge/calibration_vehicle_profile_interfaces/msg/ControllerProfile.msg rename to agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_vehicle_profile_interfaces/msg/ControllerProfile.msg diff --git a/agv_calib_brain/src/win_ubuntu_bridge/calibration_vehicle_profile_interfaces/msg/ControllerSelection.msg b/agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_vehicle_profile_interfaces/msg/ControllerSelection.msg similarity index 100% rename from agv_calib_brain/src/win_ubuntu_bridge/calibration_vehicle_profile_interfaces/msg/ControllerSelection.msg rename to agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_vehicle_profile_interfaces/msg/ControllerSelection.msg diff --git a/agv_calib_brain/src/win_ubuntu_bridge/calibration_vehicle_profile_interfaces/msg/EvaluateVehicleCalibrationApplicabilityRequest.msg b/agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_vehicle_profile_interfaces/msg/EvaluateVehicleCalibrationApplicabilityRequest.msg similarity index 100% rename from agv_calib_brain/src/win_ubuntu_bridge/calibration_vehicle_profile_interfaces/msg/EvaluateVehicleCalibrationApplicabilityRequest.msg rename to agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_vehicle_profile_interfaces/msg/EvaluateVehicleCalibrationApplicabilityRequest.msg diff --git a/agv_calib_brain/src/win_ubuntu_bridge/calibration_vehicle_profile_interfaces/msg/MechanicalArmProfile.msg b/agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_vehicle_profile_interfaces/msg/MechanicalArmProfile.msg similarity index 100% rename from agv_calib_brain/src/win_ubuntu_bridge/calibration_vehicle_profile_interfaces/msg/MechanicalArmProfile.msg rename to agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_vehicle_profile_interfaces/msg/MechanicalArmProfile.msg diff --git a/agv_calib_brain/src/win_ubuntu_bridge/calibration_vehicle_profile_interfaces/msg/RegisterOrUpdateVehicleProfileRequest.msg b/agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_vehicle_profile_interfaces/msg/RegisterOrUpdateVehicleProfileRequest.msg similarity index 100% rename from agv_calib_brain/src/win_ubuntu_bridge/calibration_vehicle_profile_interfaces/msg/RegisterOrUpdateVehicleProfileRequest.msg rename to agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_vehicle_profile_interfaces/msg/RegisterOrUpdateVehicleProfileRequest.msg diff --git a/agv_calib_brain/src/win_ubuntu_bridge/calibration_vehicle_profile_interfaces/msg/SensorProfile.msg b/agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_vehicle_profile_interfaces/msg/SensorProfile.msg similarity index 100% rename from agv_calib_brain/src/win_ubuntu_bridge/calibration_vehicle_profile_interfaces/msg/SensorProfile.msg rename to agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_vehicle_profile_interfaces/msg/SensorProfile.msg diff --git a/agv_calib_brain/src/win_ubuntu_bridge/calibration_vehicle_profile_interfaces/msg/SensorType.msg b/agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_vehicle_profile_interfaces/msg/SensorType.msg similarity index 97% rename from agv_calib_brain/src/win_ubuntu_bridge/calibration_vehicle_profile_interfaces/msg/SensorType.msg rename to agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_vehicle_profile_interfaces/msg/SensorType.msg index 22ad7d9..b8d08aa 100644 --- a/agv_calib_brain/src/win_ubuntu_bridge/calibration_vehicle_profile_interfaces/msg/SensorType.msg +++ b/agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_vehicle_profile_interfaces/msg/SensorType.msg @@ -12,4 +12,4 @@ uint8 LIDAR_2D=5 # 2D 激光雷达 uint8 IMU=6 # IMU # 当前枚举取值 -uint8 value \ No newline at end of file +uint8 value diff --git a/agv_calib_brain/src/win_ubuntu_bridge/calibration_vehicle_profile_interfaces/msg/VehicleBaseInfo.msg b/agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_vehicle_profile_interfaces/msg/VehicleBaseInfo.msg similarity index 100% rename from agv_calib_brain/src/win_ubuntu_bridge/calibration_vehicle_profile_interfaces/msg/VehicleBaseInfo.msg rename to agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_vehicle_profile_interfaces/msg/VehicleBaseInfo.msg diff --git a/agv_calib_brain/src/win_ubuntu_bridge/calibration_vehicle_profile_interfaces/msg/VehicleCalibrationApplicabilityResponse.msg b/agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_vehicle_profile_interfaces/msg/VehicleCalibrationApplicabilityResponse.msg similarity index 100% rename from agv_calib_brain/src/win_ubuntu_bridge/calibration_vehicle_profile_interfaces/msg/VehicleCalibrationApplicabilityResponse.msg rename to agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_vehicle_profile_interfaces/msg/VehicleCalibrationApplicabilityResponse.msg diff --git a/agv_calib_brain/src/win_ubuntu_bridge/calibration_vehicle_profile_interfaces/msg/VehicleProfile.msg b/agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_vehicle_profile_interfaces/msg/VehicleProfile.msg similarity index 100% rename from agv_calib_brain/src/win_ubuntu_bridge/calibration_vehicle_profile_interfaces/msg/VehicleProfile.msg rename to agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_vehicle_profile_interfaces/msg/VehicleProfile.msg diff --git a/agv_calib_brain/src/win_ubuntu_bridge/calibration_vehicle_profile_interfaces/msg/VehicleProfileQuery.msg b/agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_vehicle_profile_interfaces/msg/VehicleProfileQuery.msg similarity index 100% rename from agv_calib_brain/src/win_ubuntu_bridge/calibration_vehicle_profile_interfaces/msg/VehicleProfileQuery.msg rename to agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_vehicle_profile_interfaces/msg/VehicleProfileQuery.msg diff --git a/agv_calib_brain/src/win_ubuntu_bridge/calibration_vehicle_profile_interfaces/msg/VehicleProfileResponse.msg b/agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_vehicle_profile_interfaces/msg/VehicleProfileResponse.msg similarity index 100% rename from agv_calib_brain/src/win_ubuntu_bridge/calibration_vehicle_profile_interfaces/msg/VehicleProfileResponse.msg rename to agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_vehicle_profile_interfaces/msg/VehicleProfileResponse.msg diff --git a/agv_calib_brain/src/win_ubuntu_bridge/calibration_vehicle_profile_interfaces/msg/WorkflowStageType.msg b/agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_vehicle_profile_interfaces/msg/WorkflowStageType.msg similarity index 100% rename from agv_calib_brain/src/win_ubuntu_bridge/calibration_vehicle_profile_interfaces/msg/WorkflowStageType.msg rename to agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_vehicle_profile_interfaces/msg/WorkflowStageType.msg diff --git a/agv_calib_brain/src/win_ubuntu_bridge/calibration_vehicle_profile_interfaces/package.xml b/agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_vehicle_profile_interfaces/package.xml similarity index 100% rename from agv_calib_brain/src/win_ubuntu_bridge/calibration_vehicle_profile_interfaces/package.xml rename to agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_vehicle_profile_interfaces/package.xml diff --git a/agv_calib_brain/src/win_ubuntu_bridge/win_ubuntu_bridge/proto/vehicle_profile.proto b/agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_vehicle_profile_interfaces/proto/vehicle_profile.proto similarity index 99% rename from agv_calib_brain/src/win_ubuntu_bridge/win_ubuntu_bridge/proto/vehicle_profile.proto rename to agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_vehicle_profile_interfaces/proto/vehicle_profile.proto index 678fccd..a396702 100644 --- a/agv_calib_brain/src/win_ubuntu_bridge/win_ubuntu_bridge/proto/vehicle_profile.proto +++ b/agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_vehicle_profile_interfaces/proto/vehicle_profile.proto @@ -352,4 +352,4 @@ message VehicleCalibrationApplicabilityResponse { repeated ApplicabilityIssue issues = 5; // 问题列表 bool overall_supported = 6; // 是否总体可开展标定 repeated WorkflowStageType recommended_workflow_stages = 7; // 推荐执行阶段 -} \ No newline at end of file +} diff --git a/agv_calib_brain/src/win_ubuntu_bridge/calibration_vehicle_profile_interfaces/srv/EvaluateVehicleCalibrationApplicability.srv b/agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_vehicle_profile_interfaces/srv/EvaluateVehicleCalibrationApplicability.srv similarity index 100% rename from agv_calib_brain/src/win_ubuntu_bridge/calibration_vehicle_profile_interfaces/srv/EvaluateVehicleCalibrationApplicability.srv rename to agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_vehicle_profile_interfaces/srv/EvaluateVehicleCalibrationApplicability.srv diff --git a/agv_calib_brain/src/win_ubuntu_bridge/calibration_vehicle_profile_interfaces/srv/GetVehicleProfile.srv b/agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_vehicle_profile_interfaces/srv/GetVehicleProfile.srv similarity index 100% rename from agv_calib_brain/src/win_ubuntu_bridge/calibration_vehicle_profile_interfaces/srv/GetVehicleProfile.srv rename to agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_vehicle_profile_interfaces/srv/GetVehicleProfile.srv diff --git a/agv_calib_brain/src/win_ubuntu_bridge/calibration_vehicle_profile_interfaces/srv/Heartbeat.srv b/agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_vehicle_profile_interfaces/srv/Heartbeat.srv similarity index 100% rename from agv_calib_brain/src/win_ubuntu_bridge/calibration_vehicle_profile_interfaces/srv/Heartbeat.srv rename to agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_vehicle_profile_interfaces/srv/Heartbeat.srv diff --git a/agv_calib_brain/src/win_ubuntu_bridge/calibration_vehicle_profile_interfaces/srv/RegisterOrUpdateVehicleProfile.srv b/agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_vehicle_profile_interfaces/srv/RegisterOrUpdateVehicleProfile.srv similarity index 100% rename from agv_calib_brain/src/win_ubuntu_bridge/calibration_vehicle_profile_interfaces/srv/RegisterOrUpdateVehicleProfile.srv rename to agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_vehicle_profile_interfaces/srv/RegisterOrUpdateVehicleProfile.srv diff --git a/agv_calib_brain/src/win_ubuntu_bridge/calibration_workshop_orchestration_interfaces/CMakeLists.txt b/agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_workshop_orchestration_interfaces/CMakeLists.txt similarity index 100% rename from agv_calib_brain/src/win_ubuntu_bridge/calibration_workshop_orchestration_interfaces/CMakeLists.txt rename to agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_workshop_orchestration_interfaces/CMakeLists.txt diff --git a/agv_calib_brain/src/win_ubuntu_bridge/calibration_workshop_orchestration_interfaces/FILE_TREE.txt b/agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_workshop_orchestration_interfaces/FILE_TREE.txt similarity index 100% rename from agv_calib_brain/src/win_ubuntu_bridge/calibration_workshop_orchestration_interfaces/FILE_TREE.txt rename to agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_workshop_orchestration_interfaces/FILE_TREE.txt diff --git a/agv_calib_brain/src/win_ubuntu_bridge/calibration_workshop_orchestration_interfaces/action/ExecuteWorkshopSession.action b/agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_workshop_orchestration_interfaces/action/ExecuteWorkshopSession.action similarity index 100% rename from agv_calib_brain/src/win_ubuntu_bridge/calibration_workshop_orchestration_interfaces/action/ExecuteWorkshopSession.action rename to agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_workshop_orchestration_interfaces/action/ExecuteWorkshopSession.action diff --git a/agv_calib_brain/src/win_ubuntu_bridge/calibration_workshop_orchestration_interfaces/action/RunWorkshopPrecheck.action b/agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_workshop_orchestration_interfaces/action/RunWorkshopPrecheck.action similarity index 100% rename from agv_calib_brain/src/win_ubuntu_bridge/calibration_workshop_orchestration_interfaces/action/RunWorkshopPrecheck.action rename to agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_workshop_orchestration_interfaces/action/RunWorkshopPrecheck.action diff --git a/agv_calib_brain/src/win_ubuntu_bridge/calibration_workshop_orchestration_interfaces/msg/ApprovalState.msg b/agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_workshop_orchestration_interfaces/msg/ApprovalState.msg similarity index 100% rename from agv_calib_brain/src/win_ubuntu_bridge/calibration_workshop_orchestration_interfaces/msg/ApprovalState.msg rename to agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_workshop_orchestration_interfaces/msg/ApprovalState.msg diff --git a/agv_calib_brain/src/win_ubuntu_bridge/calibration_workshop_orchestration_interfaces/msg/ApproveStageResultRequest.msg b/agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_workshop_orchestration_interfaces/msg/ApproveStageResultRequest.msg similarity index 100% rename from agv_calib_brain/src/win_ubuntu_bridge/calibration_workshop_orchestration_interfaces/msg/ApproveStageResultRequest.msg rename to agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_workshop_orchestration_interfaces/msg/ApproveStageResultRequest.msg diff --git a/agv_calib_brain/src/win_ubuntu_bridge/calibration_workshop_orchestration_interfaces/msg/BuildExecutionPlanRequest.msg b/agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_workshop_orchestration_interfaces/msg/BuildExecutionPlanRequest.msg similarity index 100% rename from agv_calib_brain/src/win_ubuntu_bridge/calibration_workshop_orchestration_interfaces/msg/BuildExecutionPlanRequest.msg rename to agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_workshop_orchestration_interfaces/msg/BuildExecutionPlanRequest.msg diff --git a/agv_calib_brain/src/win_ubuntu_bridge/calibration_workshop_orchestration_interfaces/msg/BuildExecutionPlanResponse.msg b/agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_workshop_orchestration_interfaces/msg/BuildExecutionPlanResponse.msg similarity index 100% rename from agv_calib_brain/src/win_ubuntu_bridge/calibration_workshop_orchestration_interfaces/msg/BuildExecutionPlanResponse.msg rename to agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_workshop_orchestration_interfaces/msg/BuildExecutionPlanResponse.msg diff --git a/agv_calib_brain/src/win_ubuntu_bridge/calibration_workshop_orchestration_interfaces/msg/CancelWorkshopSessionRequest.msg b/agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_workshop_orchestration_interfaces/msg/CancelWorkshopSessionRequest.msg similarity index 100% rename from agv_calib_brain/src/win_ubuntu_bridge/calibration_workshop_orchestration_interfaces/msg/CancelWorkshopSessionRequest.msg rename to agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_workshop_orchestration_interfaces/msg/CancelWorkshopSessionRequest.msg diff --git a/agv_calib_brain/src/win_ubuntu_bridge/calibration_workshop_orchestration_interfaces/msg/CreateWorkshopSessionRequest.msg b/agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_workshop_orchestration_interfaces/msg/CreateWorkshopSessionRequest.msg similarity index 100% rename from agv_calib_brain/src/win_ubuntu_bridge/calibration_workshop_orchestration_interfaces/msg/CreateWorkshopSessionRequest.msg rename to agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_workshop_orchestration_interfaces/msg/CreateWorkshopSessionRequest.msg diff --git a/agv_calib_brain/src/win_ubuntu_bridge/calibration_workshop_orchestration_interfaces/msg/CreateWorkshopSessionResponse.msg b/agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_workshop_orchestration_interfaces/msg/CreateWorkshopSessionResponse.msg similarity index 100% rename from agv_calib_brain/src/win_ubuntu_bridge/calibration_workshop_orchestration_interfaces/msg/CreateWorkshopSessionResponse.msg rename to agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_workshop_orchestration_interfaces/msg/CreateWorkshopSessionResponse.msg diff --git a/agv_calib_brain/src/win_ubuntu_bridge/calibration_workshop_orchestration_interfaces/msg/ExecutionPlanValidationIssue.msg b/agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_workshop_orchestration_interfaces/msg/ExecutionPlanValidationIssue.msg similarity index 100% rename from agv_calib_brain/src/win_ubuntu_bridge/calibration_workshop_orchestration_interfaces/msg/ExecutionPlanValidationIssue.msg rename to agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_workshop_orchestration_interfaces/msg/ExecutionPlanValidationIssue.msg diff --git a/agv_calib_brain/src/win_ubuntu_bridge/calibration_workshop_orchestration_interfaces/msg/ManualActionType.msg b/agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_workshop_orchestration_interfaces/msg/ManualActionType.msg similarity index 100% rename from agv_calib_brain/src/win_ubuntu_bridge/calibration_workshop_orchestration_interfaces/msg/ManualActionType.msg rename to agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_workshop_orchestration_interfaces/msg/ManualActionType.msg diff --git a/agv_calib_brain/src/win_ubuntu_bridge/calibration_workshop_orchestration_interfaces/msg/ManualStepAckRequest.msg b/agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_workshop_orchestration_interfaces/msg/ManualStepAckRequest.msg similarity index 100% rename from agv_calib_brain/src/win_ubuntu_bridge/calibration_workshop_orchestration_interfaces/msg/ManualStepAckRequest.msg rename to agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_workshop_orchestration_interfaces/msg/ManualStepAckRequest.msg diff --git a/agv_calib_brain/src/win_ubuntu_bridge/calibration_workshop_orchestration_interfaces/msg/PauseWorkshopSessionRequest.msg b/agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_workshop_orchestration_interfaces/msg/PauseWorkshopSessionRequest.msg similarity index 100% rename from agv_calib_brain/src/win_ubuntu_bridge/calibration_workshop_orchestration_interfaces/msg/PauseWorkshopSessionRequest.msg rename to agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_workshop_orchestration_interfaces/msg/PauseWorkshopSessionRequest.msg diff --git a/agv_calib_brain/src/win_ubuntu_bridge/calibration_workshop_orchestration_interfaces/msg/PrecheckItem.msg b/agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_workshop_orchestration_interfaces/msg/PrecheckItem.msg similarity index 100% rename from agv_calib_brain/src/win_ubuntu_bridge/calibration_workshop_orchestration_interfaces/msg/PrecheckItem.msg rename to agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_workshop_orchestration_interfaces/msg/PrecheckItem.msg diff --git a/agv_calib_brain/src/win_ubuntu_bridge/calibration_workshop_orchestration_interfaces/msg/RebuildExecutionPlanRequest.msg b/agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_workshop_orchestration_interfaces/msg/RebuildExecutionPlanRequest.msg similarity index 100% rename from agv_calib_brain/src/win_ubuntu_bridge/calibration_workshop_orchestration_interfaces/msg/RebuildExecutionPlanRequest.msg rename to agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_workshop_orchestration_interfaces/msg/RebuildExecutionPlanRequest.msg diff --git a/agv_calib_brain/src/win_ubuntu_bridge/calibration_workshop_orchestration_interfaces/msg/RequestedCalibrationTask.msg b/agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_workshop_orchestration_interfaces/msg/RequestedCalibrationTask.msg similarity index 100% rename from agv_calib_brain/src/win_ubuntu_bridge/calibration_workshop_orchestration_interfaces/msg/RequestedCalibrationTask.msg rename to agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_workshop_orchestration_interfaces/msg/RequestedCalibrationTask.msg diff --git a/agv_calib_brain/src/win_ubuntu_bridge/calibration_workshop_orchestration_interfaces/msg/ResumeWorkshopSessionRequest.msg b/agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_workshop_orchestration_interfaces/msg/ResumeWorkshopSessionRequest.msg similarity index 100% rename from agv_calib_brain/src/win_ubuntu_bridge/calibration_workshop_orchestration_interfaces/msg/ResumeWorkshopSessionRequest.msg rename to agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_workshop_orchestration_interfaces/msg/ResumeWorkshopSessionRequest.msg diff --git a/agv_calib_brain/src/win_ubuntu_bridge/calibration_workshop_orchestration_interfaces/msg/RollbackStageParametersRequest.msg b/agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_workshop_orchestration_interfaces/msg/RollbackStageParametersRequest.msg similarity index 100% rename from agv_calib_brain/src/win_ubuntu_bridge/calibration_workshop_orchestration_interfaces/msg/RollbackStageParametersRequest.msg rename to agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_workshop_orchestration_interfaces/msg/RollbackStageParametersRequest.msg diff --git a/agv_calib_brain/src/win_ubuntu_bridge/calibration_workshop_orchestration_interfaces/msg/RunWorkshopPrecheckRequest.msg b/agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_workshop_orchestration_interfaces/msg/RunWorkshopPrecheckRequest.msg similarity index 100% rename from agv_calib_brain/src/win_ubuntu_bridge/calibration_workshop_orchestration_interfaces/msg/RunWorkshopPrecheckRequest.msg rename to agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_workshop_orchestration_interfaces/msg/RunWorkshopPrecheckRequest.msg diff --git a/agv_calib_brain/src/win_ubuntu_bridge/calibration_workshop_orchestration_interfaces/msg/StageExecutionPolicy.msg b/agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_workshop_orchestration_interfaces/msg/StageExecutionPolicy.msg similarity index 100% rename from agv_calib_brain/src/win_ubuntu_bridge/calibration_workshop_orchestration_interfaces/msg/StageExecutionPolicy.msg rename to agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_workshop_orchestration_interfaces/msg/StageExecutionPolicy.msg diff --git a/agv_calib_brain/src/win_ubuntu_bridge/calibration_workshop_orchestration_interfaces/msg/StagePlan.msg b/agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_workshop_orchestration_interfaces/msg/StagePlan.msg similarity index 100% rename from agv_calib_brain/src/win_ubuntu_bridge/calibration_workshop_orchestration_interfaces/msg/StagePlan.msg rename to agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_workshop_orchestration_interfaces/msg/StagePlan.msg diff --git a/agv_calib_brain/src/win_ubuntu_bridge/calibration_workshop_orchestration_interfaces/msg/StageResultSummary.msg b/agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_workshop_orchestration_interfaces/msg/StageResultSummary.msg similarity index 100% rename from agv_calib_brain/src/win_ubuntu_bridge/calibration_workshop_orchestration_interfaces/msg/StageResultSummary.msg rename to agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_workshop_orchestration_interfaces/msg/StageResultSummary.msg diff --git a/agv_calib_brain/src/win_ubuntu_bridge/calibration_workshop_orchestration_interfaces/msg/StartWorkshopSessionRequest.msg b/agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_workshop_orchestration_interfaces/msg/StartWorkshopSessionRequest.msg similarity index 100% rename from agv_calib_brain/src/win_ubuntu_bridge/calibration_workshop_orchestration_interfaces/msg/StartWorkshopSessionRequest.msg rename to agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_workshop_orchestration_interfaces/msg/StartWorkshopSessionRequest.msg diff --git a/agv_calib_brain/src/win_ubuntu_bridge/calibration_workshop_orchestration_interfaces/msg/ValidateExecutionPlanRequest.msg b/agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_workshop_orchestration_interfaces/msg/ValidateExecutionPlanRequest.msg similarity index 100% rename from agv_calib_brain/src/win_ubuntu_bridge/calibration_workshop_orchestration_interfaces/msg/ValidateExecutionPlanRequest.msg rename to agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_workshop_orchestration_interfaces/msg/ValidateExecutionPlanRequest.msg diff --git a/agv_calib_brain/src/win_ubuntu_bridge/calibration_workshop_orchestration_interfaces/msg/ValidateExecutionPlanResponse.msg b/agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_workshop_orchestration_interfaces/msg/ValidateExecutionPlanResponse.msg similarity index 100% rename from agv_calib_brain/src/win_ubuntu_bridge/calibration_workshop_orchestration_interfaces/msg/ValidateExecutionPlanResponse.msg rename to agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_workshop_orchestration_interfaces/msg/ValidateExecutionPlanResponse.msg diff --git a/agv_calib_brain/src/win_ubuntu_bridge/calibration_workshop_orchestration_interfaces/msg/WorkshopEvent.msg b/agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_workshop_orchestration_interfaces/msg/WorkshopEvent.msg similarity index 100% rename from agv_calib_brain/src/win_ubuntu_bridge/calibration_workshop_orchestration_interfaces/msg/WorkshopEvent.msg rename to agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_workshop_orchestration_interfaces/msg/WorkshopEvent.msg diff --git a/agv_calib_brain/src/win_ubuntu_bridge/calibration_workshop_orchestration_interfaces/msg/WorkshopEventType.msg b/agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_workshop_orchestration_interfaces/msg/WorkshopEventType.msg similarity index 100% rename from agv_calib_brain/src/win_ubuntu_bridge/calibration_workshop_orchestration_interfaces/msg/WorkshopEventType.msg rename to agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_workshop_orchestration_interfaces/msg/WorkshopEventType.msg diff --git a/agv_calib_brain/src/win_ubuntu_bridge/calibration_workshop_orchestration_interfaces/msg/WorkshopOperatorInfo.msg b/agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_workshop_orchestration_interfaces/msg/WorkshopOperatorInfo.msg similarity index 100% rename from agv_calib_brain/src/win_ubuntu_bridge/calibration_workshop_orchestration_interfaces/msg/WorkshopOperatorInfo.msg rename to agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_workshop_orchestration_interfaces/msg/WorkshopOperatorInfo.msg diff --git a/agv_calib_brain/src/win_ubuntu_bridge/calibration_workshop_orchestration_interfaces/msg/WorkshopPrecheckResponse.msg b/agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_workshop_orchestration_interfaces/msg/WorkshopPrecheckResponse.msg similarity index 100% rename from agv_calib_brain/src/win_ubuntu_bridge/calibration_workshop_orchestration_interfaces/msg/WorkshopPrecheckResponse.msg rename to agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_workshop_orchestration_interfaces/msg/WorkshopPrecheckResponse.msg diff --git a/agv_calib_brain/src/win_ubuntu_bridge/calibration_workshop_orchestration_interfaces/msg/WorkshopReport.msg b/agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_workshop_orchestration_interfaces/msg/WorkshopReport.msg similarity index 100% rename from agv_calib_brain/src/win_ubuntu_bridge/calibration_workshop_orchestration_interfaces/msg/WorkshopReport.msg rename to agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_workshop_orchestration_interfaces/msg/WorkshopReport.msg diff --git a/agv_calib_brain/src/win_ubuntu_bridge/calibration_workshop_orchestration_interfaces/msg/WorkshopReportResponse.msg b/agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_workshop_orchestration_interfaces/msg/WorkshopReportResponse.msg similarity index 100% rename from agv_calib_brain/src/win_ubuntu_bridge/calibration_workshop_orchestration_interfaces/msg/WorkshopReportResponse.msg rename to agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_workshop_orchestration_interfaces/msg/WorkshopReportResponse.msg diff --git a/agv_calib_brain/src/win_ubuntu_bridge/calibration_workshop_orchestration_interfaces/msg/WorkshopSession.msg b/agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_workshop_orchestration_interfaces/msg/WorkshopSession.msg similarity index 100% rename from agv_calib_brain/src/win_ubuntu_bridge/calibration_workshop_orchestration_interfaces/msg/WorkshopSession.msg rename to agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_workshop_orchestration_interfaces/msg/WorkshopSession.msg diff --git a/agv_calib_brain/src/win_ubuntu_bridge/calibration_workshop_orchestration_interfaces/msg/WorkshopSessionConfig.msg b/agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_workshop_orchestration_interfaces/msg/WorkshopSessionConfig.msg similarity index 100% rename from agv_calib_brain/src/win_ubuntu_bridge/calibration_workshop_orchestration_interfaces/msg/WorkshopSessionConfig.msg rename to agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_workshop_orchestration_interfaces/msg/WorkshopSessionConfig.msg diff --git a/agv_calib_brain/src/win_ubuntu_bridge/calibration_workshop_orchestration_interfaces/msg/WorkshopSessionQuery.msg b/agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_workshop_orchestration_interfaces/msg/WorkshopSessionQuery.msg similarity index 100% rename from agv_calib_brain/src/win_ubuntu_bridge/calibration_workshop_orchestration_interfaces/msg/WorkshopSessionQuery.msg rename to agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_workshop_orchestration_interfaces/msg/WorkshopSessionQuery.msg diff --git a/agv_calib_brain/src/win_ubuntu_bridge/calibration_workshop_orchestration_interfaces/msg/WorkshopSessionResponse.msg b/agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_workshop_orchestration_interfaces/msg/WorkshopSessionResponse.msg similarity index 100% rename from agv_calib_brain/src/win_ubuntu_bridge/calibration_workshop_orchestration_interfaces/msg/WorkshopSessionResponse.msg rename to agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_workshop_orchestration_interfaces/msg/WorkshopSessionResponse.msg diff --git a/agv_calib_brain/src/win_ubuntu_bridge/calibration_workshop_orchestration_interfaces/msg/WorkshopSessionState.msg b/agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_workshop_orchestration_interfaces/msg/WorkshopSessionState.msg similarity index 100% rename from agv_calib_brain/src/win_ubuntu_bridge/calibration_workshop_orchestration_interfaces/msg/WorkshopSessionState.msg rename to agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_workshop_orchestration_interfaces/msg/WorkshopSessionState.msg diff --git a/agv_calib_brain/src/win_ubuntu_bridge/calibration_workshop_orchestration_interfaces/package.xml b/agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_workshop_orchestration_interfaces/package.xml similarity index 100% rename from agv_calib_brain/src/win_ubuntu_bridge/calibration_workshop_orchestration_interfaces/package.xml rename to agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_workshop_orchestration_interfaces/package.xml diff --git a/agv_calib_brain/src/win_ubuntu_bridge/calibration_workshop_orchestration_interfaces/proto/workshop_orchestration.proto b/agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_workshop_orchestration_interfaces/proto/workshop_orchestration.proto similarity index 100% rename from agv_calib_brain/src/win_ubuntu_bridge/calibration_workshop_orchestration_interfaces/proto/workshop_orchestration.proto rename to agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_workshop_orchestration_interfaces/proto/workshop_orchestration.proto diff --git a/agv_calib_brain/src/win_ubuntu_bridge/calibration_workshop_orchestration_interfaces/srv/AcknowledgeManualStep.srv b/agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_workshop_orchestration_interfaces/srv/AcknowledgeManualStep.srv similarity index 100% rename from agv_calib_brain/src/win_ubuntu_bridge/calibration_workshop_orchestration_interfaces/srv/AcknowledgeManualStep.srv rename to agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_workshop_orchestration_interfaces/srv/AcknowledgeManualStep.srv diff --git a/agv_calib_brain/src/win_ubuntu_bridge/calibration_workshop_orchestration_interfaces/srv/ApproveStageResult.srv b/agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_workshop_orchestration_interfaces/srv/ApproveStageResult.srv similarity index 100% rename from agv_calib_brain/src/win_ubuntu_bridge/calibration_workshop_orchestration_interfaces/srv/ApproveStageResult.srv rename to agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_workshop_orchestration_interfaces/srv/ApproveStageResult.srv diff --git a/agv_calib_brain/src/win_ubuntu_bridge/calibration_workshop_orchestration_interfaces/srv/BuildExecutionPlan.srv b/agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_workshop_orchestration_interfaces/srv/BuildExecutionPlan.srv similarity index 100% rename from agv_calib_brain/src/win_ubuntu_bridge/calibration_workshop_orchestration_interfaces/srv/BuildExecutionPlan.srv rename to agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_workshop_orchestration_interfaces/srv/BuildExecutionPlan.srv diff --git a/agv_calib_brain/src/win_ubuntu_bridge/calibration_workshop_orchestration_interfaces/srv/CancelWorkshopSession.srv b/agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_workshop_orchestration_interfaces/srv/CancelWorkshopSession.srv similarity index 100% rename from agv_calib_brain/src/win_ubuntu_bridge/calibration_workshop_orchestration_interfaces/srv/CancelWorkshopSession.srv rename to agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_workshop_orchestration_interfaces/srv/CancelWorkshopSession.srv diff --git a/agv_calib_brain/src/win_ubuntu_bridge/calibration_workshop_orchestration_interfaces/srv/CreateWorkshopSession.srv b/agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_workshop_orchestration_interfaces/srv/CreateWorkshopSession.srv similarity index 100% rename from agv_calib_brain/src/win_ubuntu_bridge/calibration_workshop_orchestration_interfaces/srv/CreateWorkshopSession.srv rename to agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_workshop_orchestration_interfaces/srv/CreateWorkshopSession.srv diff --git a/agv_calib_brain/src/win_ubuntu_bridge/calibration_workshop_orchestration_interfaces/srv/GetLastWorkshopPrecheckResult.srv b/agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_workshop_orchestration_interfaces/srv/GetLastWorkshopPrecheckResult.srv similarity index 100% rename from agv_calib_brain/src/win_ubuntu_bridge/calibration_workshop_orchestration_interfaces/srv/GetLastWorkshopPrecheckResult.srv rename to agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_workshop_orchestration_interfaces/srv/GetLastWorkshopPrecheckResult.srv diff --git a/agv_calib_brain/src/win_ubuntu_bridge/calibration_workshop_orchestration_interfaces/srv/GetWorkshopJobStatus.srv b/agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_workshop_orchestration_interfaces/srv/GetWorkshopJobStatus.srv similarity index 100% rename from agv_calib_brain/src/win_ubuntu_bridge/calibration_workshop_orchestration_interfaces/srv/GetWorkshopJobStatus.srv rename to agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_workshop_orchestration_interfaces/srv/GetWorkshopJobStatus.srv diff --git a/agv_calib_brain/src/win_ubuntu_bridge/calibration_workshop_orchestration_interfaces/srv/GetWorkshopReport.srv b/agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_workshop_orchestration_interfaces/srv/GetWorkshopReport.srv similarity index 100% rename from agv_calib_brain/src/win_ubuntu_bridge/calibration_workshop_orchestration_interfaces/srv/GetWorkshopReport.srv rename to agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_workshop_orchestration_interfaces/srv/GetWorkshopReport.srv diff --git a/agv_calib_brain/src/win_ubuntu_bridge/calibration_workshop_orchestration_interfaces/srv/GetWorkshopSession.srv b/agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_workshop_orchestration_interfaces/srv/GetWorkshopSession.srv similarity index 100% rename from agv_calib_brain/src/win_ubuntu_bridge/calibration_workshop_orchestration_interfaces/srv/GetWorkshopSession.srv rename to agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_workshop_orchestration_interfaces/srv/GetWorkshopSession.srv diff --git a/agv_calib_brain/src/win_ubuntu_bridge/calibration_workshop_orchestration_interfaces/srv/Heartbeat.srv b/agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_workshop_orchestration_interfaces/srv/Heartbeat.srv similarity index 100% rename from agv_calib_brain/src/win_ubuntu_bridge/calibration_workshop_orchestration_interfaces/srv/Heartbeat.srv rename to agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_workshop_orchestration_interfaces/srv/Heartbeat.srv diff --git a/agv_calib_brain/src/win_ubuntu_bridge/calibration_workshop_orchestration_interfaces/srv/PauseWorkshopSession.srv b/agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_workshop_orchestration_interfaces/srv/PauseWorkshopSession.srv similarity index 100% rename from agv_calib_brain/src/win_ubuntu_bridge/calibration_workshop_orchestration_interfaces/srv/PauseWorkshopSession.srv rename to agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_workshop_orchestration_interfaces/srv/PauseWorkshopSession.srv diff --git a/agv_calib_brain/src/win_ubuntu_bridge/calibration_workshop_orchestration_interfaces/srv/RebuildExecutionPlan.srv b/agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_workshop_orchestration_interfaces/srv/RebuildExecutionPlan.srv similarity index 100% rename from agv_calib_brain/src/win_ubuntu_bridge/calibration_workshop_orchestration_interfaces/srv/RebuildExecutionPlan.srv rename to agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_workshop_orchestration_interfaces/srv/RebuildExecutionPlan.srv diff --git a/agv_calib_brain/src/win_ubuntu_bridge/calibration_workshop_orchestration_interfaces/srv/ResumeWorkshopSession.srv b/agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_workshop_orchestration_interfaces/srv/ResumeWorkshopSession.srv similarity index 100% rename from agv_calib_brain/src/win_ubuntu_bridge/calibration_workshop_orchestration_interfaces/srv/ResumeWorkshopSession.srv rename to agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_workshop_orchestration_interfaces/srv/ResumeWorkshopSession.srv diff --git a/agv_calib_brain/src/win_ubuntu_bridge/calibration_workshop_orchestration_interfaces/srv/RollbackStageParameters.srv b/agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_workshop_orchestration_interfaces/srv/RollbackStageParameters.srv similarity index 100% rename from agv_calib_brain/src/win_ubuntu_bridge/calibration_workshop_orchestration_interfaces/srv/RollbackStageParameters.srv rename to agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_workshop_orchestration_interfaces/srv/RollbackStageParameters.srv diff --git a/agv_calib_brain/src/win_ubuntu_bridge/calibration_workshop_orchestration_interfaces/srv/RunWorkshopPrecheck.srv b/agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_workshop_orchestration_interfaces/srv/RunWorkshopPrecheck.srv similarity index 100% rename from agv_calib_brain/src/win_ubuntu_bridge/calibration_workshop_orchestration_interfaces/srv/RunWorkshopPrecheck.srv rename to agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_workshop_orchestration_interfaces/srv/RunWorkshopPrecheck.srv diff --git a/agv_calib_brain/src/win_ubuntu_bridge/calibration_workshop_orchestration_interfaces/srv/StartWorkshopSession.srv b/agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_workshop_orchestration_interfaces/srv/StartWorkshopSession.srv similarity index 100% rename from agv_calib_brain/src/win_ubuntu_bridge/calibration_workshop_orchestration_interfaces/srv/StartWorkshopSession.srv rename to agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_workshop_orchestration_interfaces/srv/StartWorkshopSession.srv diff --git a/agv_calib_brain/src/win_ubuntu_bridge/calibration_workshop_orchestration_interfaces/srv/StreamWorkshopEvent.srv b/agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_workshop_orchestration_interfaces/srv/StreamWorkshopEvent.srv similarity index 100% rename from agv_calib_brain/src/win_ubuntu_bridge/calibration_workshop_orchestration_interfaces/srv/StreamWorkshopEvent.srv rename to agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_workshop_orchestration_interfaces/srv/StreamWorkshopEvent.srv diff --git a/agv_calib_brain/src/win_ubuntu_bridge/calibration_workshop_orchestration_interfaces/srv/ValidateExecutionPlan.srv b/agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_workshop_orchestration_interfaces/srv/ValidateExecutionPlan.srv similarity index 100% rename from agv_calib_brain/src/win_ubuntu_bridge/calibration_workshop_orchestration_interfaces/srv/ValidateExecutionPlan.srv rename to agv_calib_brain/src/communication/win_ubuntu_bridge/calibration_workshop_orchestration_interfaces/srv/ValidateExecutionPlan.srv diff --git a/agv_calib_brain/src/win_ubuntu_bridge/vehicle_agent_gateway/CMakeLists.txt b/agv_calib_brain/src/communication/win_ubuntu_bridge/vehicle_agent_gateway/CMakeLists.txt similarity index 100% rename from agv_calib_brain/src/win_ubuntu_bridge/vehicle_agent_gateway/CMakeLists.txt rename to agv_calib_brain/src/communication/win_ubuntu_bridge/vehicle_agent_gateway/CMakeLists.txt diff --git a/agv_calib_brain/src/win_ubuntu_bridge/vehicle_agent_gateway/include/vehicle_agent_gateway/chassis_bridge_node.hpp b/agv_calib_brain/src/communication/win_ubuntu_bridge/vehicle_agent_gateway/include/vehicle_agent_gateway/chassis_bridge_node.hpp similarity index 100% rename from agv_calib_brain/src/win_ubuntu_bridge/vehicle_agent_gateway/include/vehicle_agent_gateway/chassis_bridge_node.hpp rename to agv_calib_brain/src/communication/win_ubuntu_bridge/vehicle_agent_gateway/include/vehicle_agent_gateway/chassis_bridge_node.hpp diff --git a/agv_calib_brain/src/win_ubuntu_bridge/vehicle_agent_gateway/include/vehicle_agent_gateway/control_bridge_node.hpp b/agv_calib_brain/src/communication/win_ubuntu_bridge/vehicle_agent_gateway/include/vehicle_agent_gateway/control_bridge_node.hpp similarity index 100% rename from agv_calib_brain/src/win_ubuntu_bridge/vehicle_agent_gateway/include/vehicle_agent_gateway/control_bridge_node.hpp rename to agv_calib_brain/src/communication/win_ubuntu_bridge/vehicle_agent_gateway/include/vehicle_agent_gateway/control_bridge_node.hpp diff --git a/agv_calib_brain/src/win_ubuntu_bridge/vehicle_agent_gateway/include/vehicle_agent_gateway/gateway_codec.hpp b/agv_calib_brain/src/communication/win_ubuntu_bridge/vehicle_agent_gateway/include/vehicle_agent_gateway/gateway_codec.hpp similarity index 94% rename from agv_calib_brain/src/win_ubuntu_bridge/vehicle_agent_gateway/include/vehicle_agent_gateway/gateway_codec.hpp rename to agv_calib_brain/src/communication/win_ubuntu_bridge/vehicle_agent_gateway/include/vehicle_agent_gateway/gateway_codec.hpp index 7d9feb1..64e438a 100644 --- a/agv_calib_brain/src/win_ubuntu_bridge/vehicle_agent_gateway/include/vehicle_agent_gateway/gateway_codec.hpp +++ b/agv_calib_brain/src/communication/win_ubuntu_bridge/vehicle_agent_gateway/include/vehicle_agent_gateway/gateway_codec.hpp @@ -281,7 +281,14 @@ inline std::string encode_controller_evaluation_request( j["trajectory_tracking"] = { {"path", path_arr}, {"stop_at_end", req.trajectory_tracking.stop_at_end}, - {"timeout_sec", req.trajectory_tracking.timeout_sec} + {"timeout_sec", req.trajectory_tracking.timeout_sec}, + {"required_external_pose_source_id", req.trajectory_tracking.required_external_pose_source_id}, + {"max_external_pose_age_ms", req.trajectory_tracking.max_external_pose_age_ms}, + {"min_external_pose_quality_score", req.trajectory_tracking.min_external_pose_quality_score}, + {"trajectory_id", req.trajectory_tracking.trajectory_id}, + {"segment_index", req.trajectory_tracking.segment_index}, + {"total_segments", req.trajectory_tracking.total_segments}, + {"is_final_segment", req.trajectory_tracking.is_final_segment} }; break; } @@ -334,6 +341,11 @@ inline void decode_control_readiness_response( rsp.estop_released = j.value("estop_released", false); rsp.vehicle_safe_to_move = j.value("vehicle_safe_to_move", false); rsp.checked_timestamp_us = j.value("checked_timestamp_us", int64_t{0}); + rsp.external_pose_feedback_ready = j.value("external_pose_feedback_ready", false); + rsp.external_pose_source_name = j.value("external_pose_source_name", std::string{}); + rsp.external_pose_age_ms = j.value("external_pose_age_ms", 0.0); + rsp.external_pose_quality_score = j.value("external_pose_quality_score", 0.0); + rsp.external_pose_transport = j.value("external_pose_transport", std::string{}); if (j.contains("error_code")) { decode_error_code(j["error_code"], rsp.error_code); } diff --git a/agv_calib_brain/src/win_ubuntu_bridge/vehicle_agent_gateway/include/vehicle_agent_gateway/proto_frame.hpp b/agv_calib_brain/src/communication/win_ubuntu_bridge/vehicle_agent_gateway/include/vehicle_agent_gateway/proto_frame.hpp similarity index 70% rename from agv_calib_brain/src/win_ubuntu_bridge/vehicle_agent_gateway/include/vehicle_agent_gateway/proto_frame.hpp rename to agv_calib_brain/src/communication/win_ubuntu_bridge/vehicle_agent_gateway/include/vehicle_agent_gateway/proto_frame.hpp index d2c20d0..d9acc4b 100644 --- a/agv_calib_brain/src/win_ubuntu_bridge/vehicle_agent_gateway/include/vehicle_agent_gateway/proto_frame.hpp +++ b/agv_calib_brain/src/communication/win_ubuntu_bridge/vehicle_agent_gateway/include/vehicle_agent_gateway/proto_frame.hpp @@ -7,6 +7,7 @@ namespace vehicle_agent_gateway { // 消息帧类型枚举 +// 必须与 proto/transport_contract.proto 中的 Wifi6FrameType 保持一致。 // 帧格式:[4字节LE: msg_type][4字节LE: payload_len][payload_len字节: payload] enum class MsgType : uint32_t { @@ -23,6 +24,18 @@ enum class MsgType : uint32_t CONTROL_GET_READINESS_RSP = 12, CONTROL_EVALUATION_REQ = 13, CONTROL_EVALUATION_RSP = 14, + + // 传感器域 + SENSOR_GET_READINESS_REQ = 21, + SENSOR_GET_READINESS_RSP = 22, + SENSOR_GET_LATEST_FRAME_REQ = 23, + SENSOR_GET_LATEST_FRAME_RSP = 24, + SENSOR_LIST_SENSORS_REQ = 25, + SENSOR_LIST_SENSORS_RSP = 26, + + // 外部真值位姿域:车间工控机/真值系统通过 WiFi6 推给车端 + EXTERNAL_POSE_PUSH_REQ = 31, + EXTERNAL_POSE_PUSH_RSP = 32, }; // 向 fd 发送一帧。payload 可以为空(长度字段写 0)。 diff --git a/agv_calib_brain/src/win_ubuntu_bridge/vehicle_agent_gateway/include/vehicle_agent_gateway/tcp_client.hpp b/agv_calib_brain/src/communication/win_ubuntu_bridge/vehicle_agent_gateway/include/vehicle_agent_gateway/tcp_client.hpp similarity index 100% rename from agv_calib_brain/src/win_ubuntu_bridge/vehicle_agent_gateway/include/vehicle_agent_gateway/tcp_client.hpp rename to agv_calib_brain/src/communication/win_ubuntu_bridge/vehicle_agent_gateway/include/vehicle_agent_gateway/tcp_client.hpp diff --git a/agv_calib_brain/src/communication/win_ubuntu_bridge/vehicle_agent_gateway/launch/vehicle_agent_gateway.launch.py b/agv_calib_brain/src/communication/win_ubuntu_bridge/vehicle_agent_gateway/launch/vehicle_agent_gateway.launch.py new file mode 100644 index 0000000..b9cadd6 --- /dev/null +++ b/agv_calib_brain/src/communication/win_ubuntu_bridge/vehicle_agent_gateway/launch/vehicle_agent_gateway.launch.py @@ -0,0 +1,30 @@ +from launch import LaunchDescription +from launch.actions import DeclareLaunchArgument +from launch.substitutions import LaunchConfiguration +from launch_ros.actions import Node +from launch_ros.parameter_descriptions import ParameterValue + + +def generate_launch_description(): + return LaunchDescription([ + DeclareLaunchArgument("chassis_host", default_value="192.168.1.100"), + DeclareLaunchArgument("chassis_port", default_value="9000"), + DeclareLaunchArgument("chassis_timeout_ms", default_value="5000"), + DeclareLaunchArgument("control_host", default_value="192.168.1.100"), + DeclareLaunchArgument("control_port", default_value="9000"), + DeclareLaunchArgument("control_timeout_ms", default_value="5000"), + Node( + package="vehicle_agent_gateway", + executable="vehicle_agent_gateway_node", + name="vehicle_agent_gateway", + output="screen", + parameters=[{ + "chassis_host": LaunchConfiguration("chassis_host"), + "chassis_port": ParameterValue(LaunchConfiguration("chassis_port"), value_type=int), + "chassis_timeout_ms": ParameterValue(LaunchConfiguration("chassis_timeout_ms"), value_type=int), + "control_host": LaunchConfiguration("control_host"), + "control_port": ParameterValue(LaunchConfiguration("control_port"), value_type=int), + "control_timeout_ms": ParameterValue(LaunchConfiguration("control_timeout_ms"), value_type=int), + }], + ), + ]) diff --git a/agv_calib_brain/src/win_ubuntu_bridge/vehicle_agent_gateway/package.xml b/agv_calib_brain/src/communication/win_ubuntu_bridge/vehicle_agent_gateway/package.xml similarity index 100% rename from agv_calib_brain/src/win_ubuntu_bridge/vehicle_agent_gateway/package.xml rename to agv_calib_brain/src/communication/win_ubuntu_bridge/vehicle_agent_gateway/package.xml diff --git a/agv_calib_brain/src/win_ubuntu_bridge/vehicle_agent_gateway/scripts/windows_stub_server.py b/agv_calib_brain/src/communication/win_ubuntu_bridge/vehicle_agent_gateway/scripts/windows_stub_server.py similarity index 59% rename from agv_calib_brain/src/win_ubuntu_bridge/vehicle_agent_gateway/scripts/windows_stub_server.py rename to agv_calib_brain/src/communication/win_ubuntu_bridge/vehicle_agent_gateway/scripts/windows_stub_server.py index c29db14..99458a6 100644 --- a/agv_calib_brain/src/win_ubuntu_bridge/vehicle_agent_gateway/scripts/windows_stub_server.py +++ b/agv_calib_brain/src/communication/win_ubuntu_bridge/vehicle_agent_gateway/scripts/windows_stub_server.py @@ -5,6 +5,8 @@ # 默认监听: # 9001 端口 —— 底盘域 # 9002 端口 —— 运控域 +# 9003 端口 —— 传感器域 +# 9004 端口 —— 外部真值位姿输入 import json import socket @@ -15,7 +17,7 @@ import logging logging.basicConfig(level=logging.INFO, format="[%(threadName)s] %(message)s") -# 帧类型(与 proto_frame.hpp 保持一致) +# 帧类型(与 proto_frame.hpp 和 proto/transport_contract.proto 保持一致) CHASSIS_GET_READINESS_REQ = 1 CHASSIS_GET_READINESS_RSP = 2 CHASSIS_MOTION_PRIMITIVE_REQ = 3 @@ -26,6 +28,14 @@ CONTROL_GET_READINESS_REQ = 11 CONTROL_GET_READINESS_RSP = 12 CONTROL_EVALUATION_REQ = 13 CONTROL_EVALUATION_RSP = 14 +SENSOR_GET_READINESS_REQ = 21 +SENSOR_GET_READINESS_RSP = 22 +SENSOR_GET_LATEST_FRAME_REQ = 23 +SENSOR_GET_LATEST_FRAME_RSP = 24 +SENSOR_LIST_SENSORS_REQ = 25 +SENSOR_LIST_SENSORS_RSP = 26 +EXTERNAL_POSE_PUSH_REQ = 31 +EXTERNAL_POSE_PUSH_RSP = 32 # REQ → RSP 类型映射 REQ_TO_RSP = { @@ -34,6 +44,10 @@ REQ_TO_RSP = { CHASSIS_EMERGENCY_BRAKE_REQ: CHASSIS_EMERGENCY_BRAKE_RSP, CONTROL_GET_READINESS_REQ: CONTROL_GET_READINESS_RSP, CONTROL_EVALUATION_REQ: CONTROL_EVALUATION_RSP, + SENSOR_GET_READINESS_REQ: SENSOR_GET_READINESS_RSP, + SENSOR_GET_LATEST_FRAME_REQ: SENSOR_GET_LATEST_FRAME_RSP, + SENSOR_LIST_SENSORS_REQ: SENSOR_LIST_SENSORS_RSP, + EXTERNAL_POSE_PUSH_REQ: EXTERNAL_POSE_PUSH_RSP, } @@ -97,6 +111,11 @@ def _make_payload(req_type: int, req_payload: dict) -> dict: "estop_released": True, "vehicle_safe_to_move": True, "checked_timestamp_us": _ts_us(), + "external_pose_feedback_ready": True, + "external_pose_source_name": "stub_external_truth", + "external_pose_age_ms": 10.0, + "external_pose_quality_score": 1.0, + "external_pose_transport": "wifi6_stub_tcp", } elif req_type == CONTROL_EVALUATION_REQ: job_id = req_payload.get("header", {}).get("request_id", "stub_job") @@ -121,6 +140,71 @@ def _make_payload(req_type: int, req_payload: dict) -> dict: }, "artifacts": [], } + elif req_type == SENSOR_GET_READINESS_REQ: + return { + "success": True, + "error_code": {"code": 1}, + "message": "Stub: 传感器数据流就绪。", + "agent_ready": True, + "capture_pipeline_ready": True, + "storage_ready": True, + "telemetry_ready": True, + "vehicle_safe_to_move": True, + "arm_ready": True, + "ready_sensor_ids": ["demo_front_camera", "demo_down_camera", "demo_lidar_3d", "demo_lidar_2d", "demo_imu"], + "issues": [], + "checked_timestamp_us": _ts_us(), + "sensor_data_stream_ready": True, + "streaming_sensor_ids": ["demo_front_camera", "demo_down_camera", "demo_lidar_3d", "demo_lidar_2d", "demo_imu"], + "sensor_data_transport": "wifi6_stub_tcp", + } + elif req_type == SENSOR_LIST_SENSORS_REQ: + return { + "success": True, + "error_code": {"code": 1}, + "message": "Stub: 传感器列表。", + "sensors": [ + {"sensor_id": "demo_front_camera", "sensor_type": 2, "payload_type": 1, "topic": "/sensor/front_camera/image_raw", "frame_id": "front_camera_link"}, + {"sensor_id": "demo_down_camera", "sensor_type": 1, "payload_type": 1, "topic": "/sensor/down_camera/image_raw", "frame_id": "down_camera_link"}, + {"sensor_id": "demo_lidar_3d", "sensor_type": 4, "payload_type": 2, "topic": "/sensor/lidar_3d/pointcloud", "frame_id": "lidar_3d_link"}, + {"sensor_id": "demo_lidar_2d", "sensor_type": 5, "payload_type": 3, "topic": "/sensor/lidar_2d/scan", "frame_id": "lidar_2d_link"}, + {"sensor_id": "demo_imu", "sensor_type": 6, "payload_type": 4, "topic": "/sensor/imu/data", "frame_id": "imu_link"}, + ], + } + elif req_type == SENSOR_GET_LATEST_FRAME_REQ: + sensor_id = req_payload.get("sensor_id", "demo_front_camera") + return { + "success": True, + "error_code": {"code": 1}, + "message": "Stub: 最新传感器帧。", + "sensor_frame": { + "header": { + "sensor_id": sensor_id, + "sensor_type": 2, + "frame_id": "front_camera_link", + "hardware_timestamp_us": _ts_us(), + "sequence_id": 1, + "active_job_id": "", + "capture_intent": 0, + }, + "payload_type": 1, + "stream_id": f"{sensor_id}:1", + "fragment_index": 0, + "total_fragments": 1, + "is_final_fragment": True, + "uncompressed_size_bytes": 0, + "payload_digest": "", + "image": {"width": 0, "height": 0, "encoding": "stub", "is_bigendian": False, "step": 0, "data": "", "compression": "none"}, + }, + } + elif req_type == EXTERNAL_POSE_PUSH_REQ: + return { + "success": True, + "error_code": {"code": 1}, + "message": "Stub: 外部真值位姿已接收。", + "accepted_timestamp_us": _ts_us(), + "reference_source_name": req_payload.get("reference_source_name", "stub_external_truth"), + } return {} @@ -182,8 +266,16 @@ if __name__ == "__main__": t_control = threading.Thread( target=start_server, args=(9002, "control"), name="control-srv", daemon=True ) + t_sensor = threading.Thread( + target=start_server, args=(9003, "sensor"), name="sensor-srv", daemon=True + ) + t_external_pose = threading.Thread( + target=start_server, args=(9004, "external-pose"), name="external-pose-srv", daemon=True + ) t_chassis.start() t_control.start() + t_sensor.start() + t_external_pose.start() logging.info("Windows stub server 已启动,按 Ctrl+C 退出。") try: t_chassis.join() diff --git a/agv_calib_brain/src/win_ubuntu_bridge/vehicle_agent_gateway/src/chassis_bridge_node.cpp b/agv_calib_brain/src/communication/win_ubuntu_bridge/vehicle_agent_gateway/src/chassis_bridge_node.cpp similarity index 97% rename from agv_calib_brain/src/win_ubuntu_bridge/vehicle_agent_gateway/src/chassis_bridge_node.cpp rename to agv_calib_brain/src/communication/win_ubuntu_bridge/vehicle_agent_gateway/src/chassis_bridge_node.cpp index 3160311..b792f8a 100644 --- a/agv_calib_brain/src/win_ubuntu_bridge/vehicle_agent_gateway/src/chassis_bridge_node.cpp +++ b/agv_calib_brain/src/communication/win_ubuntu_bridge/vehicle_agent_gateway/src/chassis_bridge_node.cpp @@ -16,9 +16,9 @@ using calibration_common_interfaces::msg::JobState; ChassisBridgeNode::ChassisBridgeNode(const rclcpp::NodeOptions & options) : Node("chassis_bridge_node", options) { - // 节点参数:Windows 车端 IP 与端口,方便通过 launch 文件或命令行覆盖。 + // 节点参数:Windows 车端统一 WiFi6 gateway IP 与端口,方便通过 launch 文件或命令行覆盖。 chassis_host_ = declare_parameter("chassis_host", "192.168.1.100"); - chassis_port_ = static_cast(declare_parameter("chassis_port", 9001)); + chassis_port_ = static_cast(declare_parameter("chassis_port", 9000)); timeout_ms_ = declare_parameter("chassis_timeout_ms", 5000); readiness_service_ = create_service( diff --git a/agv_calib_brain/src/win_ubuntu_bridge/vehicle_agent_gateway/src/control_bridge_node.cpp b/agv_calib_brain/src/communication/win_ubuntu_bridge/vehicle_agent_gateway/src/control_bridge_node.cpp similarity index 97% rename from agv_calib_brain/src/win_ubuntu_bridge/vehicle_agent_gateway/src/control_bridge_node.cpp rename to agv_calib_brain/src/communication/win_ubuntu_bridge/vehicle_agent_gateway/src/control_bridge_node.cpp index 450e8f8..e399d2c 100644 --- a/agv_calib_brain/src/win_ubuntu_bridge/vehicle_agent_gateway/src/control_bridge_node.cpp +++ b/agv_calib_brain/src/communication/win_ubuntu_bridge/vehicle_agent_gateway/src/control_bridge_node.cpp @@ -17,9 +17,9 @@ using calibration_common_interfaces::msg::JobState; ControlBridgeNode::ControlBridgeNode(const rclcpp::NodeOptions & options) : Node("control_bridge_node", options) { - // 节点参数:Windows 车端 IP 与端口,方便通过 launch 文件或命令行覆盖。 + // 节点参数:Windows 车端统一 WiFi6 gateway IP 与端口,方便通过 launch 文件或命令行覆盖。 control_host_ = declare_parameter("control_host", "192.168.1.100"); - control_port_ = static_cast(declare_parameter("control_port", 9002)); + control_port_ = static_cast(declare_parameter("control_port", 9000)); timeout_ms_ = declare_parameter("control_timeout_ms", 5000); readiness_service_ = create_service( diff --git a/agv_calib_brain/src/win_ubuntu_bridge/vehicle_agent_gateway/src/main.cpp b/agv_calib_brain/src/communication/win_ubuntu_bridge/vehicle_agent_gateway/src/main.cpp similarity index 100% rename from agv_calib_brain/src/win_ubuntu_bridge/vehicle_agent_gateway/src/main.cpp rename to agv_calib_brain/src/communication/win_ubuntu_bridge/vehicle_agent_gateway/src/main.cpp diff --git a/agv_calib_brain/src/win_ubuntu_bridge/vehicle_agent_gateway/src/proto_frame.cpp b/agv_calib_brain/src/communication/win_ubuntu_bridge/vehicle_agent_gateway/src/proto_frame.cpp similarity index 100% rename from agv_calib_brain/src/win_ubuntu_bridge/vehicle_agent_gateway/src/proto_frame.cpp rename to agv_calib_brain/src/communication/win_ubuntu_bridge/vehicle_agent_gateway/src/proto_frame.cpp diff --git a/agv_calib_brain/src/win_ubuntu_bridge/vehicle_agent_gateway/src/tcp_client.cpp b/agv_calib_brain/src/communication/win_ubuntu_bridge/vehicle_agent_gateway/src/tcp_client.cpp similarity index 100% rename from agv_calib_brain/src/win_ubuntu_bridge/vehicle_agent_gateway/src/tcp_client.cpp rename to agv_calib_brain/src/communication/win_ubuntu_bridge/vehicle_agent_gateway/src/tcp_client.cpp diff --git a/agv_calib_brain/src/win_ubuntu_bridge/win_ubuntu_bridge/CMakeLists.txt b/agv_calib_brain/src/communication/win_ubuntu_bridge/win_ubuntu_bridge/CMakeLists.txt similarity index 100% rename from agv_calib_brain/src/win_ubuntu_bridge/win_ubuntu_bridge/CMakeLists.txt rename to agv_calib_brain/src/communication/win_ubuntu_bridge/win_ubuntu_bridge/CMakeLists.txt diff --git a/agv_calib_brain/src/win_ubuntu_bridge/win_ubuntu_bridge/docs/images/sequence_diagram.png b/agv_calib_brain/src/communication/win_ubuntu_bridge/win_ubuntu_bridge/docs/images/sequence_diagram.png similarity index 100% rename from agv_calib_brain/src/win_ubuntu_bridge/win_ubuntu_bridge/docs/images/sequence_diagram.png rename to agv_calib_brain/src/communication/win_ubuntu_bridge/win_ubuntu_bridge/docs/images/sequence_diagram.png diff --git a/agv_calib_brain/src/win_ubuntu_bridge/win_ubuntu_bridge/docs/proto 驱动任务选择图.png b/agv_calib_brain/src/communication/win_ubuntu_bridge/win_ubuntu_bridge/docs/proto 驱动任务选择图.png similarity index 100% rename from agv_calib_brain/src/win_ubuntu_bridge/win_ubuntu_bridge/docs/proto 驱动任务选择图.png rename to agv_calib_brain/src/communication/win_ubuntu_bridge/win_ubuntu_bridge/docs/proto 驱动任务选择图.png diff --git a/agv_calib_brain/src/win_ubuntu_bridge/win_ubuntu_bridge/docs/传感器标定详细流程图.png b/agv_calib_brain/src/communication/win_ubuntu_bridge/win_ubuntu_bridge/docs/传感器标定详细流程图.png similarity index 100% rename from agv_calib_brain/src/win_ubuntu_bridge/win_ubuntu_bridge/docs/传感器标定详细流程图.png rename to agv_calib_brain/src/communication/win_ubuntu_bridge/win_ubuntu_bridge/docs/传感器标定详细流程图.png diff --git a/agv_calib_brain/src/win_ubuntu_bridge/win_ubuntu_bridge/docs/底盘标定详细流程图.png b/agv_calib_brain/src/communication/win_ubuntu_bridge/win_ubuntu_bridge/docs/底盘标定详细流程图.png similarity index 100% rename from agv_calib_brain/src/win_ubuntu_bridge/win_ubuntu_bridge/docs/底盘标定详细流程图.png rename to agv_calib_brain/src/communication/win_ubuntu_bridge/win_ubuntu_bridge/docs/底盘标定详细流程图.png diff --git a/agv_calib_brain/src/win_ubuntu_bridge/win_ubuntu_bridge/docs/自动化标定车间总控完整流程图.png b/agv_calib_brain/src/communication/win_ubuntu_bridge/win_ubuntu_bridge/docs/自动化标定车间总控完整流程图.png similarity index 100% rename from agv_calib_brain/src/win_ubuntu_bridge/win_ubuntu_bridge/docs/自动化标定车间总控完整流程图.png rename to agv_calib_brain/src/communication/win_ubuntu_bridge/win_ubuntu_bridge/docs/自动化标定车间总控完整流程图.png diff --git a/agv_calib_brain/src/win_ubuntu_bridge/win_ubuntu_bridge/docs/运控参数调优详细流程图.png b/agv_calib_brain/src/communication/win_ubuntu_bridge/win_ubuntu_bridge/docs/运控参数调优详细流程图.png similarity index 100% rename from agv_calib_brain/src/win_ubuntu_bridge/win_ubuntu_bridge/docs/运控参数调优详细流程图.png rename to agv_calib_brain/src/communication/win_ubuntu_bridge/win_ubuntu_bridge/docs/运控参数调优详细流程图.png diff --git a/agv_calib_brain/src/win_ubuntu_bridge/win_ubuntu_bridge/launch/minimal_workshop_demo.launch.py b/agv_calib_brain/src/communication/win_ubuntu_bridge/win_ubuntu_bridge/launch/minimal_workshop_demo.launch.py similarity index 54% rename from agv_calib_brain/src/win_ubuntu_bridge/win_ubuntu_bridge/launch/minimal_workshop_demo.launch.py rename to agv_calib_brain/src/communication/win_ubuntu_bridge/win_ubuntu_bridge/launch/minimal_workshop_demo.launch.py index 5efa949..b01f61f 100644 --- a/agv_calib_brain/src/win_ubuntu_bridge/win_ubuntu_bridge/launch/minimal_workshop_demo.launch.py +++ b/agv_calib_brain/src/communication/win_ubuntu_bridge/win_ubuntu_bridge/launch/minimal_workshop_demo.launch.py @@ -8,6 +8,9 @@ from launch_ros.actions import Node # 参数: # use_gateway (bool, 默认 false):true 时用 vehicle_agent_gateway 替换本地底盘/运控 stub 节点。 # chassis_host / control_host:Windows 车端 IP(仅 use_gateway=true 时生效)。 +# profile_storage_path:vehicle_profile_manager 的画像持久化文件。 +# sensor_storage_root / sensor_registry / capture_pipeline_name / telemetry_topics: +# 传感器 readiness 的最小真实配置入口。 def generate_launch_description(): use_gateway_arg = DeclareLaunchArgument( "use_gateway", @@ -22,11 +25,53 @@ def generate_launch_description(): "control_host", default_value="192.168.1.100", description="Windows 车端 IP(use_gateway=true 时生效)", ) + profile_storage_path_arg = DeclareLaunchArgument( + "profile_storage_path", + default_value="/tmp/agv_calib_vehicle_profiles.db", + description="vehicle_profile_manager 的画像持久化文件路径", + ) + sensor_storage_root_arg = DeclareLaunchArgument( + "sensor_storage_root", + default_value="/tmp/agv_sensor_calibration", + description="sensor_calibration_service 的产物目录", + ) + sensor_registry_arg = DeclareLaunchArgument( + "sensor_registry", + default_value="demo_front_camera", + description="逗号分隔的可用传感器 ID 列表", + ) + capture_pipeline_name_arg = DeclareLaunchArgument( + "capture_pipeline_name", + default_value="sensor_capture_pipeline", + description="传感器采集链路名称", + ) + telemetry_topics_arg = DeclareLaunchArgument( + "telemetry_topics", + default_value="/sensor_calibration/telemetry", + description="逗号分隔的传感器遥测 topic 列表", + ) + vehicle_safe_to_move_arg = DeclareLaunchArgument( + "vehicle_safe_to_move", + default_value="true", + description="传感器任务若需要移动车辆时,当前是否允许移动", + ) + arm_ready_arg = DeclareLaunchArgument( + "arm_ready", + default_value="true", + description="当前机械臂是否就绪", + ) def make_nodes(context): use_gateway = LaunchConfiguration("use_gateway").perform(context).lower() == "true" chassis_host = LaunchConfiguration("chassis_host").perform(context) control_host = LaunchConfiguration("control_host").perform(context) + profile_storage_path = LaunchConfiguration("profile_storage_path").perform(context) + sensor_storage_root = LaunchConfiguration("sensor_storage_root").perform(context) + sensor_registry = LaunchConfiguration("sensor_registry").perform(context) + capture_pipeline_name = LaunchConfiguration("capture_pipeline_name").perform(context) + telemetry_topics = LaunchConfiguration("telemetry_topics").perform(context) + vehicle_safe_to_move = LaunchConfiguration("vehicle_safe_to_move").perform(context).lower() == "true" + arm_ready = LaunchConfiguration("arm_ready").perform(context).lower() == "true" common_nodes = [ Node( @@ -34,6 +79,9 @@ def generate_launch_description(): executable="vehicle_profile_manager_node", name="vehicle_profile_manager", output="screen", + parameters=[{ + "profile_storage_path": profile_storage_path, + }], ), Node( package="external_localization_service", @@ -46,6 +94,14 @@ def generate_launch_description(): executable="sensor_calibration_service_node", name="sensor_calibration_service", output="screen", + parameters=[{ + "sensor_storage_root": sensor_storage_root, + "sensor_registry": sensor_registry, + "capture_pipeline_name": capture_pipeline_name, + "telemetry_topics": telemetry_topics, + "vehicle_safe_to_move": vehicle_safe_to_move, + "arm_ready": arm_ready, + }], ), Node( package="workshop_orchestrator_v2", @@ -56,7 +112,7 @@ def generate_launch_description(): ] if use_gateway: - # WiFi 网关模式:底盘和运控通过 TCP 转发到 Windows 车端。 + # WiFi 网关模式:底盘和运控通过统一 TCP 端口转发到 Windows 车端。 chassis_control_nodes = [ Node( package="vehicle_agent_gateway", @@ -65,10 +121,10 @@ def generate_launch_description(): output="screen", parameters=[{ "chassis_host": chassis_host, - "chassis_port": 9001, + "chassis_port": 9000, "chassis_timeout_ms": 5000, "control_host": control_host, - "control_port": 9002, + "control_port": 9000, "control_timeout_ms": 5000, }], ), @@ -96,5 +152,12 @@ def generate_launch_description(): use_gateway_arg, chassis_host_arg, control_host_arg, + profile_storage_path_arg, + sensor_storage_root_arg, + sensor_registry_arg, + capture_pipeline_name_arg, + telemetry_topics_arg, + vehicle_safe_to_move_arg, + arm_ready_arg, OpaqueFunction(function=make_nodes), ]) diff --git a/agv_calib_brain/src/win_ubuntu_bridge/win_ubuntu_bridge/package.xml b/agv_calib_brain/src/communication/win_ubuntu_bridge/win_ubuntu_bridge/package.xml similarity index 100% rename from agv_calib_brain/src/win_ubuntu_bridge/win_ubuntu_bridge/package.xml rename to agv_calib_brain/src/communication/win_ubuntu_bridge/win_ubuntu_bridge/package.xml diff --git a/agv_calib_brain/src/win_ubuntu_bridge/win_ubuntu_bridge/proto/README.md b/agv_calib_brain/src/communication/win_ubuntu_bridge/win_ubuntu_bridge/proto/README.md similarity index 87% rename from agv_calib_brain/src/win_ubuntu_bridge/win_ubuntu_bridge/proto/README.md rename to agv_calib_brain/src/communication/win_ubuntu_bridge/win_ubuntu_bridge/proto/README.md index 0999a49..075ad26 100644 --- a/agv_calib_brain/src/win_ubuntu_bridge/win_ubuntu_bridge/proto/README.md +++ b/agv_calib_brain/src/communication/win_ubuntu_bridge/win_ubuntu_bridge/proto/README.md @@ -1,10 +1,10 @@ -# 自动化标定车间 Proto 协同工作说明 +# 自动化标定车间协议协同工作说明 -[![Protocol](https://img.shields.io/badge/Protocol-Protocol%20Buffers-blue)](https://developers.google.com/protocol-buffers) -[![Platform](https://img.shields.io/badge/Platform-Ubuntu%20%7C%20Linux%20%7C%20Windows-green)](https://ubuntu.com) -[![Status](https://img.shields.io/badge/Status-Proto%20Frozen%20Candidate-orange)](./) +> **核心定位**:面向自动化标定车间的分层协议设计,用 8 个 `.proto` 把“车间总控、车辆画像、真值参考、底盘标定、传感器标定、运控调优、参数归档、WiFi6 传输契约”串成一条可执行、可追溯、可扩展的自动化流程。 -> **核心定位**:面向自动化标定车间的分层协议设计,用 7 个 `.proto` 把“车间总控、车辆画像、真值参考、底盘标定、传感器标定、运控调优、参数归档”串成一条可执行、可追溯、可扩展的自动化流程。 +协议格式:Protocol Buffers +目标平台:Ubuntu / Linux / Windows +当前状态:Proto 冻结候选版 --- @@ -12,10 +12,10 @@ - [1. 文档目的](#1-文档目的) - [2. 系统边界与角色分工](#2-系统边界与角色分工) -- [3. 七个 Proto 文件的职责划分](#3-七个-proto-文件的职责划分) -- [4. 七个 Proto 的依赖关系](#4-七个-proto-的依赖关系) +- [3. 八个 Proto 文件的职责划分](#3-八个-proto-文件的职责划分) +- [4. 八个 Proto 的依赖关系](#4-八个-proto-的依赖关系) - [5. 自动化标定车间完整流程](#5-自动化标定车间完整流程) -- [6. 七个 Proto 在主流程中的协同方式](#6-七个-proto-在主流程中的协同方式) +- [6. 八个 Proto 在主流程中的协同方式](#6-八个-proto-在主流程中的协同方式) - [7. 当前协议设计已经明确的关键原则](#7-当前协议设计已经明确的关键原则) - [8. 冻结前建议补齐的小修补项](#8-冻结前建议补齐的小修补项) - [9. 从 Proto 到 ROS2 的映射建议](#9-从-proto-到-ros2-的映射建议) @@ -26,13 +26,13 @@ ## 1. 文档目的 -本文档用于说明当前自动化标定车间中 **7 个核心 `.proto` 文件** 的职责边界、依赖关系、协同流程和工程落地方式。 +本文档用于说明当前自动化标定车间中 **8 个核心 `.proto` 文件** 的职责边界、依赖关系、协同流程和工程落地方式。 这套协议不是为了把所有逻辑堆到一个服务里,而是为了把整个系统拆成清晰的几层: - **车间总控层**:只负责编排、状态推进、审批、回滚、归档 - **专业域服务层**:分别处理真值参考、底盘、传感器、运控调优 -- **车端代理层**:只负责动作执行、数据采集、参数写入、状态回传 +- **车端代理层**:只负责动作执行、传感器订阅与数据采集、参数写入、状态回传 - **公共协议层**:统一请求头、错误码、心跳、长任务状态、文件引用、位姿/向量类型 这份 README 的目标是让后续做 ROS2 映射、节点架构设计、C++ 服务实现时,所有人对协议语义有同一个理解。 @@ -74,12 +74,13 @@ - **Linux 负责求解和编排,不负责直接驱动硬件细节** - **Windows 负责执行和采集,不负责复杂求解与全局决策** +- **车端传感器属于车辆侧,图像、点云、LaserScan、IMU 原始帧经 WiFi6 从车端转发到车间电脑** - **所有阶段都围绕同一个 `session_id`、`vehicle_id`、`job_id` 协同** - **所有长任务都统一使用 `JobAccepted / JobStatus / JobResult` 语义** --- -## 3. 七个 Proto 文件的职责划分 +## 3. 八个 Proto 文件的职责划分 ### 3.1 calibration_common.proto @@ -96,7 +97,7 @@ - `HeartbeatRequest / HeartbeatResponse`:统一保活机制 - `AgentReadinessRequest / ReadinessIssue`:统一 readiness 检查模式 - `FileDigest / FileReference`:统一文件摘要与文件引用 -- `Vector3D / Vector3d / Pose3D / Pose3dEuler`:统一三维向量与位姿表达 +- `Vector3D / Pose3D`:统一三维向量与位姿表达 - `KeyValuePair`:为后续扩展保留元数据入口 **一句话理解**:它不描述任何专项算法,但所有专项服务都要先站在这块地基上。 @@ -272,6 +273,8 @@ - 传感器到 `base_link` 外参任务 - 手眼标定任务 - 遥测流 +- 车端传感器原始数据流 `StreamVehicleSensorData` +- 车间电脑传感器数据接收服务 `AgvCalibWorkshopSensorIngestService` - 参数写入与查询 - 任务结果与验证摘要 @@ -279,13 +282,14 @@ - 已支持相机 **只做内参 / 只做外参 / 内外参都做** - 已将 IMU 内参改为三轴强约束: - - `Vector3d accel_bias` - - `Vector3d gyro_bias` + - `Vector3D accel_bias` + - `Vector3D gyro_bias` - 已增加 `LASER_SCAN_FILE`,单独表达 2D 激光雷达产物 +- 已增加 `VehicleSensorFrame`,用于承载相机图像、3D 点云、2D LaserScan、IMU 原始帧,并支持大图像 / 点云分片传输 - 已增加 `SensorValidationSummary` - 已增加 `SensorCalibrationJobResult` -**一句话理解**:它负责把“采什么、怎么采、产出什么参数、怎么判断是否通过”描述完整。 +**一句话理解**:它负责把“采什么、怎么采、原始数据怎么从车端到车间电脑、产出什么参数、怎么判断是否通过”描述完整。 --- @@ -320,7 +324,23 @@ --- -## 4. 七个 Proto 的依赖关系 +### 3.8 transport_contract.proto + +**作用:车间工控机与车端电脑之间的 WiFi6/TCP 传输契约。** + +它不替代底盘、运控、传感器、外部真值的业务消息,而是固定网络边界上的通道和帧规则: + +- `Wifi6Channel`:底盘、运控、传感器、外部真值位姿四个逻辑通道 +- `Wifi6DefaultPort`:WiFi6 边界默认使用统一车端 gateway 端口 9000,9001-9004 只作为仿真内部域端口或旧版兼容 +- `Wifi6FrameType`:当前 TCP 帧号 1-32 +- `Wifi6TcpFrameHeader`:8 字节小端帧头的契约表达 +- `Wifi6PayloadEncoding`:当前 JSON 字段名编码与后续 protobuf binary 编码预留 + +**一句话理解**:它把“哪些数据通过 WiFi6 怎么走”固定下来,避免 Windows 车端和 Ubuntu 工控机只靠代码常量私下约定。 + +--- + +## 4. 八个 Proto 的依赖关系 依赖关系可以概括如下: @@ -333,6 +353,7 @@ | `control_calibration.proto` | `calibration_common.proto`、`vehicle_profile.proto` | 运控调优执行、遥测、验证、参数写入 | | `sensor_calibration.proto` | `calibration_common.proto`、`vehicle_profile.proto` | 传感器任务、采集、参数、验证 | | `workshop_orchestration.proto` | `calibration_common.proto`、`vehicle_profile.proto` | 整场会话编排、计划、审批、回滚、报告 | +| `transport_contract.proto` | 无 | WiFi6/TCP 通道、端口、帧头、帧号、payload 编码 | 总体依赖方向是: @@ -344,6 +365,7 @@ flowchart LR A --> E[control_calibration.proto] A --> F[sensor_calibration.proto] A --> G[workshop_orchestration.proto] + H[transport_contract.proto] B --> D B --> E B --> F @@ -458,7 +480,7 @@ flowchart TB --- -## 6. 七个 Proto 在主流程中的协同方式 +## 6. 八个 Proto 在主流程中的协同方式 ### 6.1 协同主图 @@ -555,7 +577,7 @@ Windows 代理只负责: ## 8. 冻结前建议补齐的小修补项 -当前 7 个 proto 的主结构已经可以冻结,但为了减少后续返工,建议在冻结前补 4 类小修补。 +当前 8 个 proto 的主结构已经可以冻结,但为了减少后续返工,建议在冻结前补 4 类小修补。 ### 8.1 给计划 / 结果 / 报告补统一 `metadata` @@ -620,10 +642,11 @@ message CalibrationReferenceTarget { - `RequestHeader` - `JobStatus` -- `Pose3dEuler` +- `Pose3D` - `ChassisTelemetry` - `ControlTelemetry` - `SensorCalibrationTelemetry` +- `VehicleSensorFrame` ### 9.2 srv @@ -682,7 +705,8 @@ proto/ ├── chassis_calibration.proto ├── control_calibration.proto ├── sensor_calibration.proto -└── workshop_orchestration.proto +├── workshop_orchestration.proto +└── transport_contract.proto services/ ├── workshop_orchestrator/ @@ -708,7 +732,7 @@ logs/ 建议按下面顺序推进: -1. 先冻结 7 个 proto +1. 先冻结 8 个 proto 2. 再设计 ROS2 的 msg / srv / action 对应关系 3. 再画节点架构图和 topic / service / action 拓扑 4. 再实现总控状态机 @@ -728,7 +752,7 @@ logs/ ## 11. 总结 -当前这套 7 个 proto 已经不再是“概念方案”,而是一套接近可冻结的自动化标定车间协议主干。 +当前这套 8 个 proto 已经不再是“概念方案”,而是一套接近可冻结的自动化标定车间协议主干。 它们共同支撑的不是单个算法模块,而是完整的一条自动化流程: @@ -743,7 +767,8 @@ logs/ - `control_calibration.proto` 负责横纵向控制器调优执行 - `sensor_calibration.proto` 负责多传感器采集、求解与参数表达 - `workshop_orchestration.proto` 负责总流程编排、审批、回滚与归档 +- `transport_contract.proto` 固定 WiFi6/TCP 通道、端口、帧头、帧号和 payload 编码 只要把冻结前的小修补项补齐,这套协议就可以正式进入下一阶段: -**ROS2 接口映射、节点架构设计、C++ 服务实现。** \ No newline at end of file +**ROS2 接口映射、节点架构设计、C++ 服务实现。** diff --git a/agv_calib_brain/src/win_ubuntu_bridge/win_ubuntu_bridge/proto/calibration_common.proto b/agv_calib_brain/src/communication/win_ubuntu_bridge/win_ubuntu_bridge/proto/calibration_common.proto similarity index 99% rename from agv_calib_brain/src/win_ubuntu_bridge/win_ubuntu_bridge/proto/calibration_common.proto rename to agv_calib_brain/src/communication/win_ubuntu_bridge/win_ubuntu_bridge/proto/calibration_common.proto index 4d44308..0013f90 100644 --- a/agv_calib_brain/src/win_ubuntu_bridge/win_ubuntu_bridge/proto/calibration_common.proto +++ b/agv_calib_brain/src/communication/win_ubuntu_bridge/win_ubuntu_bridge/proto/calibration_common.proto @@ -3,8 +3,8 @@ syntax = "proto3"; package agv.calibration.common; // ├── msg/ -// │ ├── Vector3d.msg -// │ ├── Pose3dEuler.msg +// │ ├── Vector3D.msg +// │ ├── Pose3D.msg // │ ├── RequestHeader.msg // │ ├── ErrorCode.msg // │ ├── StandardResponse.msg @@ -263,4 +263,4 @@ message FileReference { message KeyValuePair { string key = 1; // 键 string value = 2; // 值 -} \ No newline at end of file +} diff --git a/agv_calib_brain/src/win_ubuntu_bridge/win_ubuntu_bridge/proto/chassis_calibration.proto b/agv_calib_brain/src/communication/win_ubuntu_bridge/win_ubuntu_bridge/proto/chassis_calibration.proto similarity index 100% rename from agv_calib_brain/src/win_ubuntu_bridge/win_ubuntu_bridge/proto/chassis_calibration.proto rename to agv_calib_brain/src/communication/win_ubuntu_bridge/win_ubuntu_bridge/proto/chassis_calibration.proto diff --git a/agv_calib_brain/src/win_ubuntu_bridge/calibration_control_interfaces/proto/control_calibration.proto b/agv_calib_brain/src/communication/win_ubuntu_bridge/win_ubuntu_bridge/proto/control_calibration.proto similarity index 93% rename from agv_calib_brain/src/win_ubuntu_bridge/calibration_control_interfaces/proto/control_calibration.proto rename to agv_calib_brain/src/communication/win_ubuntu_bridge/win_ubuntu_bridge/proto/control_calibration.proto index 61a04c4..307e21b 100644 --- a/agv_calib_brain/src/win_ubuntu_bridge/calibration_control_interfaces/proto/control_calibration.proto +++ b/agv_calib_brain/src/communication/win_ubuntu_bridge/win_ubuntu_bridge/proto/control_calibration.proto @@ -115,6 +115,12 @@ message ControlReadinessResponse { bool vehicle_safe_to_move = 9; // 当前车辆是否允许移动 repeated .agv.calibration.common.ReadinessIssue issues = 10; // 不满足项 int64 checked_timestamp_us = 11; // 检查时间 + + bool external_pose_feedback_ready = 12; // 外部真值 / 定位位姿输入是否就绪 + string external_pose_source_name = 13; // 当前外部位姿源名称 + double external_pose_age_ms = 14; // 最近一帧外部位姿年龄 + double external_pose_quality_score = 15; // 最近一帧外部位姿质量分数 + string external_pose_transport = 16; // 外部位姿传输方式,例如 wifi6 } // ========================================================= @@ -249,6 +255,13 @@ message TrajectoryTrackingTask { repeated TrajectoryPoint path = 1; // 轨迹点序列 bool stop_at_end = 2; // 结束后是否停车 double timeout_sec = 3; // 超时时间 + string required_external_pose_source_id = 4; // 要求使用的外部位姿源 ID + double max_external_pose_age_ms = 5; // 允许最大外部位姿延迟 + double min_external_pose_quality_score = 6; // 允许最小外部位姿质量分数 + string trajectory_id = 7; // 轨迹 ID,用于分段下发时关联多段轨迹 + uint32 segment_index = 8; // 当前轨迹段序号,从 0 开始 + uint32 total_segments = 9; // 总段数;0 或 1 表示本请求携带完整轨迹 + bool is_final_segment = 10; // 当前段是否为最后一段,作为 total_segments 的冗余校验 } // ========================================================= @@ -353,6 +366,9 @@ message ControlTelemetry { string parameter_version = 15; // 当前生效参数版本 .agv.calibration.vehicle.profile.ControllerAlgorithmType active_lateral_algorithm = 16; // 当前横向算法 .agv.calibration.vehicle.profile.ControllerAlgorithmType active_longitudinal_algorithm = 17; // 当前纵向算法 + + string pose_source_name = 18; // 本帧控制使用的位姿源名称 + bool pose_from_external_truth = 19; // 本帧位姿是否来自外部真值 / 外部定位 } // ========================================================= @@ -420,4 +436,4 @@ message ControlJobResult { ControlValidationSummary validation_summary = 8; // 验证摘要 ControllerParameterSet estimated_parameter_set = 9; // 本轮估计参数集 repeated .agv.calibration.common.FileReference artifacts = 10; // 关联产物 -} \ No newline at end of file +} diff --git a/agv_calib_brain/src/win_ubuntu_bridge/calibration_external_localization_interfaces/proto/external_localization.proto b/agv_calib_brain/src/communication/win_ubuntu_bridge/win_ubuntu_bridge/proto/external_localization.proto similarity index 87% rename from agv_calib_brain/src/win_ubuntu_bridge/calibration_external_localization_interfaces/proto/external_localization.proto rename to agv_calib_brain/src/communication/win_ubuntu_bridge/win_ubuntu_bridge/proto/external_localization.proto index 35749b7..069bb81 100644 --- a/agv_calib_brain/src/win_ubuntu_bridge/calibration_external_localization_interfaces/proto/external_localization.proto +++ b/agv_calib_brain/src/communication/win_ubuntu_bridge/win_ubuntu_bridge/proto/external_localization.proto @@ -69,6 +69,30 @@ service AgvCalibExternalLocalizationService { returns (.agv.calibration.common.StandardResponse); } +// ========================================================= +// 服务:外部真值位姿下发服务 +// 运行位置:Ubuntu 车间电脑 / 外部真值系统桥接进程 +// 调用方:Windows 车端代理 +// 传输介质:现场通过 WiFi 6 网络承载 +// 作用: +// 1) 让车端在执行轨迹跟踪时获得车间坐标系下的实时位姿 +// 2) 与 StreamExternalLocalizationTelemetry 使用同一位姿消息结构 +// 3) 车端不得把底盘里程计当成默认全局位姿源,只可作为降级输入 +// ========================================================= +service AgvCalibExternalPoseFeedService { + // 心跳保活,用于车端确认外部真值链路在线 + rpc Heartbeat(.agv.calibration.common.HeartbeatRequest) + returns (.agv.calibration.common.HeartbeatResponse); + + // 车端订阅外部真值位姿流 + rpc StreamExternalPoseToVehicle(ExternalPoseFeedRequest) + returns (stream ExternalLocalizationTelemetry); + + // 可选:外部真值系统主动推送单帧位姿到车端 + rpc PushExternalPoseToVehicle(ExternalLocalizationTelemetry) + returns (.agv.calibration.common.StandardResponse); +} + // ========================================================= // 外部定位工作模式请求 // 作用:切换外部定位桥接服务到不同模式 @@ -238,6 +262,20 @@ message StreamExternalLocalizationTelemetryRequest { bool include_tracking_state = 6; // 是否包含跟踪状态 } +// ========================================================= +// 外部真值位姿下发请求 +// 作用:车端通过 WiFi 6 请求工控机 / 外部真值桥接进程提供实时位姿流 +// ========================================================= +message ExternalPoseFeedRequest { + .agv.calibration.common.RequestHeader header = 1; // 请求头 + string vehicle_id = 2; // 目标车辆 ID + string localization_source_id = 3; // 外部真值源 ID + string workcell_zone_id = 4; // 工位 / 区域 ID + uint32 expected_hz = 5; // 期望下发频率 + double max_pose_age_ms = 6; // 允许最大位姿延迟 + bool require_quality_metrics = 7; // 是否要求质量指标 +} + // ========================================================= // 外部定位遥测 // 作用:回传外部定位实时观测结果 @@ -248,7 +286,7 @@ message ExternalLocalizationTelemetry { int64 hardware_timestamp_us = 1; // 硬件时间戳 bool pose_valid = 2; // 位姿是否有效 - .agv.calibration.common.Pose3dEuler workshop_pose = 3; // 在车间参考系下的位姿 + .agv.calibration.common.Pose3D workshop_pose = 3; // 在车间参考系下的位姿 double position_stddev_m = 4; // 位置标准差 double yaw_stddev_rad = 5; // 航向标准差 @@ -269,7 +307,7 @@ message ExternalLocalizationCalibrationResult { string workshop_frame_id = 1; // 车间参考坐标系 ID string localization_frame_id = 2; // 外部定位坐标系 ID - .agv.calibration.common.Pose3dEuler workshop_to_localization = 3; // 车间系到定位系的位姿 + .agv.calibration.common.Pose3D workshop_to_localization = 3; // 车间系到定位系的位姿 double position_repeatability_m = 4; // 位置重复性 double yaw_repeatability_rad = 5; // 航向重复性 @@ -334,4 +372,4 @@ message ExternalLocalizationJobResult { ExternalLocalizationValidationSummary validation_summary = 9; // 真值参考验证摘要 ExternalLocalizationCalibrationResult result = 10; // 结果内容 repeated .agv.calibration.common.FileReference artifacts = 11; // 关联产物 -} \ No newline at end of file +} diff --git a/agv_calib_brain/src/win_ubuntu_bridge/calibration_sensor_interfaces/proto/sensor_calibration.proto b/agv_calib_brain/src/communication/win_ubuntu_bridge/win_ubuntu_bridge/proto/sensor_calibration.proto similarity index 70% rename from agv_calib_brain/src/win_ubuntu_bridge/calibration_sensor_interfaces/proto/sensor_calibration.proto rename to agv_calib_brain/src/communication/win_ubuntu_bridge/win_ubuntu_bridge/proto/sensor_calibration.proto index 38d0096..a121b55 100644 --- a/agv_calib_brain/src/win_ubuntu_bridge/calibration_sensor_interfaces/proto/sensor_calibration.proto +++ b/agv_calib_brain/src/communication/win_ubuntu_bridge/win_ubuntu_bridge/proto/sensor_calibration.proto @@ -11,7 +11,7 @@ import "vehicle_profile.proto"; // Ubuntu 车间电脑(Linux) -> Windows 车端代理 / 采集服务 // 说明: // 1) Linux 负责图像 / 点云 / 激光扫描 / IMU 数据分析与参数求解 -// 2) 车端 / 采集服务只负责组织采集、回传观测状态、写入结果 +// 2) 车端 / 采集服务负责订阅车端传感器、组织采集、回传原始数据和观测状态、写入结果 // 3) 支持相机内参、IMU 内参、传感器外参、手眼标定 // 4) 支持相机“只做内参 / 只做外参 / 内外参都做” // ========================================================= @@ -58,6 +58,10 @@ service AgvCalibSensorService { rpc StreamSensorCalibrationTelemetry(StreamSensorCalibrationTelemetryRequest) returns (stream SensorCalibrationTelemetry); + // 打开车端传感器原始数据流;用于 WiFi6 把车载相机、雷达、IMU 数据传给车间电脑 + rpc StreamVehicleSensorData(StreamVehicleSensorDataRequest) + returns (stream VehicleSensorFrame); + // 写入传感器标定参数 rpc CommitSensorCalibrationParameters(CommitSensorCalibrationParametersRequest) returns (.agv.calibration.common.StandardResponse); @@ -71,6 +75,22 @@ service AgvCalibSensorService { returns (.agv.calibration.common.StandardResponse); } +// ========================================================= +// 服务:车间电脑传感器数据接收服务 +// 运行位置:Ubuntu 车间电脑 +// 调用方:Windows 车端代理 / 仿真车端代理 +// 说明: +// 1) 当现场网络策略采用车端主动上报时使用 +// 2) 与 StreamVehicleSensorData 使用同一帧结构 +// ========================================================= +service AgvCalibWorkshopSensorIngestService { + rpc Heartbeat(.agv.calibration.common.HeartbeatRequest) + returns (.agv.calibration.common.HeartbeatResponse); + + rpc PushVehicleSensorFrame(VehicleSensorFrame) + returns (.agv.calibration.common.StandardResponse); +} + // ========================================================= // 传感器标定工作模式 // 作用:切换采集服务到不同模式 @@ -109,6 +129,10 @@ message SensorReadinessResponse { repeated string ready_sensor_ids = 10; // 已就绪的传感器 ID repeated .agv.calibration.common.ReadinessIssue issues = 11; // 不满足项 int64 checked_timestamp_us = 12; // 检查时间 + + bool sensor_data_stream_ready = 13; // 车端原始传感器数据流是否就绪 + repeated string streaming_sensor_ids = 14; // 当前可被转发到车间电脑的传感器 ID + string sensor_data_transport = 15; // 传输方式,例如 wifi6 / wifi6_sim_tcp } // ========================================================= @@ -137,6 +161,9 @@ message SensorCalibrationCapabilityResponse { bool supports_2d_lidar_extrinsic = 9; // 是否支持 2D 激光雷达外参 bool supports_camera_only_intrinsic = 10; // 是否支持仅做相机内参 bool supports_camera_only_extrinsic = 11; // 是否支持仅做相机外参 + bool supports_vehicle_sensor_data_stream = 12; // 是否支持车端原始传感器数据流 + bool supports_sensor_frame_chunking = 13; // 是否支持大帧分片传输 + bool supports_compressed_image_stream = 14; // 是否支持压缩图像流 } // ========================================================= @@ -238,8 +265,8 @@ enum SensorCalibrationTaskSubtype { IMU_EXTRINSIC = 6; LIDAR_2D_EXTRINSIC = 7; LIDAR_3D_EXTRINSIC = 8; - EYE_IN_HAND = 9; - EYE_TO_HAND = 10; + HAND_EYE_IN_HAND = 9; + HAND_EYE_TO_HAND = 10; } // ========================================================= @@ -300,6 +327,161 @@ message SensorCalibrationTelemetry { CaptureIntentType capture_intent = 9; // 当前采集意图 } +// ========================================================= +// 车端传感器原始数据流请求 +// 作用:车间电脑指定要接收哪些车端传感器数据 +// ========================================================= +message StreamVehicleSensorDataRequest { + .agv.calibration.common.RequestHeader header = 1; // 请求头 + repeated string sensor_ids = 2; // 要接收的传感器 ID;为空表示由任务决定 + repeated .agv.calibration.vehicle.profile.SensorType sensor_types = 3; // 要接收的传感器类型 + uint32 expected_hz = 4; // 期望上报频率,0 表示车端默认 + uint32 max_frame_payload_bytes = 5; // 单个分片最大 payload,0 表示车端默认 + bool allow_compressed = 6; // 是否允许压缩图像 / 点云 + bool include_camera_info = 7; // 是否随图像携带相机内参初值 + CaptureIntentType capture_intent = 8; // 数据采集意图 + string active_job_id = 9; // 关联的标定任务 ID +} + +// ========================================================= +// 传感器原始帧负载类型 +// ========================================================= +enum SensorFramePayloadType { + SENSOR_FRAME_PAYLOAD_TYPE_UNSPECIFIED = 0; + IMAGE_FRAME = 1; // 相机图像 + POINT_CLOUD_FRAME = 2; // 3D 激光雷达点云 + LASER_SCAN_FRAME = 3; // 2D 激光雷达扫描 + IMU_FRAME = 4; // IMU 数据 + CAMERA_INFO_FRAME = 5; // 相机内参 / camera_info + RAW_BYTES_FRAME = 6; // 预留给厂商私有二进制格式 +} + +// ========================================================= +// 传感器原始帧公共头 +// ========================================================= +message VehicleSensorFrameHeader { + string sensor_id = 1; // 传感器 ID,例如 demo_front_camera + .agv.calibration.vehicle.profile.SensorType sensor_type = 2; // 传感器类型 + string frame_id = 3; // ROS / 车端坐标系 frame_id + int64 hardware_timestamp_us = 4; // 硬件时间戳 + uint64 sequence_id = 5; // 单传感器递增帧号 + string active_job_id = 6; // 关联任务 ID + CaptureIntentType capture_intent = 7; // 采集意图 +} + +// ========================================================= +// 图像帧 +// 对齐 ROS sensor_msgs/Image 的核心字段 +// ========================================================= +message VehicleImageFrame { + uint32 width = 1; // 图像宽度 + uint32 height = 2; // 图像高度 + string encoding = 3; // 例如 rgb8 / bgr8 / mono8 / jpeg / png + bool is_bigendian = 4; // 字节序 + uint32 step = 5; // 每行字节数 + bytes data = 6; // 图像数据或分片数据 + string compression = 7; // none / jpeg / png + CameraIntrinsics camera_intrinsics_hint = 8; // 可选内参初值或当前参数 +} + +// ========================================================= +// PointCloud2 字段描述 +// ========================================================= +enum PointCloudFieldDataType { + POINT_CLOUD_FIELD_DATA_TYPE_UNSPECIFIED = 0; + POINT_CLOUD_FIELD_INT8 = 1; + POINT_CLOUD_FIELD_UINT8 = 2; + POINT_CLOUD_FIELD_INT16 = 3; + POINT_CLOUD_FIELD_UINT16 = 4; + POINT_CLOUD_FIELD_INT32 = 5; + POINT_CLOUD_FIELD_UINT32 = 6; + POINT_CLOUD_FIELD_FLOAT32 = 7; + POINT_CLOUD_FIELD_FLOAT64 = 8; +} + +message PointCloudField { + string name = 1; // 字段名,例如 x / y / z / intensity + uint32 offset = 2; // 字节偏移 + PointCloudFieldDataType datatype = 3; // 数据类型 + uint32 count = 4; // 元素数量 +} + +// ========================================================= +// 3D 点云帧 +// 对齐 ROS sensor_msgs/PointCloud2 的核心字段 +// ========================================================= +message VehiclePointCloudFrame { + uint32 height = 1; + uint32 width = 2; + repeated PointCloudField fields = 3; + bool is_bigendian = 4; + uint32 point_step = 5; + uint32 row_step = 6; + bytes data = 7; // 点云数据或分片数据 + bool is_dense = 8; + string compression = 9; // none / zstd / lz4 / vendor +} + +// ========================================================= +// 2D 激光雷达扫描帧 +// 对齐 ROS sensor_msgs/LaserScan 的核心字段 +// ========================================================= +message VehicleLaserScanFrame { + double angle_min_rad = 1; + double angle_max_rad = 2; + double angle_increment_rad = 3; + double time_increment_sec = 4; + double scan_time_sec = 5; + double range_min_m = 6; + double range_max_m = 7; + repeated float ranges_m = 8; + repeated float intensities = 9; +} + +// ========================================================= +// IMU 帧 +// 对齐 ROS sensor_msgs/Imu 的核心字段 +// ========================================================= +message VehicleImuFrame { + double orientation_x = 1; + double orientation_y = 2; + double orientation_z = 3; + double orientation_w = 4; + repeated double orientation_covariance = 5; + .agv.calibration.common.Vector3D angular_velocity = 6; + repeated double angular_velocity_covariance = 7; + .agv.calibration.common.Vector3D linear_acceleration = 8; + repeated double linear_acceleration_covariance = 9; +} + +// ========================================================= +// 车端传感器原始帧 +// 作用:通过 WiFi6 在车端和车间电脑之间传输相机 / 雷达 / IMU 数据 +// 说明: +// 1) fragment_index / total_fragments 用于大图像或点云分片 +// 2) total_fragments 为 0 或 1 表示本消息携带完整帧 +// 3) oneof 中的数据字段可携带完整数据,也可携带当前分片数据 +// ========================================================= +message VehicleSensorFrame { + VehicleSensorFrameHeader header = 1; // 公共帧头 + SensorFramePayloadType payload_type = 2; // 负载类型 + string stream_id = 3; // 数据流 ID + uint32 fragment_index = 4; // 当前分片序号,从 0 开始 + uint32 total_fragments = 5; // 总分片数;0 或 1 表示完整帧 + bool is_final_fragment = 6; // 当前是否最后一个分片 + uint32 uncompressed_size_bytes = 7; // 解压后总大小,用于校验 + string payload_digest = 8; // 完整帧摘要,例如 sha256:... + + oneof payload { + VehicleImageFrame image = 20; + VehiclePointCloudFrame point_cloud = 21; + VehicleLaserScanFrame laser_scan = 22; + VehicleImuFrame imu = 23; + CameraIntrinsics camera_info = 24; + bytes raw_payload = 25; + } +} + // ========================================================= // 相机模型类型 // 作用:表达相机内参使用的模型 @@ -335,8 +517,8 @@ message CameraIntrinsics { // 3) 采用强约束三维向量,不再使用 repeated double // ========================================================= message IMUIntrinsics { - .agv.calibration.common.Vector3d accel_bias = 1; // 加速度计偏置 - .agv.calibration.common.Vector3d gyro_bias = 2; // 陀螺仪偏置 + .agv.calibration.common.Vector3D accel_bias = 1; // 加速度计偏置 + .agv.calibration.common.Vector3D gyro_bias = 2; // 陀螺仪偏置 } // ========================================================= @@ -347,7 +529,7 @@ message IMUIntrinsics { message SensorExtrinsics { string parent_frame_id = 1; // 父坐标系,一般为 base_link string child_frame_id = 2; // 子坐标系,一般为 sensor frame - .agv.calibration.common.Pose3dEuler parent_to_child = 3; // 父到子的位姿 + .agv.calibration.common.Pose3D parent_to_child = 3; // 父到子的位姿 } // ========================================================= @@ -461,4 +643,4 @@ message SensorCalibrationJobResult { SensorValidationSummary validation_summary = 8; // 验证摘要 SensorCalibrationParameterSet estimated_params = 9; // 本轮估计参数 repeated CalibrationArtifact artifacts = 10; // 关联产物 -} \ No newline at end of file +} diff --git a/agv_calib_brain/src/communication/win_ubuntu_bridge/win_ubuntu_bridge/proto/transport_contract.proto b/agv_calib_brain/src/communication/win_ubuntu_bridge/win_ubuntu_bridge/proto/transport_contract.proto new file mode 100644 index 0000000..8cb6376 --- /dev/null +++ b/agv_calib_brain/src/communication/win_ubuntu_bridge/win_ubuntu_bridge/proto/transport_contract.proto @@ -0,0 +1,146 @@ +syntax = "proto3"; + +package agv.calibration.transport; + +// ========================================================= +// 文件作用:WiFi6/TCP 传输契约 +// 使用范围: +// 1) Ubuntu 车间工控机与 Windows 车端代理之间的网络边界 +// 2) 仿真车端 agent 与真实车端 agent 需要共同遵守的帧号和通道约定 +// 3) 业务消息仍由 chassis/control/sensor/external_localization proto 定义 +// 说明: +// 1) WiFi6 是承载网络,当前工程传输层使用 TCP 长/短连接 +// 2) 当前仿真实现 payload 使用 proto 字段名风格的 JSON +// 3) 现场可切换为 protobuf binary,但必须保持本文件中的通道和帧号不变 +// ========================================================= + +// ========================================================= +// WiFi6 逻辑通道 +// ========================================================= +enum Wifi6Channel { + WIFI6_CHANNEL_UNSPECIFIED = 0; + WIFI6_CHANNEL_CHASSIS = 1; // 底盘标定动作和急停 + WIFI6_CHANNEL_CONTROL = 2; // 运控参数评估、轨迹跟踪 + WIFI6_CHANNEL_SENSOR = 3; // 车端传感器原始数据 + WIFI6_CHANNEL_EXTERNAL_POSE = 4; // 外部真值位姿输入到车端 +} + +// ========================================================= +// 默认端口 +// 说明: +// 1) WiFi6 边界默认只暴露一个车端 gateway 端口 +// 2) channel 只是协议里的逻辑通道,不应该被理解成“一类功能一个物理端口” +// 3) 9001-9004 仅保留给仿真 router 背后的内部域服务或旧版兼容使用 +// ========================================================= +enum Wifi6DefaultPort { + WIFI6_DEFAULT_PORT_UNSPECIFIED = 0; + WIFI6_DEFAULT_PORT_VEHICLE_GATEWAY = 9000; + WIFI6_LEGACY_INTERNAL_PORT_CHASSIS = 9001; + WIFI6_LEGACY_INTERNAL_PORT_CONTROL = 9002; + WIFI6_LEGACY_INTERNAL_PORT_SENSOR = 9003; + WIFI6_LEGACY_INTERNAL_PORT_EXTERNAL_POSE = 9004; +} + +// ========================================================= +// TCP 载荷编码方式 +// ========================================================= +enum Wifi6PayloadEncoding { + WIFI6_PAYLOAD_ENCODING_UNSPECIFIED = 0; + WIFI6_PAYLOAD_ENCODING_JSON_PROTO_FIELD_NAMES = 1; // 当前仿真实现:JSON key 使用 proto 字段名 + WIFI6_PAYLOAD_ENCODING_PROTOBUF_BINARY = 2; // 现场高性能实现可选 +} + +// ========================================================= +// TCP 连接方向 +// ========================================================= +enum Wifi6FrameDirection { + WIFI6_FRAME_DIRECTION_UNSPECIFIED = 0; + WIFI6_FRAME_DIRECTION_WORKSHOP_TO_VEHICLE = 1; + WIFI6_FRAME_DIRECTION_VEHICLE_TO_WORKSHOP = 2; +} + +// ========================================================= +// TCP 帧号 +// 帧头格式固定为: +// uint32 little-endian msg_type +// uint32 little-endian payload_len +// payload_len bytes payload +// 注意: +// 1) 这个 8 字节帧头不是 protobuf 序列化结果,而是传输层二进制头 +// 2) payload 的业务结构由本字段注释中对应的 proto 消息定义 +// 3) 逻辑 channel 由 msg_type 映射得到;同一个 gateway 端口根据 msg_type 做路由 +// ========================================================= +enum Wifi6FrameType { + WIFI6_FRAME_TYPE_UNSPECIFIED = 0; + + // 底盘域:chassis_calibration.proto / AgvCalibChassisService + WIFI6_FRAME_CHASSIS_GET_READINESS_REQ = 1; // AgentReadinessRequest + WIFI6_FRAME_CHASSIS_GET_READINESS_RSP = 2; // ChassisReadinessResponse + WIFI6_FRAME_CHASSIS_MOTION_PRIMITIVE_REQ = 3; // MotionPrimitiveRequest + WIFI6_FRAME_CHASSIS_MOTION_PRIMITIVE_RSP = 4; // ChassisJobResult + WIFI6_FRAME_CHASSIS_EMERGENCY_BRAKE_REQ = 5; // Empty 或 EmergencyBrake 请求 + WIFI6_FRAME_CHASSIS_EMERGENCY_BRAKE_RSP = 6; // StandardResponse + + // 运控域:control_calibration.proto / AgvCalibControlService + WIFI6_FRAME_CONTROL_GET_READINESS_REQ = 11; // AgentReadinessRequest + WIFI6_FRAME_CONTROL_GET_READINESS_RSP = 12; // ControlReadinessResponse + WIFI6_FRAME_CONTROL_EVALUATION_REQ = 13; // ControllerEvaluationRequest + WIFI6_FRAME_CONTROL_EVALUATION_RSP = 14; // ControlJobResult + + // 传感器域:sensor_calibration.proto / AgvCalibSensorService + WIFI6_FRAME_SENSOR_GET_READINESS_REQ = 21; // AgentReadinessRequest + WIFI6_FRAME_SENSOR_GET_READINESS_RSP = 22; // SensorReadinessResponse + WIFI6_FRAME_SENSOR_GET_LATEST_FRAME_REQ = 23; // StreamVehicleSensorDataRequest 的轻量轮询形态 + WIFI6_FRAME_SENSOR_GET_LATEST_FRAME_RSP = 24; // VehicleSensorFrame + WIFI6_FRAME_SENSOR_LIST_SENSORS_REQ = 25; // Empty 或能力查询请求 + WIFI6_FRAME_SENSOR_LIST_SENSORS_RSP = 26; // Sensor 列表响应 + + // 外部真值位姿域:external_localization.proto / AgvCalibExternalPoseFeedService + WIFI6_FRAME_EXTERNAL_POSE_PUSH_REQ = 31; // ExternalLocalizationTelemetry + WIFI6_FRAME_EXTERNAL_POSE_PUSH_RSP = 32; // StandardResponse +} + +// ========================================================= +// TCP 帧头的 protobuf 表达 +// 说明: +// 1) 这个 message 仅用于文档、测试、代码生成时表达契约 +// 2) 实际线上帧头仍是上面注释中定义的 8 字节小端二进制结构 +// ========================================================= +message Wifi6TcpFrameHeader { + Wifi6FrameType msg_type = 1; // 对应 8 字节帧头中的 uint32 msg_type + uint32 payload_len = 2; // 对应 8 字节帧头中的 uint32 payload_len +} + +// ========================================================= +// 逻辑通道端点 +// ========================================================= +message Wifi6ChannelEndpoint { + Wifi6Channel channel = 1; + Wifi6DefaultPort default_port = 2; + Wifi6FrameDirection request_direction = 3; + string canonical_proto_file = 4; // 例如 chassis_calibration.proto + string canonical_service = 5; // 例如 AgvCalibChassisService +} + +// ========================================================= +// 请求 / 响应帧映射 +// ========================================================= +message Wifi6FrameMapping { + Wifi6Channel channel = 1; + Wifi6FrameType request_type = 2; + Wifi6FrameType response_type = 3; + Wifi6PayloadEncoding payload_encoding = 4; + string request_message = 5; // proto 消息名 + string response_message = 6; // proto 消息名 + string rpc_name = 7; // 对齐的 RPC 名称;轻量轮询可为空 +} + +// ========================================================= +// 当前工程默认契约版本 +// ========================================================= +message Wifi6TransportContract { + string contract_version = 1; // 当前为 "wifi6_tcp_v1" + Wifi6PayloadEncoding default_payload_encoding = 2; + repeated Wifi6ChannelEndpoint endpoints = 3; + repeated Wifi6FrameMapping frame_mappings = 4; +} diff --git a/agv_calib_brain/src/win_ubuntu_bridge/calibration_vehicle_profile_interfaces/proto/vehicle_profile.proto b/agv_calib_brain/src/communication/win_ubuntu_bridge/win_ubuntu_bridge/proto/vehicle_profile.proto similarity index 99% rename from agv_calib_brain/src/win_ubuntu_bridge/calibration_vehicle_profile_interfaces/proto/vehicle_profile.proto rename to agv_calib_brain/src/communication/win_ubuntu_bridge/win_ubuntu_bridge/proto/vehicle_profile.proto index 678fccd..a396702 100644 --- a/agv_calib_brain/src/win_ubuntu_bridge/calibration_vehicle_profile_interfaces/proto/vehicle_profile.proto +++ b/agv_calib_brain/src/communication/win_ubuntu_bridge/win_ubuntu_bridge/proto/vehicle_profile.proto @@ -352,4 +352,4 @@ message VehicleCalibrationApplicabilityResponse { repeated ApplicabilityIssue issues = 5; // 问题列表 bool overall_supported = 6; // 是否总体可开展标定 repeated WorkflowStageType recommended_workflow_stages = 7; // 推荐执行阶段 -} \ No newline at end of file +} diff --git a/agv_calib_brain/src/win_ubuntu_bridge/win_ubuntu_bridge/proto/workshop_orchestration.proto b/agv_calib_brain/src/communication/win_ubuntu_bridge/win_ubuntu_bridge/proto/workshop_orchestration.proto similarity index 100% rename from agv_calib_brain/src/win_ubuntu_bridge/win_ubuntu_bridge/proto/workshop_orchestration.proto rename to agv_calib_brain/src/communication/win_ubuntu_bridge/win_ubuntu_bridge/proto/workshop_orchestration.proto diff --git a/agv_calib_brain/src/agv_calib_core/chassis_calibration_service/CMakeLists.txt b/agv_calib_brain/src/core/agv_calib_core/chassis_calibration_service/CMakeLists.txt similarity index 100% rename from agv_calib_brain/src/agv_calib_core/chassis_calibration_service/CMakeLists.txt rename to agv_calib_brain/src/core/agv_calib_core/chassis_calibration_service/CMakeLists.txt diff --git a/agv_calib_brain/src/agv_calib_core/chassis_calibration_service/include/chassis_calibration_service/chassis_calibration_algorithm_template.hpp b/agv_calib_brain/src/core/agv_calib_core/chassis_calibration_service/include/chassis_calibration_service/chassis_calibration_algorithm_template.hpp similarity index 100% rename from agv_calib_brain/src/agv_calib_core/chassis_calibration_service/include/chassis_calibration_service/chassis_calibration_algorithm_template.hpp rename to agv_calib_brain/src/core/agv_calib_core/chassis_calibration_service/include/chassis_calibration_service/chassis_calibration_algorithm_template.hpp diff --git a/agv_calib_brain/src/agv_calib_core/chassis_calibration_service/include/chassis_calibration_service/chassis_calibration_algorithms.hpp b/agv_calib_brain/src/core/agv_calib_core/chassis_calibration_service/include/chassis_calibration_service/chassis_calibration_algorithms.hpp similarity index 100% rename from agv_calib_brain/src/agv_calib_core/chassis_calibration_service/include/chassis_calibration_service/chassis_calibration_algorithms.hpp rename to agv_calib_brain/src/core/agv_calib_core/chassis_calibration_service/include/chassis_calibration_service/chassis_calibration_algorithms.hpp diff --git a/agv_calib_brain/src/agv_calib_core/chassis_calibration_service/include/chassis_calibration_service/chassis_calibration_common.hpp b/agv_calib_brain/src/core/agv_calib_core/chassis_calibration_service/include/chassis_calibration_service/chassis_calibration_common.hpp similarity index 100% rename from agv_calib_brain/src/agv_calib_core/chassis_calibration_service/include/chassis_calibration_service/chassis_calibration_common.hpp rename to agv_calib_brain/src/core/agv_calib_core/chassis_calibration_service/include/chassis_calibration_service/chassis_calibration_common.hpp diff --git a/agv_calib_brain/src/agv_calib_core/chassis_calibration_service/include/chassis_calibration_service/chassis_calibration_service_node.hpp b/agv_calib_brain/src/core/agv_calib_core/chassis_calibration_service/include/chassis_calibration_service/chassis_calibration_service_node.hpp similarity index 100% rename from agv_calib_brain/src/agv_calib_core/chassis_calibration_service/include/chassis_calibration_service/chassis_calibration_service_node.hpp rename to agv_calib_brain/src/core/agv_calib_core/chassis_calibration_service/include/chassis_calibration_service/chassis_calibration_service_node.hpp diff --git a/agv_calib_brain/src/agv_calib_core/chassis_calibration_service/launch/chassis_calibration_service_component.launch.py b/agv_calib_brain/src/core/agv_calib_core/chassis_calibration_service/launch/chassis_calibration_service_component.launch.py similarity index 100% rename from agv_calib_brain/src/agv_calib_core/chassis_calibration_service/launch/chassis_calibration_service_component.launch.py rename to agv_calib_brain/src/core/agv_calib_core/chassis_calibration_service/launch/chassis_calibration_service_component.launch.py diff --git a/agv_calib_brain/src/agv_calib_core/chassis_calibration_service/package.xml b/agv_calib_brain/src/core/agv_calib_core/chassis_calibration_service/package.xml similarity index 100% rename from agv_calib_brain/src/agv_calib_core/chassis_calibration_service/package.xml rename to agv_calib_brain/src/core/agv_calib_core/chassis_calibration_service/package.xml diff --git a/agv_calib_brain/src/agv_calib_core/chassis_calibration_service/src/ackermann_chassis_algorithm.cpp b/agv_calib_brain/src/core/agv_calib_core/chassis_calibration_service/src/ackermann_chassis_algorithm.cpp similarity index 100% rename from agv_calib_brain/src/agv_calib_core/chassis_calibration_service/src/ackermann_chassis_algorithm.cpp rename to agv_calib_brain/src/core/agv_calib_core/chassis_calibration_service/src/ackermann_chassis_algorithm.cpp diff --git a/agv_calib_brain/src/agv_calib_core/chassis_calibration_service/src/chassis_calibration_algorithm_template.cpp b/agv_calib_brain/src/core/agv_calib_core/chassis_calibration_service/src/chassis_calibration_algorithm_template.cpp similarity index 100% rename from agv_calib_brain/src/agv_calib_core/chassis_calibration_service/src/chassis_calibration_algorithm_template.cpp rename to agv_calib_brain/src/core/agv_calib_core/chassis_calibration_service/src/chassis_calibration_algorithm_template.cpp diff --git a/agv_calib_brain/src/agv_calib_core/chassis_calibration_service/src/chassis_calibration_algorithms.cpp b/agv_calib_brain/src/core/agv_calib_core/chassis_calibration_service/src/chassis_calibration_algorithms.cpp similarity index 100% rename from agv_calib_brain/src/agv_calib_core/chassis_calibration_service/src/chassis_calibration_algorithms.cpp rename to agv_calib_brain/src/core/agv_calib_core/chassis_calibration_service/src/chassis_calibration_algorithms.cpp diff --git a/agv_calib_brain/src/agv_calib_core/chassis_calibration_service/src/chassis_calibration_service_node.cpp b/agv_calib_brain/src/core/agv_calib_core/chassis_calibration_service/src/chassis_calibration_service_node.cpp similarity index 100% rename from agv_calib_brain/src/agv_calib_core/chassis_calibration_service/src/chassis_calibration_service_node.cpp rename to agv_calib_brain/src/core/agv_calib_core/chassis_calibration_service/src/chassis_calibration_service_node.cpp diff --git a/agv_calib_brain/src/core/agv_calib_core/chassis_calibration_service/src/differential_chassis_algorithm.cpp b/agv_calib_brain/src/core/agv_calib_core/chassis_calibration_service/src/differential_chassis_algorithm.cpp new file mode 100644 index 0000000..e8742d8 --- /dev/null +++ b/agv_calib_brain/src/core/agv_calib_core/chassis_calibration_service/src/differential_chassis_algorithm.cpp @@ -0,0 +1,76 @@ +#include "chassis_calibration_service/chassis_calibration_common.hpp" + +#include "calibration_chassis_interfaces/msg/chassis_specific_params_type.hpp" + +namespace chassis_calibration_service +{ + +bool DifferentialChassisAlgorithm::run( + const ChassisCalibrationInput & input, + ChassisCalibrationOutput & output, + std::string & failure_reason) const +{ + // 差速底盘标定逻辑(轮径与轴距标定模拟): + // 1. 检查底盘反馈数据 + if (input.chassis_telemetry_history.empty()) { + failure_reason = "没有底盘遥测数据,无法标定轮径。"; + return false; + } + + // 2. 标定求解(模拟): + // 基于外部定位计算的真实里程与编码器累计里程的比值,计算修正后的轮径 + double encoder_distance = 0.0; + double truth_distance = 0.0; + size_t valid_points = 0; + + for (const auto & s : input.chassis_telemetry_history) { + if (s.linear_velocity_ms > 0.1) { + encoder_distance += s.linear_velocity_ms * 0.01; // 假设采样周期 10ms + valid_points++; + } + } + + if (valid_points < 100) { + failure_reason = "运动数据点过少 (当前: " + std::to_string(valid_points) + "),无法标定轮径。"; + return false; + } + + // 假设外部真值测得的距离比编码器测得的短 2% (说明标称轮径偏大) + truth_distance = encoder_distance * 0.98; + + // 3. 填充结果 + fill_common_result(input, output); + + auto & res = output.response.result; + res.message = "差速底盘标定成功,已计算轮径修正系数。"; + res.recommended_parameter_version = "diff_chassis_v1.1"; + + auto & params = res.estimated_params; + params.chassis_type.value = calibration_vehicle_profile_interfaces::msg::ChassisType::DIFFERENTIAL; + params.selected_specific_params.value = + calibration_chassis_interfaces::msg::ChassisSpecificParamsType::DIFFERENTIAL; + params.common.has_longitudinal_scale = true; + params.common.longitudinal_scale = truth_distance / encoder_distance; + params.common.has_effective_track_width_m = true; + params.common.effective_track_width_m = 0.65; + params.differential.has_left_wheel_radius_m = true; + params.differential.left_wheel_radius_m = 0.125 * 0.98; // 原始 125mm + params.differential.has_right_wheel_radius_m = true; + params.differential.right_wheel_radius_m = 0.125 * 0.98; + params.differential.has_axle_track_width_m = true; + params.differential.axle_track_width_m = 0.65; // 轮距保持标称值 + + // 4. 评估标定质量 + res.validation_summary.max_lateral_error_m = 0.015; + res.validation_summary.max_yaw_error_rad = 0.005; + res.validation_summary.rms_lateral_error_m = 0.005; + res.validation_summary.rms_yaw_error_rad = 0.002; + res.validation_summary.repeatability_error_m = 0.005; + res.validation_summary.curvature_error = 0.0; + res.validation_summary.module_consistency_error = 0.0; + res.validation_summary.auto_acceptance_passed = true; + + return true; +} + +} // namespace chassis_calibration_service diff --git a/agv_calib_brain/src/agv_calib_core/chassis_calibration_service/src/main.cpp b/agv_calib_brain/src/core/agv_calib_core/chassis_calibration_service/src/main.cpp similarity index 100% rename from agv_calib_brain/src/agv_calib_core/chassis_calibration_service/src/main.cpp rename to agv_calib_brain/src/core/agv_calib_core/chassis_calibration_service/src/main.cpp diff --git a/agv_calib_brain/src/agv_calib_core/chassis_calibration_service/src/multi_steer_wheel_chassis_algorithm.cpp b/agv_calib_brain/src/core/agv_calib_core/chassis_calibration_service/src/multi_steer_wheel_chassis_algorithm.cpp similarity index 100% rename from agv_calib_brain/src/agv_calib_core/chassis_calibration_service/src/multi_steer_wheel_chassis_algorithm.cpp rename to agv_calib_brain/src/core/agv_calib_core/chassis_calibration_service/src/multi_steer_wheel_chassis_algorithm.cpp diff --git a/agv_calib_brain/src/agv_calib_core/chassis_calibration_service/src/single_steer_wheel_chassis_algorithm.cpp b/agv_calib_brain/src/core/agv_calib_core/chassis_calibration_service/src/single_steer_wheel_chassis_algorithm.cpp similarity index 100% rename from agv_calib_brain/src/agv_calib_core/chassis_calibration_service/src/single_steer_wheel_chassis_algorithm.cpp rename to agv_calib_brain/src/core/agv_calib_core/chassis_calibration_service/src/single_steer_wheel_chassis_algorithm.cpp diff --git a/agv_calib_brain/src/agv_calib_core/control_calibration_service/CMakeLists.txt b/agv_calib_brain/src/core/agv_calib_core/control_calibration_service/CMakeLists.txt similarity index 100% rename from agv_calib_brain/src/agv_calib_core/control_calibration_service/CMakeLists.txt rename to agv_calib_brain/src/core/agv_calib_core/control_calibration_service/CMakeLists.txt diff --git a/agv_calib_brain/src/agv_calib_core/control_calibration_service/include/control_calibration_service/control_calibration_algorithm_template.hpp b/agv_calib_brain/src/core/agv_calib_core/control_calibration_service/include/control_calibration_service/control_calibration_algorithm_template.hpp similarity index 100% rename from agv_calib_brain/src/agv_calib_core/control_calibration_service/include/control_calibration_service/control_calibration_algorithm_template.hpp rename to agv_calib_brain/src/core/agv_calib_core/control_calibration_service/include/control_calibration_service/control_calibration_algorithm_template.hpp diff --git a/agv_calib_brain/src/agv_calib_core/control_calibration_service/include/control_calibration_service/control_calibration_algorithms.hpp b/agv_calib_brain/src/core/agv_calib_core/control_calibration_service/include/control_calibration_service/control_calibration_algorithms.hpp similarity index 100% rename from agv_calib_brain/src/agv_calib_core/control_calibration_service/include/control_calibration_service/control_calibration_algorithms.hpp rename to agv_calib_brain/src/core/agv_calib_core/control_calibration_service/include/control_calibration_service/control_calibration_algorithms.hpp diff --git a/agv_calib_brain/src/agv_calib_core/control_calibration_service/include/control_calibration_service/control_calibration_common.hpp b/agv_calib_brain/src/core/agv_calib_core/control_calibration_service/include/control_calibration_service/control_calibration_common.hpp similarity index 100% rename from agv_calib_brain/src/agv_calib_core/control_calibration_service/include/control_calibration_service/control_calibration_common.hpp rename to agv_calib_brain/src/core/agv_calib_core/control_calibration_service/include/control_calibration_service/control_calibration_common.hpp diff --git a/agv_calib_brain/src/agv_calib_core/control_calibration_service/include/control_calibration_service/control_calibration_service_node.hpp b/agv_calib_brain/src/core/agv_calib_core/control_calibration_service/include/control_calibration_service/control_calibration_service_node.hpp similarity index 100% rename from agv_calib_brain/src/agv_calib_core/control_calibration_service/include/control_calibration_service/control_calibration_service_node.hpp rename to agv_calib_brain/src/core/agv_calib_core/control_calibration_service/include/control_calibration_service/control_calibration_service_node.hpp diff --git a/agv_calib_brain/src/agv_calib_core/control_calibration_service/launch/control_calibration_service_component.launch.py b/agv_calib_brain/src/core/agv_calib_core/control_calibration_service/launch/control_calibration_service_component.launch.py similarity index 100% rename from agv_calib_brain/src/agv_calib_core/control_calibration_service/launch/control_calibration_service_component.launch.py rename to agv_calib_brain/src/core/agv_calib_core/control_calibration_service/launch/control_calibration_service_component.launch.py diff --git a/agv_calib_brain/src/agv_calib_core/control_calibration_service/package.xml b/agv_calib_brain/src/core/agv_calib_core/control_calibration_service/package.xml similarity index 100% rename from agv_calib_brain/src/agv_calib_core/control_calibration_service/package.xml rename to agv_calib_brain/src/core/agv_calib_core/control_calibration_service/package.xml diff --git a/agv_calib_brain/src/agv_calib_core/control_calibration_service/src/control_calibration_algorithm_template.cpp b/agv_calib_brain/src/core/agv_calib_core/control_calibration_service/src/control_calibration_algorithm_template.cpp similarity index 100% rename from agv_calib_brain/src/agv_calib_core/control_calibration_service/src/control_calibration_algorithm_template.cpp rename to agv_calib_brain/src/core/agv_calib_core/control_calibration_service/src/control_calibration_algorithm_template.cpp diff --git a/agv_calib_brain/src/agv_calib_core/control_calibration_service/src/control_calibration_service_node.cpp b/agv_calib_brain/src/core/agv_calib_core/control_calibration_service/src/control_calibration_service_node.cpp similarity index 100% rename from agv_calib_brain/src/agv_calib_core/control_calibration_service/src/control_calibration_service_node.cpp rename to agv_calib_brain/src/core/agv_calib_core/control_calibration_service/src/control_calibration_service_node.cpp diff --git a/agv_calib_brain/src/agv_calib_core/control_calibration_service/src/lqr_control_calibration_algorithm.cpp b/agv_calib_brain/src/core/agv_calib_core/control_calibration_service/src/lqr_control_calibration_algorithm.cpp similarity index 100% rename from agv_calib_brain/src/agv_calib_core/control_calibration_service/src/lqr_control_calibration_algorithm.cpp rename to agv_calib_brain/src/core/agv_calib_core/control_calibration_service/src/lqr_control_calibration_algorithm.cpp diff --git a/agv_calib_brain/src/agv_calib_core/control_calibration_service/src/main.cpp b/agv_calib_brain/src/core/agv_calib_core/control_calibration_service/src/main.cpp similarity index 100% rename from agv_calib_brain/src/agv_calib_core/control_calibration_service/src/main.cpp rename to agv_calib_brain/src/core/agv_calib_core/control_calibration_service/src/main.cpp diff --git a/agv_calib_brain/src/agv_calib_core/control_calibration_service/src/mpc_control_calibration_algorithm.cpp b/agv_calib_brain/src/core/agv_calib_core/control_calibration_service/src/mpc_control_calibration_algorithm.cpp similarity index 100% rename from agv_calib_brain/src/agv_calib_core/control_calibration_service/src/mpc_control_calibration_algorithm.cpp rename to agv_calib_brain/src/core/agv_calib_core/control_calibration_service/src/mpc_control_calibration_algorithm.cpp diff --git a/agv_calib_brain/src/agv_calib_core/control_calibration_service/src/pid_control_calibration_algorithm.cpp b/agv_calib_brain/src/core/agv_calib_core/control_calibration_service/src/pid_control_calibration_algorithm.cpp similarity index 100% rename from agv_calib_brain/src/agv_calib_core/control_calibration_service/src/pid_control_calibration_algorithm.cpp rename to agv_calib_brain/src/core/agv_calib_core/control_calibration_service/src/pid_control_calibration_algorithm.cpp diff --git a/agv_calib_brain/src/core/agv_calib_core/control_calibration_service/src/pure_pursuit_control_calibration_algorithm.cpp b/agv_calib_brain/src/core/agv_calib_core/control_calibration_service/src/pure_pursuit_control_calibration_algorithm.cpp new file mode 100644 index 0000000..eceb509 --- /dev/null +++ b/agv_calib_brain/src/core/agv_calib_core/control_calibration_service/src/pure_pursuit_control_calibration_algorithm.cpp @@ -0,0 +1,92 @@ +#include "control_calibration_service/control_calibration_common.hpp" + +#include + +#include "calibration_control_interfaces/msg/controller_parameter_kind.hpp" +#include "calibration_control_interfaces/msg/controller_parameter_pack.hpp" +#include "calibration_vehicle_profile_interfaces/msg/control_axis_type.hpp" + +namespace control_calibration_service +{ + +bool PurePursuitControlCalibrationAlgorithm::run( + const ControlCalibrationInput & input, + ControlCalibrationOutput & output, + std::string & failure_reason) const +{ + // Pure Pursuit 控制标定逻辑(模拟): + // 1. 检查控制反馈数据 + if (input.control_telemetry_history.empty()) { + failure_reason = "没有控制遥测数据,无法评估控制效果。"; + return false; + } + + // 2. 统计评估(模拟): + // 对历史观测中的横向误差进行均方根(RMS)统计 + double sum_sq_lat_err = 0.0; + double sum_sq_heading_err = 0.0; + double sum_sq_speed_err = 0.0; + size_t saturation_points = 0; + size_t valid_points = 0; + + for (const auto & s : input.control_telemetry_history) { + if (s.active_job_id.empty()) { + continue; + } + sum_sq_lat_err += (s.lateral_error_m * s.lateral_error_m); + sum_sq_heading_err += (s.heading_error_rad * s.heading_error_rad); + sum_sq_speed_err += (s.speed_error_ms * s.speed_error_ms); + if (s.saturation_flag) { + saturation_points++; + } + valid_points++; + } + + if (valid_points < 50) { + failure_reason = "有效跟踪点过少 (当前: " + std::to_string(valid_points) + "),无法评估控制质量。"; + return false; + } + + const double inv_points = 1.0 / static_cast(valid_points); + const double rms_lat_err = std::sqrt(sum_sq_lat_err * inv_points); + const double rms_heading_err = std::sqrt(sum_sq_heading_err * inv_points); + const double rms_speed_err = std::sqrt(sum_sq_speed_err * inv_points); + + // 3. 填充结果 + fill_common_result(input, output); + + auto & res = output.response.result; + res.message = "Pure Pursuit 控制标定/评估完成。"; + res.recommended_parameter_version = "pp_params_v2.0"; + + calibration_control_interfaces::msg::ControllerParameterPack params; + params.control_axis.value = + calibration_vehicle_profile_interfaces::msg::ControlAxisType::LATERAL_CONTROL; + params.algorithm_type.value = + calibration_vehicle_profile_interfaces::msg::ControllerAlgorithmType::PURE_PURSUIT; + params.selected_params.value = + calibration_control_interfaces::msg::ControllerParameterKind::PURE_PURSUIT; + params.pure_pursuit.lookahead_m = 1.2; + params.pure_pursuit.has_min_lookahead_m = true; + params.pure_pursuit.min_lookahead_m = 0.5; + params.pure_pursuit.has_max_lookahead_m = true; + params.pure_pursuit.max_lookahead_m = 2.5; + params.pure_pursuit.has_curvature_gain = true; + params.pure_pursuit.curvature_gain = 1.0; + params.pure_pursuit.has_steering_limit_deg = true; + params.pure_pursuit.steering_limit_deg = 32.0; + res.estimated_parameter_set.items.clear(); + res.estimated_parameter_set.items.push_back(params); + + // 4. 评估控制质量 + res.validation_summary.rms_lateral_error_m = rms_lat_err; + res.validation_summary.rms_heading_error_rad = rms_heading_err; + res.validation_summary.rms_speed_error_ms = rms_speed_err; + res.validation_summary.saturation_ratio = + static_cast(saturation_points) / static_cast(valid_points); + res.validation_summary.auto_acceptance_passed = (rms_lat_err < 0.05); // 示例验收标准:5cm RMS + + return true; +} + +} // namespace control_calibration_service diff --git a/agv_calib_brain/src/agv_calib_core/external_localization_service/CMakeLists.txt b/agv_calib_brain/src/core/agv_calib_core/external_localization_service/CMakeLists.txt similarity index 100% rename from agv_calib_brain/src/agv_calib_core/external_localization_service/CMakeLists.txt rename to agv_calib_brain/src/core/agv_calib_core/external_localization_service/CMakeLists.txt diff --git a/agv_calib_brain/src/agv_calib_core/external_localization_service/include/external_localization_service/external_localization_algorithm_template.hpp b/agv_calib_brain/src/core/agv_calib_core/external_localization_service/include/external_localization_service/external_localization_algorithm_template.hpp similarity index 100% rename from agv_calib_brain/src/agv_calib_core/external_localization_service/include/external_localization_service/external_localization_algorithm_template.hpp rename to agv_calib_brain/src/core/agv_calib_core/external_localization_service/include/external_localization_service/external_localization_algorithm_template.hpp diff --git a/agv_calib_brain/src/agv_calib_core/external_localization_service/include/external_localization_service/external_localization_algorithms.hpp b/agv_calib_brain/src/core/agv_calib_core/external_localization_service/include/external_localization_service/external_localization_algorithms.hpp similarity index 100% rename from agv_calib_brain/src/agv_calib_core/external_localization_service/include/external_localization_service/external_localization_algorithms.hpp rename to agv_calib_brain/src/core/agv_calib_core/external_localization_service/include/external_localization_service/external_localization_algorithms.hpp diff --git a/agv_calib_brain/src/agv_calib_core/external_localization_service/include/external_localization_service/external_localization_common.hpp b/agv_calib_brain/src/core/agv_calib_core/external_localization_service/include/external_localization_service/external_localization_common.hpp similarity index 100% rename from agv_calib_brain/src/agv_calib_core/external_localization_service/include/external_localization_service/external_localization_common.hpp rename to agv_calib_brain/src/core/agv_calib_core/external_localization_service/include/external_localization_service/external_localization_common.hpp diff --git a/agv_calib_brain/src/agv_calib_core/external_localization_service/include/external_localization_service/external_localization_service_node.hpp b/agv_calib_brain/src/core/agv_calib_core/external_localization_service/include/external_localization_service/external_localization_service_node.hpp similarity index 52% rename from agv_calib_brain/src/agv_calib_core/external_localization_service/include/external_localization_service/external_localization_service_node.hpp rename to agv_calib_brain/src/core/agv_calib_core/external_localization_service/include/external_localization_service/external_localization_service_node.hpp index 3747d2b..f179cdb 100644 --- a/agv_calib_brain/src/agv_calib_core/external_localization_service/include/external_localization_service/external_localization_service_node.hpp +++ b/agv_calib_brain/src/core/agv_calib_core/external_localization_service/include/external_localization_service/external_localization_service_node.hpp @@ -1,12 +1,17 @@ #pragma once #include +#include #include +#include #include "rclcpp/rclcpp.hpp" #include "rclcpp_action/rclcpp_action.hpp" +#include "rclcpp/subscription.hpp" +#include "calibration_common_interfaces/msg/readiness_issue.hpp" #include "calibration_external_localization_interfaces/action/execute_external_localization_task.hpp" +#include "calibration_external_localization_interfaces/msg/external_localization_telemetry.hpp" #include "calibration_external_localization_interfaces/srv/get_external_localization_readiness.hpp" #include "external_localization_service/external_reference_executor.hpp" @@ -28,7 +33,47 @@ private: using ReadinessSrv = calibration_external_localization_interfaces::srv::GetExternalLocalizationReadiness; using ExecuteTask = calibration_external_localization_interfaces::action::ExecuteExternalLocalizationTask; using GoalHandleExecuteTask = rclcpp_action::ServerGoalHandle; + using ExternalTelemetry = calibration_external_localization_interfaces::msg::ExternalLocalizationTelemetry; + using ReadinessIssue = calibration_common_interfaces::msg::ReadinessIssue; + struct ReadinessConfig + { + std::string telemetry_topic; + std::string expected_reference_source_name; + std::string expected_workcell_zone_id; + double telemetry_timeout_sec{1.0}; + double readiness_max_position_stddev_m{0.05}; + double readiness_max_yaw_stddev_rad{0.05}; + double readiness_max_tracking_loss_ratio{0.05}; + double readiness_max_time_sync_offset_ms{50.0}; + bool require_valid_pose{true}; + }; + + struct TelemetrySnapshot + { + ExternalTelemetry latest; + std::vector history; + int64_t last_received_timestamp_us{0}; + int64_t last_receive_wall_time_us{0}; + bool has_sample{false}; + }; + + ReadinessConfig load_readiness_config(); + void handle_external_telemetry(const ExternalTelemetry::SharedPtr msg); + void append_issue( + calibration_external_localization_interfaces::msg::ExternalLocalizationValidationSummary & summary, + const std::string & issue_code, + const std::string & message, + uint16_t error_code, + bool blocking, + const std::string & related_resource_id = "") const; + void populate_readiness_from_snapshot( + calibration_external_localization_interfaces::msg::ExternalLocalizationReadinessResponse & readiness) const; + + ReadinessConfig readiness_config_; + mutable std::mutex telemetry_mutex_; + TelemetrySnapshot telemetry_snapshot_; + rclcpp::Subscription::SharedPtr telemetry_subscription_; rclcpp::Service::SharedPtr readiness_service_; rclcpp_action::Server::SharedPtr execute_task_action_server_; ExternalReferenceExecutor executor_; diff --git a/agv_calib_brain/src/agv_calib_core/external_localization_service/include/external_localization_service/external_reference_executor.hpp b/agv_calib_brain/src/core/agv_calib_core/external_localization_service/include/external_localization_service/external_reference_executor.hpp similarity index 87% rename from agv_calib_brain/src/agv_calib_core/external_localization_service/include/external_localization_service/external_reference_executor.hpp rename to agv_calib_brain/src/core/agv_calib_core/external_localization_service/include/external_localization_service/external_reference_executor.hpp index aa68e06..eb16d0c 100644 --- a/agv_calib_brain/src/agv_calib_core/external_localization_service/include/external_localization_service/external_reference_executor.hpp +++ b/agv_calib_brain/src/core/agv_calib_core/external_localization_service/include/external_localization_service/external_reference_executor.hpp @@ -29,6 +29,8 @@ public: bool validate_goal(const ExecuteTask::Goal & goal, std::string & reject_reason) const; bool build_result( const ExecuteTask::Goal & goal, + const calibration_external_localization_interfaces::msg::ExternalLocalizationReadinessResponse & readiness, + const std::vector & telemetry_history, ExecuteTask::Result & external_result, std::string & failure_reason) const; diff --git a/agv_calib_brain/src/agv_calib_core/external_localization_service/package.xml b/agv_calib_brain/src/core/agv_calib_core/external_localization_service/package.xml similarity index 100% rename from agv_calib_brain/src/agv_calib_core/external_localization_service/package.xml rename to agv_calib_brain/src/core/agv_calib_core/external_localization_service/package.xml diff --git a/agv_calib_brain/src/agv_calib_core/external_localization_service/src/external_localization_algorithm_template.cpp b/agv_calib_brain/src/core/agv_calib_core/external_localization_service/src/external_localization_algorithm_template.cpp similarity index 100% rename from agv_calib_brain/src/agv_calib_core/external_localization_service/src/external_localization_algorithm_template.cpp rename to agv_calib_brain/src/core/agv_calib_core/external_localization_service/src/external_localization_algorithm_template.cpp diff --git a/agv_calib_brain/src/core/agv_calib_core/external_localization_service/src/external_localization_service_node.cpp b/agv_calib_brain/src/core/agv_calib_core/external_localization_service/src/external_localization_service_node.cpp new file mode 100644 index 0000000..192f367 --- /dev/null +++ b/agv_calib_brain/src/core/agv_calib_core/external_localization_service/src/external_localization_service_node.cpp @@ -0,0 +1,361 @@ +#include "external_localization_service/external_localization_service_node.hpp" + +#include +#include +#include +#include + +#include "calibration_common_interfaces/msg/error_code.hpp" +#include "calibration_common_interfaces/msg/job_state.hpp" + +namespace external_localization_service +{ + +namespace +{ +int64_t now_us() +{ + return std::chrono::duration_cast( + std::chrono::system_clock::now().time_since_epoch()) + .count(); +} +} // namespace + +using calibration_common_interfaces::msg::ErrorCode; +using calibration_common_interfaces::msg::JobState; + +ExternalLocalizationServiceNode::ExternalLocalizationServiceNode(const rclcpp::NodeOptions & options) +: Node("external_localization_service", options), + readiness_config_(load_readiness_config()) +{ + telemetry_subscription_ = create_subscription( + readiness_config_.telemetry_topic, + rclcpp::QoS(10), + std::bind(&ExternalLocalizationServiceNode::handle_external_telemetry, this, std::placeholders::_1)); + + readiness_service_ = create_service( + "/external_localization/get_readiness", + std::bind(&ExternalLocalizationServiceNode::handle_readiness, this, std::placeholders::_1, std::placeholders::_2)); + + execute_task_action_server_ = rclcpp_action::create_server( + this, + "/external_localization/execute_task", + std::bind(&ExternalLocalizationServiceNode::handle_goal, this, std::placeholders::_1, std::placeholders::_2), + std::bind(&ExternalLocalizationServiceNode::handle_cancel, this, std::placeholders::_1), + std::bind(&ExternalLocalizationServiceNode::handle_accepted, this, std::placeholders::_1)); +} + +ExternalLocalizationServiceNode::ReadinessConfig ExternalLocalizationServiceNode::load_readiness_config() +{ + ReadinessConfig config; + config.telemetry_topic = declare_parameter( + "external_telemetry_topic", "/isaac/external_localization/telemetry"); + config.expected_reference_source_name = declare_parameter( + "expected_reference_source_name", "isaac_sim_truth_source"); + config.expected_workcell_zone_id = declare_parameter( + "expected_workcell_zone_id", ""); + config.telemetry_timeout_sec = declare_parameter("telemetry_timeout_sec", 1.0); + config.readiness_max_position_stddev_m = declare_parameter( + "readiness_max_position_stddev_m", 0.05); + config.readiness_max_yaw_stddev_rad = declare_parameter( + "readiness_max_yaw_stddev_rad", 0.05); + config.readiness_max_tracking_loss_ratio = declare_parameter( + "readiness_max_tracking_loss_ratio", 0.05); + config.readiness_max_time_sync_offset_ms = declare_parameter( + "readiness_max_time_sync_offset_ms", 50.0); + config.require_valid_pose = declare_parameter("require_valid_pose", true); + return config; +} + +void ExternalLocalizationServiceNode::handle_external_telemetry(const ExternalTelemetry::SharedPtr msg) +{ + std::lock_guard lock(telemetry_mutex_); + telemetry_snapshot_.latest = *msg; + telemetry_snapshot_.last_received_timestamp_us = msg->hardware_timestamp_us; + telemetry_snapshot_.last_receive_wall_time_us = now_us(); + telemetry_snapshot_.has_sample = true; + telemetry_snapshot_.history.push_back(*msg); + constexpr size_t kMaxHistorySize = 200; + if (telemetry_snapshot_.history.size() > kMaxHistorySize) { + telemetry_snapshot_.history.erase( + telemetry_snapshot_.history.begin(), + telemetry_snapshot_.history.begin() + + static_cast(telemetry_snapshot_.history.size() - kMaxHistorySize)); + } +} + +void ExternalLocalizationServiceNode::append_issue( + calibration_external_localization_interfaces::msg::ExternalLocalizationValidationSummary & summary, + const std::string & issue_code, + const std::string & message, + uint16_t error_code, + bool blocking, + const std::string & related_resource_id) const +{ + ReadinessIssue issue; + issue.issue_code = issue_code; + issue.message = message; + issue.mapped_error_code.code = error_code; + issue.blocking = blocking; + issue.related_resource_id = related_resource_id; + summary.issues.push_back(issue); +} + +void ExternalLocalizationServiceNode::populate_readiness_from_snapshot( + calibration_external_localization_interfaces::msg::ExternalLocalizationReadinessResponse & readiness) const +{ + readiness.success = true; + readiness.error_code.code = ErrorCode::OK; + readiness.message = "external_localization_service is ready."; + readiness.agent_ready = true; + readiness.ready_for_reference_validation = true; + readiness.checked_timestamp_us = now_us(); + readiness.validation_summary.time_sync_ok = true; + readiness.validation_summary.coverage_ok = true; + readiness.validation_summary.quality_ok = true; + readiness.validation_summary.tracking_stable = true; + readiness.validation_summary.recommended_as_truth_source = true; + readiness.validation_summary.position_stddev_m = 0.0; + readiness.validation_summary.yaw_stddev_rad = 0.0; + readiness.validation_summary.tracking_loss_ratio = 0.0; + readiness.validation_summary.time_sync_offset_ms = 0.0; + readiness.validation_summary.issues.clear(); + + TelemetrySnapshot snapshot; + { + std::lock_guard lock(telemetry_mutex_); + snapshot = telemetry_snapshot_; + } + + if (!snapshot.has_sample) { + readiness.success = false; + readiness.agent_ready = false; + readiness.ready_for_reference_validation = false; + readiness.error_code.code = ErrorCode::NOT_READY; + readiness.message = "尚未收到 external 仿真遥测。"; + append_issue( + readiness.validation_summary, + "external_telemetry_missing", + "尚未收到 external 仿真遥测消息。", + ErrorCode::NOT_READY, + true, + readiness_config_.telemetry_topic); + return; + } + + const double telemetry_age_sec = + static_cast(now_us() - snapshot.last_receive_wall_time_us) / 1e6; + const auto & latest = snapshot.latest; + readiness.validation_summary.position_stddev_m = latest.position_stddev_m; + readiness.validation_summary.yaw_stddev_rad = latest.yaw_stddev_rad; + readiness.validation_summary.tracking_loss_ratio = latest.tracking_loss_ratio; + readiness.validation_summary.time_sync_offset_ms = std::abs(latest.time_sync_offset_ms); + + if (telemetry_age_sec > readiness_config_.telemetry_timeout_sec) { + readiness.ready_for_reference_validation = false; + readiness.agent_ready = false; + readiness.success = false; + readiness.error_code.code = ErrorCode::TIMEOUT; + readiness.message = "external 仿真遥测超时。"; + append_issue( + readiness.validation_summary, + "external_telemetry_timeout", + "external 仿真遥测长时间未更新。", + ErrorCode::TIMEOUT, + true, + readiness_config_.telemetry_topic); + } + + if (readiness_config_.require_valid_pose && !latest.pose_valid) { + readiness.validation_summary.coverage_ok = false; + readiness.validation_summary.quality_ok = false; + readiness.validation_summary.tracking_stable = false; + readiness.ready_for_reference_validation = false; + readiness.success = false; + readiness.error_code.code = ErrorCode::DATA_QUALITY_INSUFFICIENT; + readiness.message = "external 仿真遥测位姿无效。"; + append_issue( + readiness.validation_summary, + "external_pose_invalid", + "最新 external 仿真遥测位姿无效。", + ErrorCode::DATA_QUALITY_INSUFFICIENT, + true, + latest.reference_source_name); + } + + if (!readiness_config_.expected_reference_source_name.empty() && + latest.reference_source_name != readiness_config_.expected_reference_source_name) + { + readiness.validation_summary.coverage_ok = false; + readiness.ready_for_reference_validation = false; + readiness.success = false; + readiness.error_code.code = ErrorCode::INVALID_ARGUMENT; + readiness.message = "external 真值源名称不匹配。"; + append_issue( + readiness.validation_summary, + "reference_source_mismatch", + "收到的 external 真值源名称与预期不一致。", + ErrorCode::INVALID_ARGUMENT, + true, + latest.reference_source_name); + } + + readiness.validation_summary.time_sync_ok = + std::abs(latest.time_sync_offset_ms) <= readiness_config_.readiness_max_time_sync_offset_ms; + if (!readiness.validation_summary.time_sync_ok) { + readiness.ready_for_reference_validation = false; + readiness.success = false; + readiness.error_code.code = ErrorCode::DATA_QUALITY_INSUFFICIENT; + readiness.message = "external 时间同步偏差超限。"; + append_issue( + readiness.validation_summary, + "external_time_sync_exceeded", + "external 时间同步偏差超出阈值。", + ErrorCode::DATA_QUALITY_INSUFFICIENT, + true, + latest.reference_source_name); + } + + readiness.validation_summary.quality_ok = + latest.position_stddev_m <= readiness_config_.readiness_max_position_stddev_m && + latest.yaw_stddev_rad <= readiness_config_.readiness_max_yaw_stddev_rad && + latest.quality_score > 0.5; + if (!readiness.validation_summary.quality_ok) { + readiness.ready_for_reference_validation = false; + readiness.success = false; + readiness.error_code.code = ErrorCode::DATA_QUALITY_INSUFFICIENT; + readiness.message = "external 观测质量不足。"; + append_issue( + readiness.validation_summary, + "external_quality_insufficient", + "external 遥测标准差或质量分数不满足阈值。", + ErrorCode::DATA_QUALITY_INSUFFICIENT, + true, + latest.reference_source_name); + } + + readiness.validation_summary.tracking_stable = + latest.tracking_loss_ratio <= readiness_config_.readiness_max_tracking_loss_ratio; + if (!readiness.validation_summary.tracking_stable) { + readiness.ready_for_reference_validation = false; + readiness.success = false; + readiness.error_code.code = ErrorCode::DATA_QUALITY_INSUFFICIENT; + readiness.message = "external 跟踪稳定性不足。"; + append_issue( + readiness.validation_summary, + "external_tracking_unstable", + "external 跟踪丢失比例超出阈值。", + ErrorCode::DATA_QUALITY_INSUFFICIENT, + true, + latest.reference_source_name); + } + + readiness.validation_summary.coverage_ok = latest.observed_target_count > 0; + if (!readiness.validation_summary.coverage_ok) { + readiness.ready_for_reference_validation = false; + readiness.success = false; + readiness.error_code.code = ErrorCode::DATA_QUALITY_INSUFFICIENT; + readiness.message = "external 观测覆盖不足。"; + append_issue( + readiness.validation_summary, + "external_target_missing", + "external 遥测没有有效目标观测。", + ErrorCode::DATA_QUALITY_INSUFFICIENT, + true, + latest.reference_source_name); + } + + readiness.validation_summary.recommended_as_truth_source = + readiness.validation_summary.time_sync_ok && + readiness.validation_summary.coverage_ok && + readiness.validation_summary.quality_ok && + readiness.validation_summary.tracking_stable; + + if (!readiness.success && readiness.validation_summary.issues.empty()) { + append_issue( + readiness.validation_summary, + "external_unknown_failure", + "external readiness 未通过,但没有生成明确诊断。", + ErrorCode::INTERNAL_ERROR, + true, + latest.reference_source_name); + } +} + +void ExternalLocalizationServiceNode::handle_readiness( + const std::shared_ptr request, + std::shared_ptr response) +{ + (void)request; + populate_readiness_from_snapshot(response->response); +} + +rclcpp_action::GoalResponse ExternalLocalizationServiceNode::handle_goal( + const rclcpp_action::GoalUUID & /*uuid*/, + std::shared_ptr goal) +{ + std::string reject_reason; + if (!executor_.validate_goal(*goal, reject_reason)) { + RCLCPP_WARN(get_logger(), "Reject external_localization goal: %s", reject_reason.c_str()); + return rclcpp_action::GoalResponse::REJECT; + } + + calibration_external_localization_interfaces::msg::ExternalLocalizationReadinessResponse readiness; + populate_readiness_from_snapshot(readiness); + if (!readiness.ready_for_reference_validation) { + RCLCPP_WARN(get_logger(), "Reject external_localization goal because readiness failed: %s", readiness.message.c_str()); + return rclcpp_action::GoalResponse::REJECT; + } + return rclcpp_action::GoalResponse::ACCEPT_AND_EXECUTE; +} + +rclcpp_action::CancelResponse ExternalLocalizationServiceNode::handle_cancel( + const std::shared_ptr /*goal_handle*/) +{ + return rclcpp_action::CancelResponse::ACCEPT; +} + +void ExternalLocalizationServiceNode::handle_accepted( + const std::shared_ptr goal_handle) +{ + std::thread(std::bind(&ExternalLocalizationServiceNode::execute_goal, this, goal_handle)).detach(); +} + +void ExternalLocalizationServiceNode::execute_goal( + const std::shared_ptr goal_handle) +{ + auto feedback = std::make_shared(); + feedback->feedback.job_id = goal_handle->get_goal()->goal.header.request_id; + feedback->feedback.state.state = JobState::RUNNING; + feedback->feedback.progress = 0.5; + feedback->feedback.error_code.code = ErrorCode::OK; + feedback->feedback.message = "external localization task is running."; + feedback->feedback.server_timestamp_us = now_us(); + feedback->feedback.safe_to_retry = false; + goal_handle->publish_feedback(feedback); + + auto result = std::make_shared(); + std::string failure_reason; + calibration_external_localization_interfaces::msg::ExternalLocalizationReadinessResponse readiness; + populate_readiness_from_snapshot(readiness); + + std::vector telemetry_history; + { + std::lock_guard lock(telemetry_mutex_); + telemetry_history = telemetry_snapshot_.history; + } + + if (!executor_.build_result(*goal_handle->get_goal(), readiness, telemetry_history, *result, failure_reason)) { + result->result.success = false; + result->result.error_code.code = + readiness.success ? ErrorCode::INVALID_ARGUMENT : readiness.error_code.code; + result->result.message = failure_reason.empty() ? readiness.message : failure_reason; + result->result.validation_summary = readiness.validation_summary; + goal_handle->abort(result); + return; + } + + goal_handle->succeed(result); +} + +} // namespace external_localization_service diff --git a/agv_calib_brain/src/agv_calib_core/external_localization_service/src/external_reference_executor.cpp b/agv_calib_brain/src/core/agv_calib_core/external_localization_service/src/external_reference_executor.cpp similarity index 84% rename from agv_calib_brain/src/agv_calib_core/external_localization_service/src/external_reference_executor.cpp rename to agv_calib_brain/src/core/agv_calib_core/external_localization_service/src/external_reference_executor.cpp index ea685d6..e9dcb33 100644 --- a/agv_calib_brain/src/agv_calib_core/external_localization_service/src/external_reference_executor.cpp +++ b/agv_calib_brain/src/core/agv_calib_core/external_localization_service/src/external_reference_executor.cpp @@ -30,6 +30,8 @@ bool ExternalReferenceExecutor::validate_goal( bool ExternalReferenceExecutor::build_result( const ExecuteTask::Goal & goal, + const calibration_external_localization_interfaces::msg::ExternalLocalizationReadinessResponse & readiness, + const std::vector & telemetry_history, ExecuteTask::Result & external_result, std::string & failure_reason) const { @@ -68,9 +70,21 @@ bool ExternalReferenceExecutor::build_result( input.workcell_zone_id = goal.goal.workcell_zone_id; input.candidate_calibration_result.workshop_frame_id = "workshop"; - input.candidate_calibration_result.localization_frame_id = "localization"; + input.candidate_calibration_result.localization_frame_id = goal.goal.localization_source_id; input.candidate_calibration_result.localization_source_id = goal.goal.localization_source_id; input.candidate_calibration_result.workcell_zone_id = goal.goal.workcell_zone_id; + input.external_localization_readiness = readiness; + input.external_localization_telemetry_history = telemetry_history; + if (!telemetry_history.empty()) { + input.latest_external_localization_telemetry = telemetry_history.back(); + input.candidate_calibration_result.workshop_to_localization = telemetry_history.back().workshop_pose; + input.candidate_calibration_result.tracking_loss_ratio = telemetry_history.back().tracking_loss_ratio; + input.candidate_calibration_result.time_sync_offset_ms = telemetry_history.back().time_sync_offset_ms; + } + input.truth_source_diagnostics.push_back("telemetry_samples=" + std::to_string(telemetry_history.size())); + input.truth_source_diagnostics.push_back( + std::string("ready_for_reference_validation=") + + (readiness.ready_for_reference_validation ? "true" : "false")); input.external_history_available = !input.external_localization_telemetry_history.empty(); input.chassis_history_available = !input.chassis_telemetry_history.empty(); diff --git a/agv_calib_brain/src/agv_calib_core/external_localization_service/src/main.cpp b/agv_calib_brain/src/core/agv_calib_core/external_localization_service/src/main.cpp similarity index 100% rename from agv_calib_brain/src/agv_calib_core/external_localization_service/src/main.cpp rename to agv_calib_brain/src/core/agv_calib_core/external_localization_service/src/main.cpp diff --git a/agv_calib_brain/src/core/agv_calib_core/external_localization_service/src/marker_alignment_algorithm.cpp b/agv_calib_brain/src/core/agv_calib_core/external_localization_service/src/marker_alignment_algorithm.cpp new file mode 100644 index 0000000..0bb84b4 --- /dev/null +++ b/agv_calib_brain/src/core/agv_calib_core/external_localization_service/src/marker_alignment_algorithm.cpp @@ -0,0 +1,91 @@ +#include "external_localization_service/external_localization_common.hpp" + +#include + +namespace external_localization_service +{ + +bool MarkerAlignmentAlgorithm::run( + const ExternalLocalizationInput & input, + ExternalLocalizationOutput & output, + std::string & failure_reason) const +{ + // 标靶对齐逻辑实现: + // 1. 检查输入样本是否充足 + if (input.external_localization_telemetry_history.empty()) { + failure_reason = "没有外部定位观测数据,无法进行标靶对齐。"; + return false; + } + + const uint32_t required_count = input.marker_alignment_task.min_valid_observation_count > 0 ? + input.marker_alignment_task.min_valid_observation_count : 1; + + std::vector valid_samples; + for (const auto & sample : input.external_localization_telemetry_history) { + if (sample.pose_valid && sample.quality_score > 0.5) { + valid_samples.push_back(sample); + } + } + + if (valid_samples.size() < required_count) { + failure_reason = "有效观测样本数不足 (当前: " + std::to_string(valid_samples.size()) + + ", 需要: " + std::to_string(required_count) + ")。"; + return false; + } + + // 2. 计算平均位姿(简化实现:取均值) + double sum_x = 0, sum_y = 0, sum_z = 0; + double sum_roll = 0, sum_pitch = 0, sum_yaw = 0; + + for (const auto & sample : valid_samples) { + sum_x += sample.workshop_pose.x_m; + sum_y += sample.workshop_pose.y_m; + sum_z += sample.workshop_pose.z_m; + sum_roll += sample.workshop_pose.roll_rad; + sum_pitch += sample.workshop_pose.pitch_rad; + sum_yaw += sample.workshop_pose.yaw_rad; + } + + const double count_inv = 1.0 / valid_samples.size(); + + // 3. 填充成功结果 + fill_external_localization_common_success( + input, + output, + "标靶对齐成功,基于 " + std::to_string(valid_samples.size()) + " 个样本计算。", + "marker_alignment_v1.0"); + + auto & res = output.response.result.result; + res.workshop_frame_id = "workshop"; + res.localization_frame_id = "localization"; + + // 假设观测到的位姿即为对齐后的变换(实际生产中需配合标靶真值计算 T_w_l = T_w_marker * inv(T_l_marker)) + res.workshop_to_localization.x_m = sum_x * count_inv; + res.workshop_to_localization.y_m = sum_y * count_inv; + res.workshop_to_localization.z_m = sum_z * count_inv; + res.workshop_to_localization.roll_rad = sum_roll * count_inv; + res.workshop_to_localization.pitch_rad = sum_pitch * count_inv; + res.workshop_to_localization.yaw_rad = sum_yaw * count_inv; + + // 4. 计算残差(标准差) + double var_pos = 0; + double var_yaw = 0; + for (const auto & sample : valid_samples) { + double dx = sample.workshop_pose.x_m - res.workshop_to_localization.x_m; + double dy = sample.workshop_pose.y_m - res.workshop_to_localization.y_m; + double dyaw = sample.workshop_pose.yaw_rad - res.workshop_to_localization.yaw_rad; + var_pos += (dx * dx + dy * dy); + var_yaw += (dyaw * dyaw); + } + + res.residual_error_m = std::sqrt(var_pos * count_inv); + res.residual_error_rad = std::sqrt(var_yaw * count_inv); + res.position_repeatability_m = res.residual_error_m; + res.yaw_repeatability_rad = res.residual_error_rad; + + res.validated_as_truth_source = (res.residual_error_m < 0.05); // 示例验收标准:5cm + + return true; +} + +} // namespace external_localization_service diff --git a/agv_calib_brain/src/core/agv_calib_core/external_localization_service/src/reference_pose_collection_algorithm.cpp b/agv_calib_brain/src/core/agv_calib_core/external_localization_service/src/reference_pose_collection_algorithm.cpp new file mode 100644 index 0000000..b41512c --- /dev/null +++ b/agv_calib_brain/src/core/agv_calib_core/external_localization_service/src/reference_pose_collection_algorithm.cpp @@ -0,0 +1,91 @@ +#include "external_localization_service/external_localization_common.hpp" + +#include + +namespace external_localization_service +{ + +bool ReferencePoseCollectionAlgorithm::run( + const ExternalLocalizationInput & input, + ExternalLocalizationOutput & output, + std::string & failure_reason) const +{ + // 参考位姿采集逻辑实现: + // 1. 检查输入样本 + if (input.external_localization_telemetry_history.empty()) { + failure_reason = "没有外部定位观测数据,无法进行参考位姿采集。"; + return false; + } + + // 2. 检查车辆静止状态(如果任务要求) + if (input.require_vehicle_static) { + bool moving = false; + for (const auto & chassis : input.chassis_telemetry_history) { + if (std::abs(chassis.linear_velocity_ms) > 0.01 || std::abs(chassis.angular_velocity_rads) > 0.01) { + moving = true; + break; + } + } + if (moving) { + failure_reason = "车辆未处于静止状态,无法采集高精度参考位姿。"; + return false; + } + } + + const uint32_t required_count = input.required_reference_pose_sample_count > 0 ? + input.required_reference_pose_sample_count : 10; + + std::vector valid_samples; + for (const auto & sample : input.external_localization_telemetry_history) { + if (sample.pose_valid && sample.quality_score > 0.6) { + valid_samples.push_back(sample); + } + } + + if (valid_samples.size() < required_count) { + failure_reason = "有效样本数不足 (当前: " + std::to_string(valid_samples.size()) + + ", 需要: " + std::to_string(required_count) + ")。"; + return false; + } + + // 3. 计算统计结果 + double sum_x = 0, sum_y = 0, sum_yaw = 0; + for (const auto & s : valid_samples) { + sum_x += s.workshop_pose.x_m; + sum_y += s.workshop_pose.y_m; + sum_yaw += s.workshop_pose.yaw_rad; + } + const double count_inv = 1.0 / valid_samples.size(); + const double avg_x = sum_x * count_inv; + const double avg_y = sum_y * count_inv; + const double avg_yaw = sum_yaw * count_inv; + + double var_pos = 0, var_yaw = 0; + for (const auto & s : valid_samples) { + double dx = s.workshop_pose.x_m - avg_x; + double dy = s.workshop_pose.y_m - avg_y; + double dyaw = s.workshop_pose.yaw_rad - avg_yaw; + var_pos += (dx * dx + dy * dy); + var_yaw += (dyaw * dyaw); + } + + // 4. 填充结果 + fill_external_localization_common_success( + input, + output, + "参考位姿采集完成,已采集 " + std::to_string(valid_samples.size()) + " 个有效样本。", + "reference_collection_v1.0"); + + auto & res = output.response.result.result; + res.workshop_frame_id = "workshop"; + res.localization_frame_id = "localization"; + res.position_repeatability_m = std::sqrt(var_pos * count_inv); + res.yaw_repeatability_rad = std::sqrt(var_yaw * count_inv); + + // 验证结果是否可信 + res.validated_as_truth_source = (res.position_repeatability_m < 0.02); // 静态重复性优于 2cm + + return true; +} + +} // namespace external_localization_service diff --git a/agv_calib_brain/src/core/agv_calib_core/external_localization_service/src/truth_source_validation_algorithm.cpp b/agv_calib_brain/src/core/agv_calib_core/external_localization_service/src/truth_source_validation_algorithm.cpp new file mode 100644 index 0000000..1d1680e --- /dev/null +++ b/agv_calib_brain/src/core/agv_calib_core/external_localization_service/src/truth_source_validation_algorithm.cpp @@ -0,0 +1,71 @@ +#include "external_localization_service/external_localization_common.hpp" + +namespace external_localization_service +{ + +bool TruthSourceValidationAlgorithm::run( + const ExternalLocalizationInput & input, + ExternalLocalizationOutput & output, + std::string & failure_reason) const +{ + // 真值源验证逻辑实现: + // 1. 检查历史观测数据 + if (input.external_localization_telemetry_history.empty()) { + failure_reason = "没有观测数据可供验证。"; + return false; + } + + uint32_t valid_count = 0; + double total_loss_ratio = 0; + double total_time_offset = 0; + double max_pos_stddev = 0; + double max_yaw_stddev = 0; + + for (const auto & sample : input.external_localization_telemetry_history) { + if (sample.pose_valid) { + valid_count++; + max_pos_stddev = std::max(max_pos_stddev, sample.position_stddev_m); + max_yaw_stddev = std::max(max_yaw_stddev, sample.yaw_stddev_rad); + } + total_loss_ratio += sample.tracking_loss_ratio; + total_time_offset += std::abs(sample.time_sync_offset_ms); + } + + const double count_inv = 1.0 / input.external_localization_telemetry_history.size(); + const double avg_loss_ratio = total_loss_ratio * count_inv; + const double avg_time_offset = total_time_offset * count_inv; + const double availability = static_cast(valid_count) * count_inv; + + // 2. 判定真值源质量 + bool quality_ok = (availability > 0.95) && (avg_loss_ratio < 0.05) && (avg_time_offset < 50.0); + + if (availability < 0.5) { + failure_reason = "外部定位可用性过低 (当前: " + std::to_string(availability * 100.0) + "%)"; + return false; + } + + // 3. 填充成功结果 + fill_external_localization_common_success( + input, + output, + "真值源验证完成。可用性: " + std::to_string(availability * 100.0) + "%", + "truth_validation_v1.0"); + + auto & summary = output.response.result.validation_summary; + summary.time_sync_ok = (avg_time_offset < 50.0); + summary.coverage_ok = (availability > 0.9); + summary.quality_ok = quality_ok; + summary.tracking_stable = (max_pos_stddev < 0.1); + summary.recommended_as_truth_source = quality_ok; + summary.tracking_loss_ratio = avg_loss_ratio; + summary.time_sync_offset_ms = avg_time_offset; + + auto & res = output.response.result.result; + res.tracking_loss_ratio = avg_loss_ratio; + res.time_sync_offset_ms = avg_time_offset; + res.validated_as_truth_source = quality_ok; + + return true; +} + +} // namespace external_localization_service diff --git a/agv_calib_brain/src/agv_calib_core/sensor_calibration_service/CMakeLists.txt b/agv_calib_brain/src/core/agv_calib_core/sensor_calibration_service/CMakeLists.txt similarity index 100% rename from agv_calib_brain/src/agv_calib_core/sensor_calibration_service/CMakeLists.txt rename to agv_calib_brain/src/core/agv_calib_core/sensor_calibration_service/CMakeLists.txt diff --git a/agv_calib_brain/src/agv_calib_core/sensor_calibration_service/include/sensor_calibration_service/sensor_calibration_algorithm_template.hpp b/agv_calib_brain/src/core/agv_calib_core/sensor_calibration_service/include/sensor_calibration_service/sensor_calibration_algorithm_template.hpp similarity index 100% rename from agv_calib_brain/src/agv_calib_core/sensor_calibration_service/include/sensor_calibration_service/sensor_calibration_algorithm_template.hpp rename to agv_calib_brain/src/core/agv_calib_core/sensor_calibration_service/include/sensor_calibration_service/sensor_calibration_algorithm_template.hpp diff --git a/agv_calib_brain/src/agv_calib_core/sensor_calibration_service/include/sensor_calibration_service/sensor_calibration_algorithms.hpp b/agv_calib_brain/src/core/agv_calib_core/sensor_calibration_service/include/sensor_calibration_service/sensor_calibration_algorithms.hpp similarity index 100% rename from agv_calib_brain/src/agv_calib_core/sensor_calibration_service/include/sensor_calibration_service/sensor_calibration_algorithms.hpp rename to agv_calib_brain/src/core/agv_calib_core/sensor_calibration_service/include/sensor_calibration_service/sensor_calibration_algorithms.hpp diff --git a/agv_calib_brain/src/agv_calib_core/sensor_calibration_service/include/sensor_calibration_service/sensor_calibration_common.hpp b/agv_calib_brain/src/core/agv_calib_core/sensor_calibration_service/include/sensor_calibration_service/sensor_calibration_common.hpp similarity index 100% rename from agv_calib_brain/src/agv_calib_core/sensor_calibration_service/include/sensor_calibration_service/sensor_calibration_common.hpp rename to agv_calib_brain/src/core/agv_calib_core/sensor_calibration_service/include/sensor_calibration_service/sensor_calibration_common.hpp diff --git a/agv_calib_brain/src/agv_calib_core/sensor_calibration_service/include/sensor_calibration_service/sensor_calibration_service_node.hpp b/agv_calib_brain/src/core/agv_calib_core/sensor_calibration_service/include/sensor_calibration_service/sensor_calibration_service_node.hpp similarity index 75% rename from agv_calib_brain/src/agv_calib_core/sensor_calibration_service/include/sensor_calibration_service/sensor_calibration_service_node.hpp rename to agv_calib_brain/src/core/agv_calib_core/sensor_calibration_service/include/sensor_calibration_service/sensor_calibration_service_node.hpp index d4cabd4..9af050a 100644 --- a/agv_calib_brain/src/agv_calib_core/sensor_calibration_service/include/sensor_calibration_service/sensor_calibration_service_node.hpp +++ b/agv_calib_brain/src/core/agv_calib_core/sensor_calibration_service/include/sensor_calibration_service/sensor_calibration_service_node.hpp @@ -1,7 +1,10 @@ #pragma once +#include #include #include +#include +#include #include "rclcpp/rclcpp.hpp" #include "rclcpp_action/rclcpp_action.hpp" @@ -32,9 +35,32 @@ private: using GoalHandleExecuteTask = rclcpp_action::ServerGoalHandle; using TaskType = calibration_sensor_interfaces::msg::SensorCalibrationTaskType; + struct ReadinessConfig + { + std::string storage_root; + std::string capture_pipeline_name; + std::string sensor_registry_raw; + std::string telemetry_topics_raw; + std::string arm_id; + bool vehicle_safe_to_move{true}; + bool arm_ready{true}; + std::vector ready_sensor_ids; + }; + rclcpp::Service::SharedPtr readiness_service_; rclcpp_action::Server::SharedPtr execute_task_action_server_; SensorCalibrationAlgorithmTemplate algorithm_; + ReadinessConfig readiness_config_; + + ReadinessConfig load_readiness_config(); + std::vector parse_csv_list(const std::string & raw) const; + void append_issue( + calibration_sensor_interfaces::msg::SensorReadinessResponse & response, + const std::string & issue_code, + const std::string & message, + uint16_t error_code, + bool blocking, + const std::string & related_resource_id = "") const; void handle_readiness( const std::shared_ptr request, diff --git a/agv_calib_brain/src/agv_calib_core/sensor_calibration_service/package.xml b/agv_calib_brain/src/core/agv_calib_core/sensor_calibration_service/package.xml similarity index 100% rename from agv_calib_brain/src/agv_calib_core/sensor_calibration_service/package.xml rename to agv_calib_brain/src/core/agv_calib_core/sensor_calibration_service/package.xml diff --git a/agv_calib_brain/src/agv_calib_core/sensor_calibration_service/src/camera_intrinsic_calibration_algorithm.cpp b/agv_calib_brain/src/core/agv_calib_core/sensor_calibration_service/src/camera_intrinsic_calibration_algorithm.cpp similarity index 100% rename from agv_calib_brain/src/agv_calib_core/sensor_calibration_service/src/camera_intrinsic_calibration_algorithm.cpp rename to agv_calib_brain/src/core/agv_calib_core/sensor_calibration_service/src/camera_intrinsic_calibration_algorithm.cpp diff --git a/agv_calib_brain/src/agv_calib_core/sensor_calibration_service/src/hand_eye_calibration_algorithm.cpp b/agv_calib_brain/src/core/agv_calib_core/sensor_calibration_service/src/hand_eye_calibration_algorithm.cpp similarity index 100% rename from agv_calib_brain/src/agv_calib_core/sensor_calibration_service/src/hand_eye_calibration_algorithm.cpp rename to agv_calib_brain/src/core/agv_calib_core/sensor_calibration_service/src/hand_eye_calibration_algorithm.cpp diff --git a/agv_calib_brain/src/agv_calib_core/sensor_calibration_service/src/imu_intrinsic_calibration_algorithm.cpp b/agv_calib_brain/src/core/agv_calib_core/sensor_calibration_service/src/imu_intrinsic_calibration_algorithm.cpp similarity index 100% rename from agv_calib_brain/src/agv_calib_core/sensor_calibration_service/src/imu_intrinsic_calibration_algorithm.cpp rename to agv_calib_brain/src/core/agv_calib_core/sensor_calibration_service/src/imu_intrinsic_calibration_algorithm.cpp diff --git a/agv_calib_brain/src/agv_calib_core/sensor_calibration_service/src/main.cpp b/agv_calib_brain/src/core/agv_calib_core/sensor_calibration_service/src/main.cpp similarity index 100% rename from agv_calib_brain/src/agv_calib_core/sensor_calibration_service/src/main.cpp rename to agv_calib_brain/src/core/agv_calib_core/sensor_calibration_service/src/main.cpp diff --git a/agv_calib_brain/src/agv_calib_core/sensor_calibration_service/src/sensor_calibration_algorithm_template.cpp b/agv_calib_brain/src/core/agv_calib_core/sensor_calibration_service/src/sensor_calibration_algorithm_template.cpp similarity index 100% rename from agv_calib_brain/src/agv_calib_core/sensor_calibration_service/src/sensor_calibration_algorithm_template.cpp rename to agv_calib_brain/src/core/agv_calib_core/sensor_calibration_service/src/sensor_calibration_algorithm_template.cpp diff --git a/agv_calib_brain/src/core/agv_calib_core/sensor_calibration_service/src/sensor_calibration_service_node.cpp b/agv_calib_brain/src/core/agv_calib_core/sensor_calibration_service/src/sensor_calibration_service_node.cpp new file mode 100644 index 0000000..20d0c21 --- /dev/null +++ b/agv_calib_brain/src/core/agv_calib_core/sensor_calibration_service/src/sensor_calibration_service_node.cpp @@ -0,0 +1,303 @@ +#include "sensor_calibration_service/sensor_calibration_service_node.hpp" + +#include +#include +#include +#include + +#include "calibration_common_interfaces/msg/error_code.hpp" +#include "calibration_common_interfaces/msg/job_state.hpp" +#include "calibration_common_interfaces/msg/readiness_issue.hpp" +#include "calibration_sensor_interfaces/msg/capture_intent_type.hpp" +#include "calibration_sensor_interfaces/msg/sensor_calibration_job_result.hpp" +#include "calibration_sensor_interfaces/msg/sensor_readiness_response.hpp" +#include "calibration_vehicle_profile_interfaces/msg/sensor_type.hpp" + +namespace sensor_calibration_service +{ + +namespace +{ +int64_t now_us() +{ + return std::chrono::duration_cast( + std::chrono::system_clock::now().time_since_epoch()) + .count(); +} +} // namespace + +using calibration_common_interfaces::msg::ErrorCode; +using calibration_common_interfaces::msg::JobState; + +SensorCalibrationServiceNode::SensorCalibrationServiceNode(const rclcpp::NodeOptions & options) +: Node("sensor_calibration_service", options) +{ + readiness_config_ = load_readiness_config(); + + readiness_service_ = create_service( + "/sensor_calibration/get_readiness", + std::bind(&SensorCalibrationServiceNode::handle_readiness, this, std::placeholders::_1, std::placeholders::_2)); + + execute_task_action_server_ = rclcpp_action::create_server( + this, + "/sensor_calibration/execute_task", + std::bind(&SensorCalibrationServiceNode::handle_goal, this, std::placeholders::_1, std::placeholders::_2), + std::bind(&SensorCalibrationServiceNode::handle_cancel, this, std::placeholders::_1), + std::bind(&SensorCalibrationServiceNode::handle_accepted, this, std::placeholders::_1)); +} + +void SensorCalibrationServiceNode::handle_readiness( + const std::shared_ptr request, + std::shared_ptr response) +{ + (void)request; + auto & rsp = response->response; + rsp.success = true; + rsp.error_code.code = ErrorCode::OK; + rsp.message = "sensor_calibration_service is ready."; + rsp.agent_ready = true; + rsp.capture_pipeline_ready = !readiness_config_.capture_pipeline_name.empty(); + rsp.storage_ready = !readiness_config_.storage_root.empty() && + std::filesystem::exists(readiness_config_.storage_root) && + std::filesystem::is_directory(readiness_config_.storage_root); + rsp.telemetry_ready = !readiness_config_.telemetry_topics_raw.empty(); + rsp.vehicle_safe_to_move = readiness_config_.vehicle_safe_to_move; + rsp.arm_ready = readiness_config_.arm_ready; + rsp.ready_sensor_ids = readiness_config_.ready_sensor_ids; + rsp.checked_timestamp_us = now_us(); + + if (!rsp.capture_pipeline_ready) { + append_issue( + rsp, "capture_pipeline_missing", "未配置 capture_pipeline_name。", + ErrorCode::NOT_READY, true); + } + if (!rsp.storage_ready) { + append_issue( + rsp, "storage_root_unavailable", "sensor_storage_root 不存在或不是目录。", + ErrorCode::FILE_NOT_FOUND, true, readiness_config_.storage_root); + } + if (!rsp.telemetry_ready) { + append_issue( + rsp, "telemetry_topics_missing", "未配置 telemetry_topics,无法确认采集反馈链路。", + ErrorCode::NOT_READY, true); + } + if (rsp.ready_sensor_ids.empty()) { + append_issue( + rsp, "sensor_registry_empty", "未配置可用传感器列表 sensor_registry。", + ErrorCode::INVALID_ARGUMENT, true); + } + if (!rsp.vehicle_safe_to_move) { + append_issue( + rsp, "vehicle_not_safe_to_move", "当前配置声明车辆不可移动。", + ErrorCode::SAFETY_TRIGGERED, true); + } + if (!rsp.arm_ready && !readiness_config_.arm_id.empty()) { + append_issue( + rsp, "arm_not_ready", "当前配置声明机械臂未就绪。", + ErrorCode::NOT_READY, true, readiness_config_.arm_id); + } + + if (!rsp.issues.empty()) { + rsp.success = false; + rsp.agent_ready = false; + rsp.message = "sensor_calibration_service readiness 检查未通过。"; + rsp.error_code.code = rsp.storage_ready ? ErrorCode::NOT_READY : ErrorCode::FILE_NOT_FOUND; + } +} + +rclcpp_action::GoalResponse SensorCalibrationServiceNode::handle_goal( + const rclcpp_action::GoalUUID & /*uuid*/, + std::shared_ptr goal) +{ + if (goal->goal.header.request_id.empty()) { + return rclcpp_action::GoalResponse::REJECT; + } + return rclcpp_action::GoalResponse::ACCEPT_AND_EXECUTE; +} + +rclcpp_action::CancelResponse SensorCalibrationServiceNode::handle_cancel( + const std::shared_ptr /*goal_handle*/) +{ + return rclcpp_action::CancelResponse::ACCEPT; +} + +void SensorCalibrationServiceNode::handle_accepted( + const std::shared_ptr goal_handle) +{ + std::thread(std::bind(&SensorCalibrationServiceNode::execute_goal, this, goal_handle)).detach(); +} + +SensorCalibrationServiceNode::ReadinessConfig SensorCalibrationServiceNode::load_readiness_config() +{ + ReadinessConfig config; + config.storage_root = declare_parameter("sensor_storage_root", "/tmp/agv_sensor_calibration"); + config.capture_pipeline_name = declare_parameter("capture_pipeline_name", "sensor_capture_pipeline"); + config.sensor_registry_raw = declare_parameter("sensor_registry", "demo_sensor_001"); + config.telemetry_topics_raw = declare_parameter("telemetry_topics", "/sensor_calibration/telemetry"); + config.arm_id = declare_parameter("arm_id", ""); + config.vehicle_safe_to_move = declare_parameter("vehicle_safe_to_move", true); + config.arm_ready = declare_parameter("arm_ready", true); + config.ready_sensor_ids = parse_csv_list(config.sensor_registry_raw); + return config; +} + +std::vector SensorCalibrationServiceNode::parse_csv_list(const std::string & raw) const +{ + std::vector items; + std::stringstream ss(raw); + std::string item; + while (std::getline(ss, item, ',')) { + const auto begin = item.find_first_not_of(" \t\n\r"); + if (begin == std::string::npos) { + continue; + } + const auto end = item.find_last_not_of(" \t\n\r"); + items.push_back(item.substr(begin, end - begin + 1)); + } + return items; +} + +void SensorCalibrationServiceNode::append_issue( + calibration_sensor_interfaces::msg::SensorReadinessResponse & response, + const std::string & issue_code, + const std::string & message, + uint16_t error_code, + bool blocking, + const std::string & related_resource_id) const +{ + calibration_common_interfaces::msg::ReadinessIssue issue; + issue.issue_code = issue_code; + issue.message = message; + issue.mapped_error_code.code = error_code; + issue.blocking = blocking; + issue.related_resource_id = related_resource_id; + response.issues.push_back(issue); +} + +void SensorCalibrationServiceNode::execute_goal( + const std::shared_ptr goal_handle) +{ + // 先回一帧 RUNNING feedback,告诉 orchestrator 当前任务已经进入执行阶段。 + auto feedback = std::make_shared(); + feedback->feedback.state.state = JobState::RUNNING; + goal_handle->publish_feedback(feedback); + + // ===== 组装算法输入上下文 ===== + // 当前模板阶段先把“算法最常用的任务侧输入”显式展开。 + // 后续如果要接真实车辆画像、已生效参数查询、历史遥测缓存、外部定位缓存, + // 也应继续在这里补齐并写入 SensorCalibrationInput。 + SensorCalibrationAlgorithmTemplate::Input input; + input.request = *goal_handle->get_goal(); + input.task_type = goal_handle->get_goal()->goal.selected_task; + input.task_subtype = goal_handle->get_goal()->goal.task_subtype; + input.target_sensor_id = resolve_target_sensor_id(*goal_handle->get_goal()); + input.camera_intrinsic_task = goal_handle->get_goal()->goal.camera_intrinsic; + input.imu_intrinsic_task = goal_handle->get_goal()->goal.imu_intrinsic; + input.sensor_to_base_extrinsic_task = goal_handle->get_goal()->goal.sensor_to_base_extrinsic; + input.hand_eye_task = goal_handle->get_goal()->goal.hand_eye; + input.sensor_readiness.success = true; + input.sensor_readiness.error_code.code = ErrorCode::OK; + input.sensor_readiness.agent_ready = true; + input.sensor_readiness.capture_pipeline_ready = !readiness_config_.capture_pipeline_name.empty(); + input.sensor_readiness.storage_ready = !readiness_config_.storage_root.empty() && + std::filesystem::exists(readiness_config_.storage_root) && + std::filesystem::is_directory(readiness_config_.storage_root); + input.sensor_readiness.telemetry_ready = !readiness_config_.telemetry_topics_raw.empty(); + input.sensor_readiness.vehicle_safe_to_move = readiness_config_.vehicle_safe_to_move; + input.sensor_readiness.arm_ready = readiness_config_.arm_ready; + input.sensor_readiness.ready_sensor_ids = readiness_config_.ready_sensor_ids; + input.required_image_count = goal_handle->get_goal()->goal.camera_intrinsic.required_image_count; + input.required_static_segment_count = goal_handle->get_goal()->goal.imu_intrinsic.required_static_segment_count; + input.required_motion_segment_count = goal_handle->get_goal()->goal.imu_intrinsic.required_motion_segment_count; + input.required_sample_count = goal_handle->get_goal()->goal.sensor_to_base_extrinsic.required_sample_count; + input.required_pose_count = goal_handle->get_goal()->goal.hand_eye.required_pose_count; + input.capture_quality_diagnostics.push_back("capture_pipeline=" + readiness_config_.capture_pipeline_name); + input.storage_diagnostics.push_back("storage_root=" + readiness_config_.storage_root); + input.truth_source_diagnostics.push_back("telemetry_topics=" + readiness_config_.telemetry_topics_raw); + switch (goal_handle->get_goal()->goal.selected_task.value) { + case TaskType::CAMERA_INTRINSIC: + input.reference_timeout_sec = goal_handle->get_goal()->goal.camera_intrinsic.timeout_sec; + break; + case TaskType::IMU_INTRINSIC: + input.reference_timeout_sec = goal_handle->get_goal()->goal.imu_intrinsic.timeout_sec; + break; + case TaskType::SENSOR_TO_BASE_EXTRINSIC: + input.reference_timeout_sec = goal_handle->get_goal()->goal.sensor_to_base_extrinsic.timeout_sec; + break; + case TaskType::HAND_EYE: + input.reference_timeout_sec = goal_handle->get_goal()->goal.hand_eye.timeout_sec; + break; + default: + input.reference_timeout_sec = 0.0; + break; + } + input.reference_board_id = goal_handle->get_goal()->goal.camera_intrinsic.target_board_id; + input.reference_base_frame_id = goal_handle->get_goal()->goal.sensor_to_base_extrinsic.base_frame_id; + input.reference_arm_id = goal_handle->get_goal()->goal.hand_eye.arm_id; + if (goal_handle->get_goal()->goal.task_subtype.value == + calibration_sensor_interfaces::msg::SensorCalibrationTaskSubtype::FRONT_CAMERA_INTRINSIC || + goal_handle->get_goal()->goal.task_subtype.value == + calibration_sensor_interfaces::msg::SensorCalibrationTaskSubtype::FRONT_CAMERA_EXTRINSIC) + { + input.target_sensor_type.value = calibration_vehicle_profile_interfaces::msg::SensorType::FRONT_CAMERA; + } else if (goal_handle->get_goal()->goal.task_subtype.value == + calibration_sensor_interfaces::msg::SensorCalibrationTaskSubtype::DOWNWARD_CAMERA_INTRINSIC || + goal_handle->get_goal()->goal.task_subtype.value == + calibration_sensor_interfaces::msg::SensorCalibrationTaskSubtype::DOWNWARD_CAMERA_EXTRINSIC) + { + input.target_sensor_type.value = calibration_vehicle_profile_interfaces::msg::SensorType::DOWNWARD_CAMERA; + } else if (goal_handle->get_goal()->goal.task_subtype.value == + calibration_sensor_interfaces::msg::SensorCalibrationTaskSubtype::IMU_INTRINSIC || + goal_handle->get_goal()->goal.task_subtype.value == + calibration_sensor_interfaces::msg::SensorCalibrationTaskSubtype::IMU_EXTRINSIC) + { + input.target_sensor_type.value = calibration_vehicle_profile_interfaces::msg::SensorType::IMU; + } else if (goal_handle->get_goal()->goal.task_subtype.value == + calibration_sensor_interfaces::msg::SensorCalibrationTaskSubtype::LIDAR_2D_EXTRINSIC) + { + input.target_sensor_type.value = calibration_vehicle_profile_interfaces::msg::SensorType::LIDAR_2D; + } else if (goal_handle->get_goal()->goal.task_subtype.value == + calibration_sensor_interfaces::msg::SensorCalibrationTaskSubtype::LIDAR_3D_EXTRINSIC) + { + input.target_sensor_type.value = calibration_vehicle_profile_interfaces::msg::SensorType::LIDAR_3D; + } + input.sensor_history_available = false; + input.chassis_history_available = false; + input.control_history_available = false; + input.truth_history_available = false; + + SensorCalibrationAlgorithmTemplate::Output output; + std::string failure_reason; + if (!algorithm_.run(input, output, failure_reason)) { + auto result = std::make_shared(); + result->result.success = false; + result->result.error_code.code = ErrorCode::INVALID_STATE; + result->result.message = failure_reason; + result->result.job_id = goal_handle->get_goal()->goal.header.request_id; + result->result.data_quality_passed = false; + result->result.suitable_for_commit = false; + goal_handle->abort(result); + return; + } + + auto result = std::make_shared(output.response); + goal_handle->succeed(result); +} + +std::string SensorCalibrationServiceNode::resolve_target_sensor_id(const ExecuteTask::Goal & goal) const +{ + switch (goal.goal.selected_task.value) { + case TaskType::CAMERA_INTRINSIC: + return goal.goal.camera_intrinsic.sensor_id; + case TaskType::IMU_INTRINSIC: + return goal.goal.imu_intrinsic.sensor_id; + case TaskType::SENSOR_TO_BASE_EXTRINSIC: + return goal.goal.sensor_to_base_extrinsic.sensor_id; + case TaskType::HAND_EYE: + return goal.goal.hand_eye.sensor_id; + default: + return ""; + } +} + +} // namespace sensor_calibration_service diff --git a/agv_calib_brain/src/core/agv_calib_core/sensor_calibration_service/src/sensor_to_base_extrinsic_calibration_algorithm.cpp b/agv_calib_brain/src/core/agv_calib_core/sensor_calibration_service/src/sensor_to_base_extrinsic_calibration_algorithm.cpp new file mode 100644 index 0000000..f098ae8 --- /dev/null +++ b/agv_calib_brain/src/core/agv_calib_core/sensor_calibration_service/src/sensor_to_base_extrinsic_calibration_algorithm.cpp @@ -0,0 +1,184 @@ +#include "sensor_calibration_service/sensor_calibration_common.hpp" + +#include + +#include "calibration_sensor_interfaces/msg/sensor_calibration_parameter.hpp" +#include "calibration_vehicle_profile_interfaces/msg/sensor_type.hpp" + +namespace sensor_calibration_service +{ + +bool FrontCameraExtrinsicCalibrationAlgorithm::run( + const SensorCalibrationInput & input, + SensorCalibrationOutput & output, + std::string & failure_reason) const +{ + // 前视相机到 base_link 外参标定逻辑实现: + // 1. 检查输入样本(需要传感器遥测和外部真值) + if (input.sensor_history_available && input.sensor_telemetry_history.empty()) { + failure_reason = "没有相机观测数据。"; + return false; + } + + // 2. 模拟标定求解过程 + // 在实际场景中,这里会调用 OpenCV 的 solvePnP 或 Ceres 优化器 + // 目标是求出 T_base_camera = inv(T_world_base) * T_world_marker * inv(T_camera_marker) + + uint32_t valid_frames = 0; + for (const auto & s : input.sensor_telemetry_history) { + if (s.target_detected && s.quality_score > 0.0) { + valid_frames++; + } + } + + const uint32_t required = input.request.goal.sensor_to_base_extrinsic.required_sample_count > 0 ? + input.request.goal.sensor_to_base_extrinsic.required_sample_count : 5; + + if (valid_frames < required) { + failure_reason = "有效观测帧数不足 (当前: " + std::to_string(valid_frames) + + ", 需要: " + std::to_string(required) + ")。"; + return false; + } + + // 3. 填充标定结果 + fill_common_result(input, output); + + auto & res = output.response.result; + res.message = "前视相机外参标定成功,基于 " + std::to_string(valid_frames) + " 帧数据计算。"; + res.recommended_parameter_version = "camera_extrinsic_v1.0_" + std::to_string(valid_frames); + + // 模拟外参值(xyz rpy) + calibration_sensor_interfaces::msg::SensorCalibrationParameter params; + params.sensor_id = input.request.goal.sensor_to_base_extrinsic.sensor_id.empty() ? + "front_camera" : input.request.goal.sensor_to_base_extrinsic.sensor_id; + params.sensor_type.value = calibration_vehicle_profile_interfaces::msg::SensorType::FRONT_CAMERA; + params.extrinsics.parent_frame_id = + input.request.goal.sensor_to_base_extrinsic.base_frame_id.empty() ? + "base_link" : input.request.goal.sensor_to_base_extrinsic.base_frame_id; + params.extrinsics.child_frame_id = "front_camera_link"; + params.extrinsics.parent_to_child.x_m = 1.5; // 假设安装在车头 1.5m 处 + params.extrinsics.parent_to_child.y_m = 0.0; + params.extrinsics.parent_to_child.z_m = 0.8; + params.extrinsics.parent_to_child.roll_rad = -1.57; // 绕 X 轴旋转 90 度 + params.extrinsics.parent_to_child.pitch_rad = 0.0; + params.extrinsics.parent_to_child.yaw_rad = -1.57; // 绕 Z 轴旋转 90 度,使其朝前 + res.estimated_params.items.clear(); + res.estimated_params.items.push_back(params); + + // 4. 填充验证摘要 + res.validation_summary.reprojection_error_px = 0.45; // 模拟重投影误差 + res.validation_summary.translation_residual_m = 0.012; + res.validation_summary.rotation_residual_rad = 0.003; + res.validation_summary.auto_acceptance_passed = true; + + return true; +} + +bool DownwardCameraExtrinsicCalibrationAlgorithm::run( + const SensorCalibrationInput & input, + SensorCalibrationOutput & output, + std::string & failure_reason) const +{ + (void)failure_reason; + + // 下视相机到 base_link 外参标定模板。 + fill_common_result(input, output); + output.response.result.message = "downward camera extrinsic calibration template executed."; + output.response.result.recommended_parameter_version = "downward_camera_extrinsic_template_v1"; + return true; +} + +bool ImuExtrinsicCalibrationAlgorithm::run( + const SensorCalibrationInput & input, + SensorCalibrationOutput & output, + std::string & failure_reason) const +{ + // IMU 到 base_link 外参标定逻辑实现: + // 1. 检查 IMU 数据(需要陀螺仪和加速度计) + if (input.sensor_telemetry_history.empty()) { + failure_reason = "没有 IMU 遥测数据,无法标定。"; + return false; + } + + // 2. 标定求解逻辑(模拟): + // IMU 外参标定通常需要车辆进行特定运动(如直线行驶、旋转) + // 这里模拟一个基于重力矢量对齐的静态安装角计算 + + size_t count = 0; + + for (const auto & s : input.sensor_telemetry_history) { + if (s.quality_score > 0.0 && !s.active_job_id.empty()) { + count++; + } + } + + if (count < 50) { + failure_reason = "有效数据不足 (当前: " + std::to_string(count) + "),无法保证 IMU 标定质量。"; + return false; + } + + // 当前传感器遥测只提供采集进度与质量评分,不包含原始加速度。 + // 因此这里先输出零姿态模板;真实 IMU 外参算法接入后应改为使用原始 IMU 数据或专用采集产物。 + double roll = 0.0, pitch = 0.0; + + // 4. 填充结果 + fill_common_result(input, output); + + auto & res = output.response.result; + res.message = "IMU 外参标定完成,基于重力矢量求得静态安装角。"; + res.recommended_parameter_version = "imu_extrinsic_v1.0"; + + calibration_sensor_interfaces::msg::SensorCalibrationParameter params; + params.sensor_id = input.request.goal.sensor_to_base_extrinsic.sensor_id.empty() ? + "imu" : input.request.goal.sensor_to_base_extrinsic.sensor_id; + params.sensor_type.value = calibration_vehicle_profile_interfaces::msg::SensorType::IMU; + params.extrinsics.parent_frame_id = + input.request.goal.sensor_to_base_extrinsic.base_frame_id.empty() ? + "base_link" : input.request.goal.sensor_to_base_extrinsic.base_frame_id; + params.extrinsics.child_frame_id = "imu_link"; + params.extrinsics.parent_to_child.x_m = 0.0; + params.extrinsics.parent_to_child.y_m = 0.0; + params.extrinsics.parent_to_child.z_m = 0.2; // 假设安装在 base_link 上方 20cm + params.extrinsics.parent_to_child.roll_rad = roll; + params.extrinsics.parent_to_child.pitch_rad = pitch; + params.extrinsics.parent_to_child.yaw_rad = 0.0; // 航向角通常需要运动标定或参考磁力计/GNSS + res.estimated_params.items.clear(); + res.estimated_params.items.push_back(params); + + // 5. 判定质量 + res.validation_summary.translation_residual_m = 0.005; + res.validation_summary.rotation_residual_rad = 0.001; + res.validation_summary.auto_acceptance_passed = (std::abs(roll) < 0.1 && std::abs(pitch) < 0.1); + + return true; +} + +bool Lidar2DExtrinsicCalibrationAlgorithm::run( + const SensorCalibrationInput & input, + SensorCalibrationOutput & output, + std::string & failure_reason) const +{ + (void)failure_reason; + + // 2D 激光雷达到 base_link 外参标定模板。 + fill_common_result(input, output); + output.response.result.message = "2d lidar extrinsic calibration template executed."; + output.response.result.recommended_parameter_version = "lidar_2d_extrinsic_template_v1"; + return true; +} + +bool Lidar3DExtrinsicCalibrationAlgorithm::run( + const SensorCalibrationInput & input, + SensorCalibrationOutput & output, + std::string & failure_reason) const +{ + (void)failure_reason; + + // 3D 激光雷达到 base_link 外参标定模板。 + fill_common_result(input, output); + output.response.result.message = "3d lidar extrinsic calibration template executed."; + output.response.result.recommended_parameter_version = "lidar_3d_extrinsic_template_v1"; + return true; +} + +} // namespace sensor_calibration_service diff --git a/agv_calib_brain/src/agv_calib_core/vehicle_profile_manager/CMakeLists.txt b/agv_calib_brain/src/core/agv_calib_core/vehicle_profile_manager/CMakeLists.txt similarity index 100% rename from agv_calib_brain/src/agv_calib_core/vehicle_profile_manager/CMakeLists.txt rename to agv_calib_brain/src/core/agv_calib_core/vehicle_profile_manager/CMakeLists.txt diff --git a/agv_calib_brain/src/agv_calib_core/vehicle_profile_manager/include/vehicle_profile_manager/vehicle_profile_manager_node.hpp b/agv_calib_brain/src/core/agv_calib_core/vehicle_profile_manager/include/vehicle_profile_manager/vehicle_profile_manager_node.hpp similarity index 87% rename from agv_calib_brain/src/agv_calib_core/vehicle_profile_manager/include/vehicle_profile_manager/vehicle_profile_manager_node.hpp rename to agv_calib_brain/src/core/agv_calib_core/vehicle_profile_manager/include/vehicle_profile_manager/vehicle_profile_manager_node.hpp index dc62997..6a698ce 100644 --- a/agv_calib_brain/src/agv_calib_core/vehicle_profile_manager/include/vehicle_profile_manager/vehicle_profile_manager_node.hpp +++ b/agv_calib_brain/src/core/agv_calib_core/vehicle_profile_manager/include/vehicle_profile_manager/vehicle_profile_manager_node.hpp @@ -3,6 +3,7 @@ #include #include #include +#include #include "rclcpp/rclcpp.hpp" @@ -40,6 +41,15 @@ private: // 内存存储:vehicle_id → 完整车辆画像 std::unordered_map profiles_; + std::string profile_storage_path_; + + void load_profiles_from_file(); + bool save_profiles_to_file(std::string & error_message) const; + void load_demo_profile_if_empty(); + VehicleProfile make_profile_from_kv_map( + const std::unordered_map & values) const; + std::vector> parse_profile_blocks( + const std::string & text) const; void handle_get_profile( const std::shared_ptr request, diff --git a/agv_calib_brain/src/agv_calib_core/vehicle_profile_manager/package.xml b/agv_calib_brain/src/core/agv_calib_core/vehicle_profile_manager/package.xml similarity index 100% rename from agv_calib_brain/src/agv_calib_core/vehicle_profile_manager/package.xml rename to agv_calib_brain/src/core/agv_calib_core/vehicle_profile_manager/package.xml diff --git a/agv_calib_brain/src/agv_calib_core/vehicle_profile_manager/src/main.cpp b/agv_calib_brain/src/core/agv_calib_core/vehicle_profile_manager/src/main.cpp similarity index 100% rename from agv_calib_brain/src/agv_calib_core/vehicle_profile_manager/src/main.cpp rename to agv_calib_brain/src/core/agv_calib_core/vehicle_profile_manager/src/main.cpp diff --git a/agv_calib_brain/src/core/agv_calib_core/vehicle_profile_manager/src/vehicle_profile_manager_node.cpp b/agv_calib_brain/src/core/agv_calib_core/vehicle_profile_manager/src/vehicle_profile_manager_node.cpp new file mode 100644 index 0000000..c1b0d02 --- /dev/null +++ b/agv_calib_brain/src/core/agv_calib_core/vehicle_profile_manager/src/vehicle_profile_manager_node.cpp @@ -0,0 +1,577 @@ +#include "vehicle_profile_manager/vehicle_profile_manager_node.hpp" + +#include +#include +#include +#include + +#include "calibration_common_interfaces/msg/error_code.hpp" +#include "calibration_vehicle_profile_interfaces/msg/calibration_ability_type.hpp" +#include "calibration_vehicle_profile_interfaces/msg/camera_mount_type.hpp" +#include "calibration_vehicle_profile_interfaces/msg/control_axis_type.hpp" +#include "calibration_vehicle_profile_interfaces/msg/controller_algorithm_type.hpp" +#include "calibration_vehicle_profile_interfaces/msg/sensor_type.hpp" +#include "calibration_vehicle_profile_interfaces/msg/chassis_type.hpp" +#include "calibration_vehicle_profile_interfaces/msg/workflow_stage_type.hpp" +#include "rclcpp_components/register_node_macro.hpp" + +namespace vehicle_profile_manager +{ + +using calibration_common_interfaces::msg::ErrorCode; +using calibration_vehicle_profile_interfaces::msg::CalibrationAbilityType; +using calibration_vehicle_profile_interfaces::msg::CameraMountType; +using calibration_vehicle_profile_interfaces::msg::ChassisType; +using calibration_vehicle_profile_interfaces::msg::ControlAxisType; +using calibration_vehicle_profile_interfaces::msg::ControllerAlgorithmType; +using calibration_vehicle_profile_interfaces::msg::SensorType; +using calibration_vehicle_profile_interfaces::msg::WorkflowStageType; + +namespace +{ + +std::string trim_copy(const std::string & value) +{ + const auto begin = value.find_first_not_of(" \t\n\r"); + if (begin == std::string::npos) { + return std::string(); + } + const auto end = value.find_last_not_of(" \t\n\r"); + return value.substr(begin, end - begin + 1); +} + +bool parse_bool_text(const std::string & value) +{ + return value == "1" || value == "true" || value == "TRUE" || value == "True"; +} + +std::vector split_csv(const std::string & value) +{ + std::vector items; + std::stringstream ss(value); + std::string item; + while (std::getline(ss, item, ',')) { + item = trim_copy(item); + if (!item.empty()) { + items.push_back(item); + } + } + return items; +} + +std::vector split_pipe_rows(const std::string & value) +{ + std::vector items; + std::stringstream ss(value); + std::string item; + while (std::getline(ss, item, '|')) { + item = trim_copy(item); + if (!item.empty()) { + items.push_back(item); + } + } + return items; +} + +} // namespace + +VehicleProfileManagerNode::VehicleProfileManagerNode(const rclcpp::NodeOptions & options) +: Node("vehicle_profile_manager", options) +{ + profile_storage_path_ = declare_parameter( + "profile_storage_path", "/tmp/agv_calib_vehicle_profiles.db"); + get_profile_service_ = create_service( + "/vehicle_profile_manager/get_vehicle_profile", + std::bind( + &VehicleProfileManagerNode::handle_get_profile, this, + std::placeholders::_1, std::placeholders::_2)); + + register_service_ = create_service( + "/vehicle_profile_manager/register_or_update_vehicle_profile", + std::bind( + &VehicleProfileManagerNode::handle_register, this, + std::placeholders::_1, std::placeholders::_2)); + + applicability_service_ = create_service( + "/vehicle_profile_manager/evaluate_vehicle_calibration_applicability", + std::bind( + &VehicleProfileManagerNode::handle_applicability, this, + std::placeholders::_1, std::placeholders::_2)); + + heartbeat_service_ = create_service( + "/vehicle_profile_manager/heartbeat", + std::bind( + &VehicleProfileManagerNode::handle_heartbeat, this, + std::placeholders::_1, std::placeholders::_2)); + + load_profiles_from_file(); + load_demo_profile_if_empty(); + + RCLCPP_INFO( + get_logger(), "VehicleProfileManagerNode 启动,当前画像数量=%zu,存储文件=%s", + profiles_.size(), profile_storage_path_.c_str()); +} + +void VehicleProfileManagerNode::handle_get_profile( + const std::shared_ptr request, + std::shared_ptr response) +{ + const auto & vehicle_id = request->request.vehicle_id; + auto it = profiles_.find(vehicle_id); + if (it == profiles_.end()) { + response->response.success = false; + response->response.error_code.code = ErrorCode::INVALID_ARGUMENT; + response->response.message = "找不到 vehicle_id=[" + vehicle_id + "] 的车辆画像。"; + return; + } + response->response.success = true; + response->response.error_code.code = ErrorCode::OK; + response->response.message = "查询成功。"; + response->response.profile = it->second; +} + +void VehicleProfileManagerNode::handle_register( + const std::shared_ptr request, + std::shared_ptr response) +{ + const auto & vehicle_id = request->request.profile.base_info.vehicle_id; + if (vehicle_id.empty()) { + response->response.success = false; + response->response.error_code.code = ErrorCode::INVALID_ARGUMENT; + response->response.message = "vehicle_id 不能为空。"; + return; + } + auto profile = request->request.profile; + profile.enabled_workflow_stages = evaluate_supported_stages(profile); + profiles_[vehicle_id] = profile; + + std::string save_error; + if (!save_profiles_to_file(save_error)) { + response->response.success = false; + response->response.error_code.code = ErrorCode::INTERNAL_ERROR; + response->response.message = save_error; + return; + } + + RCLCPP_INFO(get_logger(), "已注册/更新车辆画像 vehicle_id=[%s]", vehicle_id.c_str()); + response->response.success = true; + response->response.error_code.code = ErrorCode::OK; + response->response.message = "注册/更新成功。"; +} + +void VehicleProfileManagerNode::handle_applicability( + const std::shared_ptr request, + std::shared_ptr response) +{ + const auto & profile = request->request.profile_snapshot; + auto stages = evaluate_supported_stages(profile); + + response->response.success = true; + response->response.error_code.code = ErrorCode::OK; + response->response.message = "适用性评估完成。"; + response->response.overall_supported = !stages.empty(); + response->response.recommended_workflow_stages = stages; +} + +void VehicleProfileManagerNode::handle_heartbeat( + const std::shared_ptr /*request*/, + std::shared_ptr response) +{ + const auto now_us = std::chrono::duration_cast( + std::chrono::system_clock::now().time_since_epoch()).count(); + response->response.success = true; + response->response.error_code.code = ErrorCode::OK; + response->response.message = "vehicle_profile_manager 在线。"; + response->response.server_timestamp_us = now_us; + response->response.vehicle_ready = true; +} + +std::vector +VehicleProfileManagerNode::evaluate_supported_stages(const VehicleProfile & profile) const +{ + std::vector stages; + + auto make_stage = [](uint8_t v) { + WorkflowStageType s; + s.value = v; + return s; + }; + + // 预检和画像校验始终支持。 + stages.push_back(make_stage(WorkflowStageType::PROFILE_VALIDATION_STAGE)); + stages.push_back(make_stage(WorkflowStageType::WORKSHOP_PRECHECK_STAGE)); + + // 底盘标定:底盘类型已指定时支持。 + if (profile.chassis_type.value != ChassisType::CHASSIS_TYPE_UNSPECIFIED) { + stages.push_back(make_stage(WorkflowStageType::CHASSIS_CALIBRATION_STAGE)); + stages.push_back(make_stage(WorkflowStageType::CONTROL_CALIBRATION_STAGE)); + } + + // 传感器标定:有传感器配置时支持。 + if (!profile.sensors.empty()) { + stages.push_back(make_stage(WorkflowStageType::SENSOR_INTRINSIC_CALIBRATION_STAGE)); + stages.push_back(make_stage(WorkflowStageType::SENSOR_EXTRINSIC_CALIBRATION_STAGE)); + } + + // 手眼标定:有机械臂且有传感器时支持。 + if (profile.arm_profile.has_mechanical_arm && !profile.sensors.empty()) { + stages.push_back(make_stage(WorkflowStageType::HAND_EYE_CALIBRATION_STAGE)); + } + + // 最终阶段始终加入。 + stages.push_back(make_stage(WorkflowStageType::PARAMETER_COMMIT_STAGE)); + stages.push_back(make_stage(WorkflowStageType::REPORT_ARCHIVE_STAGE)); + + return stages; +} + +void VehicleProfileManagerNode::load_profiles_from_file() +{ + profiles_.clear(); + if (profile_storage_path_.empty()) { + RCLCPP_WARN(get_logger(), "profile_storage_path 为空,跳过画像文件加载。"); + return; + } + if (!std::filesystem::exists(profile_storage_path_)) { + RCLCPP_WARN(get_logger(), "画像存储文件不存在:%s,后续将回退到 demo 画像。", profile_storage_path_.c_str()); + return; + } + + std::ifstream ifs(profile_storage_path_); + if (!ifs.is_open()) { + RCLCPP_WARN(get_logger(), "无法打开画像存储文件:%s", profile_storage_path_.c_str()); + return; + } + + std::stringstream buffer; + buffer << ifs.rdbuf(); + for (const auto & block : parse_profile_blocks(buffer.str())) { + auto profile = make_profile_from_kv_map(block); + if (!profile.base_info.vehicle_id.empty()) { + profile.enabled_workflow_stages = evaluate_supported_stages(profile); + profiles_[profile.base_info.vehicle_id] = profile; + } + } +} + +bool VehicleProfileManagerNode::save_profiles_to_file(std::string & error_message) const +{ + if (profile_storage_path_.empty()) { + error_message = "profile_storage_path 为空,无法保存车辆画像。"; + return false; + } + + const auto parent = std::filesystem::path(profile_storage_path_).parent_path(); + if (!parent.empty()) { + std::error_code ec; + std::filesystem::create_directories(parent, ec); + if (ec) { + error_message = "创建画像存储目录失败:" + ec.message(); + return false; + } + } + + std::ofstream ofs(profile_storage_path_, std::ios::trunc); + if (!ofs.is_open()) { + error_message = "无法打开画像存储文件进行写入:" + profile_storage_path_; + return false; + } + + for (const auto & [vehicle_id, profile] : profiles_) { + ofs << "[profile]\n"; + ofs << "vehicle_id=" << vehicle_id << "\n"; + ofs << "vehicle_name=" << profile.base_info.vehicle_name << "\n"; + ofs << "model_name=" << profile.base_info.model_name << "\n"; + ofs << "serial_number=" << profile.base_info.serial_number << "\n"; + ofs << "manufacturer=" << profile.base_info.manufacturer << "\n"; + ofs << "description=" << profile.base_info.description << "\n"; + ofs << "profile_version=" << profile.profile_version << "\n"; + ofs << "base_link_frame=" << profile.base_link_frame << "\n"; + ofs << "chassis_type=" << static_cast(profile.chassis_type.value) << "\n"; + ofs << "has_mechanical_arm=" << (profile.arm_profile.has_mechanical_arm ? "true" : "false") << "\n"; + ofs << "arm_id=" << profile.arm_profile.arm_id << "\n"; + ofs << "arm_model=" << profile.arm_profile.arm_model << "\n"; + ofs << "arm_dof=" << profile.arm_profile.dof << "\n"; + ofs << "arm_base_frame=" << profile.arm_profile.arm_base_frame << "\n"; + ofs << "tool_frame=" << profile.arm_profile.tool_frame << "\n"; + + for (const auto & sensor : profile.sensors) { + ofs << "sensor=" + << sensor.sensor_id << "," + << static_cast(sensor.sensor_type.value) << "," + << sensor.sensor_name << "," + << sensor.frame_id << "," + << static_cast(sensor.camera_mount_type.value) << "," + << (sensor.enabled ? "true" : "false") << "," + << (sensor.needs_intrinsic_calibration ? "true" : "false") << "," + << (sensor.needs_extrinsic_calibration ? "true" : "false") << "," + << sensor.device_hint << "," + << (sensor.selected_for_this_session ? "true" : "false") << "," + << (sensor.supports_intrinsic_calibration ? "true" : "false") << "," + << (sensor.supports_extrinsic_calibration ? "true" : "false") << "\n"; + } + + for (const auto & cap : profile.capabilities) { + ofs << "capability=" + << static_cast(cap.ability_type.value) << "," + << (cap.supported ? "true" : "false") << "," + << cap.message << "\n"; + } + + for (const auto & controller : profile.controllers) { + ofs << "controller=" + << static_cast(controller.control_axis.value) << "," + << static_cast(controller.default_algorithm.value) << "\n"; + } + + ofs << "\n"; + } + + return true; +} + +void VehicleProfileManagerNode::load_demo_profile_if_empty() +{ + if (!profiles_.empty()) { + return; + } + load_demo_profile(); + std::string save_error; + if (!save_profiles_to_file(save_error)) { + RCLCPP_WARN(get_logger(), "写入 demo 画像到存储文件失败:%s", save_error.c_str()); + } +} + +VehicleProfileManagerNode::VehicleProfile VehicleProfileManagerNode::make_profile_from_kv_map( + const std::unordered_map & values) const +{ + VehicleProfile profile; + auto get = [&](const std::string & key) -> std::string { + const auto it = values.find(key); + return it == values.end() ? std::string() : it->second; + }; + + profile.base_info.vehicle_id = get("vehicle_id"); + profile.base_info.vehicle_name = get("vehicle_name"); + profile.base_info.model_name = get("model_name"); + profile.base_info.serial_number = get("serial_number"); + profile.base_info.manufacturer = get("manufacturer"); + profile.base_info.description = get("description"); + profile.profile_version = get("profile_version"); + profile.base_link_frame = get("base_link_frame"); + + try { + if (!get("chassis_type").empty()) { + profile.chassis_type.value = static_cast(std::stoul(get("chassis_type"))); + } + if (!get("arm_dof").empty()) { + profile.arm_profile.dof = static_cast(std::stoul(get("arm_dof"))); + } + } catch (...) { + } + + profile.arm_profile.has_mechanical_arm = parse_bool_text(get("has_mechanical_arm")); + profile.arm_profile.arm_id = get("arm_id"); + profile.arm_profile.arm_model = get("arm_model"); + profile.arm_profile.arm_base_frame = get("arm_base_frame"); + profile.arm_profile.tool_frame = get("tool_frame"); + + const auto sensor_rows = split_pipe_rows(get("sensor_rows")); + for (const auto & row : sensor_rows) { + auto parts = split_csv(row); + if (parts.size() < 12) { + continue; + } + calibration_vehicle_profile_interfaces::msg::SensorProfile sensor; + sensor.sensor_id = parts[0]; + sensor.sensor_type.value = static_cast(std::stoul(parts[1])); + sensor.sensor_name = parts[2]; + sensor.frame_id = parts[3]; + sensor.camera_mount_type.value = static_cast(std::stoul(parts[4])); + sensor.enabled = parse_bool_text(parts[5]); + sensor.needs_intrinsic_calibration = parse_bool_text(parts[6]); + sensor.needs_extrinsic_calibration = parse_bool_text(parts[7]); + sensor.device_hint = parts[8]; + sensor.selected_for_this_session = parse_bool_text(parts[9]); + sensor.supports_intrinsic_calibration = parse_bool_text(parts[10]); + sensor.supports_extrinsic_calibration = parse_bool_text(parts[11]); + profile.sensors.push_back(sensor); + } + + const auto capability_rows = split_pipe_rows(get("capability_rows")); + for (const auto & row : capability_rows) { + auto parts = split_csv(row); + if (parts.size() < 3) { + continue; + } + calibration_vehicle_profile_interfaces::msg::CalibrationCapability capability; + capability.ability_type.value = static_cast(std::stoul(parts[0])); + capability.supported = parse_bool_text(parts[1]); + capability.message = parts[2]; + profile.capabilities.push_back(capability); + } + + const auto controller_rows = split_pipe_rows(get("controller_rows")); + for (const auto & row : controller_rows) { + auto parts = split_csv(row); + if (parts.size() < 2) { + continue; + } + calibration_vehicle_profile_interfaces::msg::ControllerProfile controller; + controller.control_axis.value = static_cast(std::stoul(parts[0])); + controller.default_algorithm.value = static_cast(std::stoul(parts[1])); + profile.controllers.push_back(controller); + } + + return profile; +} + +std::vector> VehicleProfileManagerNode::parse_profile_blocks( + const std::string & text) const +{ + std::vector> blocks; + std::unordered_map current; + std::stringstream ss(text); + std::string line; + std::vector sensor_rows; + std::vector capability_rows; + std::vector controller_rows; + + auto flush_current = [&]() { + if (!current.empty()) { + std::string sensor_rows_joined; + for (size_t i = 0; i < sensor_rows.size(); ++i) { + if (i != 0) { + sensor_rows_joined += "|"; + } + sensor_rows_joined += sensor_rows[i]; + } + std::string capability_rows_joined; + for (size_t i = 0; i < capability_rows.size(); ++i) { + if (i != 0) { + capability_rows_joined += "|"; + } + capability_rows_joined += capability_rows[i]; + } + std::string controller_rows_joined; + for (size_t i = 0; i < controller_rows.size(); ++i) { + if (i != 0) { + controller_rows_joined += "|"; + } + controller_rows_joined += controller_rows[i]; + } + current["sensor_rows"] = sensor_rows_joined; + current["capability_rows"] = capability_rows_joined; + current["controller_rows"] = controller_rows_joined; + blocks.push_back(current); + } + current.clear(); + sensor_rows.clear(); + capability_rows.clear(); + controller_rows.clear(); + }; + + while (std::getline(ss, line)) { + line = trim_copy(line); + if (line.empty()) { + flush_current(); + continue; + } + if (line == "[profile]") { + flush_current(); + continue; + } + const auto sep = line.find('='); + if (sep == std::string::npos) { + continue; + } + const auto key = trim_copy(line.substr(0, sep)); + const auto value = trim_copy(line.substr(sep + 1)); + if (key == "sensor") { + sensor_rows.push_back(value); + } else if (key == "capability") { + capability_rows.push_back(value); + } else if (key == "controller") { + controller_rows.push_back(value); + } else { + current[key] = value; + } + } + flush_current(); + return blocks; +} + +void VehicleProfileManagerNode::load_demo_profile() +{ + VehicleProfile demo; + + // 基础信息 + demo.base_info.vehicle_id = "demo_agv_001"; + demo.base_info.vehicle_name = "Demo AGV"; + demo.base_info.model_name = "DemoModel-X1"; + demo.base_info.manufacturer = "Demo Manufacturer"; + + // 底盘类型:差速 + demo.chassis_type.value = ChassisType::DIFFERENTIAL; + + // base_link + demo.base_link_frame = "base_link"; + + // 画像版本 + demo.profile_version = "demo_v1"; + + calibration_vehicle_profile_interfaces::msg::SensorProfile front_camera; + front_camera.sensor_id = "demo_front_camera"; + front_camera.sensor_type.value = SensorType::FRONT_CAMERA; + front_camera.sensor_name = "Demo Front Camera"; + front_camera.frame_id = "front_camera_link"; + front_camera.camera_mount_type.value = CameraMountType::FRONT_MOUNTED; + front_camera.enabled = true; + front_camera.needs_intrinsic_calibration = true; + front_camera.needs_extrinsic_calibration = true; + front_camera.device_hint = "/dev/video0"; + front_camera.selected_for_this_session = true; + front_camera.supports_intrinsic_calibration = true; + front_camera.supports_extrinsic_calibration = true; + demo.sensors.push_back(front_camera); + + calibration_vehicle_profile_interfaces::msg::CalibrationCapability external_cap; + external_cap.ability_type.value = CalibrationAbilityType::EXTERNAL_LOCALIZATION_CALIBRATION; + external_cap.supported = true; + external_cap.message = "demo external localization supported"; + demo.capabilities.push_back(external_cap); + + calibration_vehicle_profile_interfaces::msg::CalibrationCapability chassis_cap; + chassis_cap.ability_type.value = CalibrationAbilityType::CHASSIS_CALIBRATION; + chassis_cap.supported = true; + chassis_cap.message = "demo chassis calibration supported"; + demo.capabilities.push_back(chassis_cap); + + calibration_vehicle_profile_interfaces::msg::CalibrationCapability control_cap; + control_cap.ability_type.value = CalibrationAbilityType::CONTROL_CALIBRATION; + control_cap.supported = true; + control_cap.message = "demo control calibration supported"; + demo.capabilities.push_back(control_cap); + + calibration_vehicle_profile_interfaces::msg::CalibrationCapability sensor_cap; + sensor_cap.ability_type.value = CalibrationAbilityType::SENSOR_CALIBRATION; + sensor_cap.supported = true; + sensor_cap.message = "demo sensor calibration supported"; + demo.capabilities.push_back(sensor_cap); + + calibration_vehicle_profile_interfaces::msg::ControllerProfile controller; + controller.control_axis.value = ControlAxisType::LATERAL_CONTROL; + controller.default_algorithm.value = ControllerAlgorithmType::PURE_PURSUIT; + demo.controllers.push_back(controller); + + // 启用的工作流阶段 + demo.enabled_workflow_stages = evaluate_supported_stages(demo); + + profiles_[demo.base_info.vehicle_id] = demo; + RCLCPP_INFO(get_logger(), "已预载 demo 车辆画像 vehicle_id=[%s]", + demo.base_info.vehicle_id.c_str()); +} + +} // namespace vehicle_profile_manager + +RCLCPP_COMPONENTS_REGISTER_NODE(vehicle_profile_manager::VehicleProfileManagerNode) diff --git a/agv_calib_brain/src/agv_calib_core/workshop_orchestrator/ANNOTATION_NOTES.md b/agv_calib_brain/src/core/agv_calib_core/workshop_orchestrator/ANNOTATION_NOTES.md similarity index 100% rename from agv_calib_brain/src/agv_calib_core/workshop_orchestrator/ANNOTATION_NOTES.md rename to agv_calib_brain/src/core/agv_calib_core/workshop_orchestrator/ANNOTATION_NOTES.md diff --git a/agv_calib_brain/src/agv_calib_core/workshop_orchestrator/CMakeLists.txt b/agv_calib_brain/src/core/agv_calib_core/workshop_orchestrator/CMakeLists.txt similarity index 92% rename from agv_calib_brain/src/agv_calib_core/workshop_orchestrator/CMakeLists.txt rename to agv_calib_brain/src/core/agv_calib_core/workshop_orchestrator/CMakeLists.txt index 7cc5eb4..fd9c460 100644 --- a/agv_calib_brain/src/agv_calib_core/workshop_orchestrator/CMakeLists.txt +++ b/agv_calib_brain/src/core/agv_calib_core/workshop_orchestrator/CMakeLists.txt @@ -19,6 +19,7 @@ find_package(calibration_external_localization_interfaces REQUIRED) find_package(calibration_chassis_interfaces REQUIRED) find_package(calibration_control_interfaces REQUIRED) find_package(calibration_sensor_interfaces REQUIRED) +find_package(nlohmann_json REQUIRED) include_directories(include) @@ -31,6 +32,7 @@ add_executable(workshop_orchestrator_v2_node src/chassis_gateway_client.cpp src/control_gateway_client.cpp src/sensor_gateway_client.cpp + src/wifi6_link_client.cpp src/workshop_orchestrator_v2_node.cpp ) @@ -47,6 +49,10 @@ ament_target_dependencies(workshop_orchestrator_v2_node calibration_sensor_interfaces ) +target_link_libraries(workshop_orchestrator_v2_node + nlohmann_json::nlohmann_json +) + install(TARGETS workshop_orchestrator_v2_node DESTINATION lib/${PROJECT_NAME} ) diff --git a/agv_calib_brain/src/agv_calib_core/workshop_orchestrator/INTEGRATION_NOTES.md b/agv_calib_brain/src/core/agv_calib_core/workshop_orchestrator/INTEGRATION_NOTES.md similarity index 85% rename from agv_calib_brain/src/agv_calib_core/workshop_orchestrator/INTEGRATION_NOTES.md rename to agv_calib_brain/src/core/agv_calib_core/workshop_orchestrator/INTEGRATION_NOTES.md index 484c60e..9c36cd8 100644 --- a/agv_calib_brain/src/agv_calib_core/workshop_orchestrator/INTEGRATION_NOTES.md +++ b/agv_calib_brain/src/core/agv_calib_core/workshop_orchestrator/INTEGRATION_NOTES.md @@ -1,6 +1,6 @@ 把这个包接入你现在的仓库时,建议放到: -`src/win_ubuntu_bridge/service/workshop_orchestrator_v2` +`src/communication/win_ubuntu_bridge/service/workshop_orchestrator_v2` 如果你已经创建了同名包,可以直接把 `include/`、`src/`、`launch/` 里的对应文件合并进去。 diff --git a/agv_calib_brain/src/agv_calib_core/workshop_orchestrator/MIGRATION_NOTES.md b/agv_calib_brain/src/core/agv_calib_core/workshop_orchestrator/MIGRATION_NOTES.md similarity index 100% rename from agv_calib_brain/src/agv_calib_core/workshop_orchestrator/MIGRATION_NOTES.md rename to agv_calib_brain/src/core/agv_calib_core/workshop_orchestrator/MIGRATION_NOTES.md diff --git a/agv_calib_brain/src/agv_calib_core/workshop_orchestrator/README.md b/agv_calib_brain/src/core/agv_calib_core/workshop_orchestrator/README.md similarity index 57% rename from agv_calib_brain/src/agv_calib_core/workshop_orchestrator/README.md rename to agv_calib_brain/src/core/agv_calib_core/workshop_orchestrator/README.md index 870dc19..00a1f8e 100644 --- a/agv_calib_brain/src/agv_calib_core/workshop_orchestrator/README.md +++ b/agv_calib_brain/src/core/agv_calib_core/workshop_orchestrator/README.md @@ -1,4 +1,4 @@ -# workshop_orchestrator +# 车间总控编排模块 这是自动化标定车间的总编排说明文档。这个 README 需要跟着代码一起更新,用来保留当前流程设计、任务粒度和模块职责。 @@ -27,7 +27,27 @@ - 阶段审批 - 报告生成与查询 -### 2. 任务选择方式 +### 2. WiFi6 链路调度 + +车间工控机到车端电脑只使用一个统一 WiFi6/TCP 入口,默认是: + +```text +wifi6_vehicle_host: 127.0.0.1 +wifi6_vehicle_port: 9000 +``` + +总控预检时会在同一个 TCP endpoint 上依次检查四个逻辑通道: + +| 逻辑通道 | 帧类型 | 作用 | +|---|---|---| +| 底盘 `CHASSIS` | `1 -> 2` | 检查底盘 agent、急停、运动安全状态 | +| 运控 `CONTROL` | `11 -> 12` | 检查轨迹执行器、车辆反馈、控制输出 | +| 传感器 `SENSOR` | `21 -> 22` | 检查车端传感器采集和数据流状态 | +| 外部真值 `EXTERNAL_POSE` | `31 -> 32` | 检查外部真值位姿能否通过 WiFi6 推入车端 | + +这里的“四条链路”是协议里的逻辑通道,不是四个物理端口。仿真环境中如果底盘、运控、传感器、外部真值仍由不同脚本承载,可以启动 `vehicle_wifi6_gateway_sim.py`,由它监听 `9000` 并转发到内部后端端口。 + +### 3. 任务选择方式 当前设计是“**由 UI 或上层显式选择要执行的阶段**”。 @@ -36,7 +56,7 @@ ## 当前执行主线 -`CreateSession -> BuildPlan -> Precheck -> Stage Execution -> Commit/Validation -> Report` +当前主线为:创建会话 -> 生成计划 -> 执行预检 -> 阶段执行 -> 提交/验收 -> 生成报告。 其中阶段执行会按会话配置和车辆画像决定是否加入: @@ -71,12 +91,12 @@ | 模块 | 职责 | 输入 | 输出 | 实现位置 | |---|---|---|---|---| -| 车间总控 `workshop_orchestrator_v2` | 管会话、生成计划、调度执行、处理人工确认/审批、生成报告 | `VehicleProfile`、`WorkshopSessionConfig`、`RequestedCalibrationTask`、`StagePlan`、`StageResultSummary` | `WorkshopSession`、`WorkshopPrecheckResponse`、`WorkshopReport`、`WorkshopEvent` | `src/agv_calib_core/workshop_orchestrator/src/main.cpp`
`src/agv_calib_core/workshop_orchestrator/src/workshop_orchestrator_v2_node.cpp`
`src/agv_calib_core/workshop_orchestrator/include/workshop_orchestrator_v2/workshop_orchestrator_v2_node.hpp`
`src/agv_calib_core/workshop_orchestrator/src/plan_builder.cpp` / `include/workshop_orchestrator_v2/plan_builder.hpp`
`src/agv_calib_core/workshop_orchestrator/src/precheck_runner.cpp` / `include/workshop_orchestrator_v2/precheck_runner.hpp`
`src/agv_calib_core/workshop_orchestrator/src/report_builder.cpp` / `include/workshop_orchestrator_v2/report_builder.hpp`
`src/agv_calib_core/workshop_orchestrator/include/workshop_orchestrator_v2/types.hpp` | -| 外部定位 / 真值接入 `external_localization_service` | goal 校验、外部参考接入、真值校核、结果回填 | `ExecuteExternalLocalizationTask::Goal`、`WorkshopSession`、`StagePlan` | `StageResultSummary`、`ExecuteExternalLocalizationTask::Result` | `src/agv_calib_core/external_localization_service/include/external_localization_service/external_reference_executor.hpp`
`src/agv_calib_core/external_localization_service/src/external_reference_executor.cpp` | -| 底盘标定客户端 | readiness 检查、底盘动作原语任务下发、结果翻译 | `WorkshopSession`、`StagePlan` | `StageResultSummary`、底盘专项 goal / result | `src/agv_calib_core/workshop_orchestrator/include/workshop_orchestrator_v2/chassis_gateway_client.hpp`
`src/agv_calib_core/workshop_orchestrator/src/chassis_gateway_client.cpp` | -| 运控参数标定客户端 | readiness 检查、控制评估任务下发、轨迹组装、结果翻译 | `WorkshopSession`、`StagePlan`、`stage.metadata` 中的轨迹与参数 | `StageResultSummary`、运控专项 goal / result | `src/agv_calib_core/workshop_orchestrator/include/workshop_orchestrator_v2/control_gateway_client.hpp`
`src/agv_calib_core/workshop_orchestrator/src/control_gateway_client.cpp` | -| 传感器标定客户端 | readiness 检查、传感器任务路由、结果翻译 | `WorkshopSession`、`StagePlan`、`stage.metadata` 中的传感器信息 | `StageResultSummary`、传感器专项 goal / result | `src/agv_calib_core/workshop_orchestrator/include/workshop_orchestrator_v2/sensor_gateway_client.hpp`
`src/agv_calib_core/workshop_orchestrator/src/sensor_gateway_client.cpp` | -| 外部定位客户端 | readiness 检查、外部参考接入任务下发、结果接入总控 | `WorkshopSession`、`StagePlan` | `StageResultSummary`、外部定位专项 goal / result | `src/agv_calib_core/workshop_orchestrator/include/workshop_orchestrator_v2/external_localization_client.hpp`
`src/agv_calib_core/workshop_orchestrator/src/external_localization_client.cpp` | +| 车间总控 `workshop_orchestrator_v2` | 管会话、生成计划、调度执行、处理人工确认/审批、生成报告 | `VehicleProfile`、`WorkshopSessionConfig`、`RequestedCalibrationTask`、`StagePlan`、`StageResultSummary` | `WorkshopSession`、`WorkshopPrecheckResponse`、`WorkshopReport`、`WorkshopEvent` | `src/core/agv_calib_core/workshop_orchestrator/src/main.cpp`
`src/core/agv_calib_core/workshop_orchestrator/src/workshop_orchestrator_v2_node.cpp`
`src/core/agv_calib_core/workshop_orchestrator/include/workshop_orchestrator_v2/workshop_orchestrator_v2_node.hpp`
`src/core/agv_calib_core/workshop_orchestrator/src/plan_builder.cpp` / `include/workshop_orchestrator_v2/plan_builder.hpp`
`src/core/agv_calib_core/workshop_orchestrator/src/precheck_runner.cpp` / `include/workshop_orchestrator_v2/precheck_runner.hpp`
`src/core/agv_calib_core/workshop_orchestrator/src/report_builder.cpp` / `include/workshop_orchestrator_v2/report_builder.hpp`
`src/core/agv_calib_core/workshop_orchestrator/include/workshop_orchestrator_v2/types.hpp` | +| 外部定位 / 真值接入 `external_localization_service` | goal 校验、外部参考接入、真值校核、结果回填 | `ExecuteExternalLocalizationTask::Goal`、`WorkshopSession`、`StagePlan` | `StageResultSummary`、`ExecuteExternalLocalizationTask::Result` | `src/core/agv_calib_core/external_localization_service/include/external_localization_service/external_reference_executor.hpp`
`src/core/agv_calib_core/external_localization_service/src/external_reference_executor.cpp` | +| 底盘标定客户端 | readiness 检查、底盘动作原语任务下发、结果翻译 | `WorkshopSession`、`StagePlan` | `StageResultSummary`、底盘专项 goal / result | `src/core/agv_calib_core/workshop_orchestrator/include/workshop_orchestrator_v2/chassis_gateway_client.hpp`
`src/core/agv_calib_core/workshop_orchestrator/src/chassis_gateway_client.cpp` | +| 运控参数标定客户端 | readiness 检查、控制评估任务下发、轨迹组装、结果翻译 | `WorkshopSession`、`StagePlan`、`stage.metadata` 中的轨迹与参数 | `StageResultSummary`、运控专项 goal / result | `src/core/agv_calib_core/workshop_orchestrator/include/workshop_orchestrator_v2/control_gateway_client.hpp`
`src/core/agv_calib_core/workshop_orchestrator/src/control_gateway_client.cpp` | +| 传感器标定客户端 | readiness 检查、传感器任务路由、结果翻译 | `WorkshopSession`、`StagePlan`、`stage.metadata` 中的传感器信息 | `StageResultSummary`、传感器专项 goal / result | `src/core/agv_calib_core/workshop_orchestrator/include/workshop_orchestrator_v2/sensor_gateway_client.hpp`
`src/core/agv_calib_core/workshop_orchestrator/src/sensor_gateway_client.cpp` | +| 外部定位客户端 | readiness 检查、外部参考接入任务下发、结果接入总控 | `WorkshopSession`、`StagePlan` | `StageResultSummary`、外部定位专项 goal / result | `src/core/agv_calib_core/workshop_orchestrator/include/workshop_orchestrator_v2/external_localization_client.hpp`
`src/core/agv_calib_core/workshop_orchestrator/src/external_localization_client.cpp` | ### 1. 车间总控模块 `workshop_orchestrator_v2` @@ -100,13 +120,13 @@ - `WorkshopEvent` **实现位置** -- 节点入口:`src/agv_calib_core/workshop_orchestrator/src/main.cpp` -- 总控节点:`src/agv_calib_core/workshop_orchestrator/src/workshop_orchestrator_v2_node.cpp` -- 总控头文件:`src/agv_calib_core/workshop_orchestrator/include/workshop_orchestrator_v2/workshop_orchestrator_v2_node.hpp` -- 阶段规划:`src/agv_calib_core/workshop_orchestrator/src/plan_builder.cpp` / `include/workshop_orchestrator_v2/plan_builder.hpp` -- 预检逻辑:`src/agv_calib_core/workshop_orchestrator/src/precheck_runner.cpp` / `include/workshop_orchestrator_v2/precheck_runner.hpp` -- 报告生成:`src/agv_calib_core/workshop_orchestrator/src/report_builder.cpp` / `include/workshop_orchestrator_v2/report_builder.hpp` -- 共享类型:`src/agv_calib_core/workshop_orchestrator/include/workshop_orchestrator_v2/types.hpp` +- 节点入口:`src/core/agv_calib_core/workshop_orchestrator/src/main.cpp` +- 总控节点:`src/core/agv_calib_core/workshop_orchestrator/src/workshop_orchestrator_v2_node.cpp` +- 总控头文件:`src/core/agv_calib_core/workshop_orchestrator/include/workshop_orchestrator_v2/workshop_orchestrator_v2_node.hpp` +- 阶段规划:`src/core/agv_calib_core/workshop_orchestrator/src/plan_builder.cpp` / `include/workshop_orchestrator_v2/plan_builder.hpp` +- 预检逻辑:`src/core/agv_calib_core/workshop_orchestrator/src/precheck_runner.cpp` / `include/workshop_orchestrator_v2/precheck_runner.hpp` +- 报告生成:`src/core/agv_calib_core/workshop_orchestrator/src/report_builder.cpp` / `include/workshop_orchestrator_v2/report_builder.hpp` +- 共享类型:`src/core/agv_calib_core/workshop_orchestrator/include/workshop_orchestrator_v2/types.hpp` ### 2. 外部定位 / 真值接入模块 `external_localization_service` @@ -125,8 +145,8 @@ - `ExecuteExternalLocalizationTask::Result` **实现位置** -- 执行器头文件:`src/agv_calib_core/external_localization_service/include/external_localization_service/external_reference_executor.hpp` -- 执行器实现:`src/agv_calib_core/external_localization_service/src/external_reference_executor.cpp` +- 执行器头文件:`src/core/agv_calib_core/external_localization_service/include/external_localization_service/external_reference_executor.hpp` +- 执行器实现:`src/core/agv_calib_core/external_localization_service/src/external_reference_executor.cpp` ### 3. 底盘标定客户端 @@ -144,8 +164,8 @@ - 底盘专项 goal / result **实现位置** -- 头文件:`src/agv_calib_core/workshop_orchestrator/include/workshop_orchestrator_v2/chassis_gateway_client.hpp` -- 实现:`src/agv_calib_core/workshop_orchestrator/src/chassis_gateway_client.cpp` +- 头文件:`src/core/agv_calib_core/workshop_orchestrator/include/workshop_orchestrator_v2/chassis_gateway_client.hpp` +- 实现:`src/core/agv_calib_core/workshop_orchestrator/src/chassis_gateway_client.cpp` ### 4. 运控参数标定客户端 @@ -165,8 +185,8 @@ - 运控专项 goal / result **实现位置** -- 头文件:`src/agv_calib_core/workshop_orchestrator/include/workshop_orchestrator_v2/control_gateway_client.hpp` -- 实现:`src/agv_calib_core/workshop_orchestrator/src/control_gateway_client.cpp` +- 头文件:`src/core/agv_calib_core/workshop_orchestrator/include/workshop_orchestrator_v2/control_gateway_client.hpp` +- 实现:`src/core/agv_calib_core/workshop_orchestrator/src/control_gateway_client.cpp` ### 5. 传感器标定客户端 @@ -186,8 +206,8 @@ - 传感器专项 goal / result **实现位置** -- 头文件:`src/agv_calib_core/workshop_orchestrator/include/workshop_orchestrator_v2/sensor_gateway_client.hpp` -- 实现:`src/agv_calib_core/workshop_orchestrator/src/sensor_gateway_client.cpp` +- 头文件:`src/core/agv_calib_core/workshop_orchestrator/include/workshop_orchestrator_v2/sensor_gateway_client.hpp` +- 实现:`src/core/agv_calib_core/workshop_orchestrator/src/sensor_gateway_client.cpp` ### 6. 外部定位客户端 @@ -205,8 +225,8 @@ - 外部定位专项 goal / result **实现位置** -- 头文件:`src/agv_calib_core/workshop_orchestrator/include/workshop_orchestrator_v2/external_localization_client.hpp` -- 实现:`src/agv_calib_core/workshop_orchestrator/src/external_localization_client.cpp` +- 头文件:`src/core/agv_calib_core/workshop_orchestrator/include/workshop_orchestrator_v2/external_localization_client.hpp` +- 实现:`src/core/agv_calib_core/workshop_orchestrator/src/external_localization_client.cpp` ### 7. 外部定位专项执行骨架 @@ -223,8 +243,8 @@ - `StageResultSummary` **实现位置** -- 头文件:`src/agv_calib_core/external_localization_service/include/external_localization_service/external_reference_executor.hpp` -- 实现:`src/agv_calib_core/external_localization_service/src/external_reference_executor.cpp` +- 头文件:`src/core/agv_calib_core/external_localization_service/include/external_localization_service/external_reference_executor.hpp` +- 实现:`src/core/agv_calib_core/external_localization_service/src/external_reference_executor.cpp` ## 专项任务清单 @@ -233,9 +253,9 @@ ### 1. 外部定位 / 真值接入 **对应代码** -- `src/agv_calib_core/workshop_orchestrator/src/plan_builder.cpp` -- `src/agv_calib_core/workshop_orchestrator/src/external_localization_client.cpp` -- `src/agv_calib_core/external_localization_service/src/external_reference_executor.cpp` +- `src/core/agv_calib_core/workshop_orchestrator/src/plan_builder.cpp` +- `src/core/agv_calib_core/workshop_orchestrator/src/external_localization_client.cpp` +- `src/core/agv_calib_core/external_localization_service/src/external_reference_executor.cpp` **当前任务** - 外部真值参考接入 @@ -251,8 +271,8 @@ ### 2. 底盘标定 **对应代码** -- `src/agv_calib_core/workshop_orchestrator/src/plan_builder.cpp` -- `src/agv_calib_core/workshop_orchestrator/src/chassis_gateway_client.cpp` +- `src/core/agv_calib_core/workshop_orchestrator/src/plan_builder.cpp` +- `src/core/agv_calib_core/workshop_orchestrator/src/chassis_gateway_client.cpp` **当前任务** - 底盘 readiness 检查 @@ -267,8 +287,8 @@ ### 3. 运控参数标定 **对应代码** -- `src/agv_calib_core/workshop_orchestrator/src/plan_builder.cpp` -- `src/agv_calib_core/workshop_orchestrator/src/control_gateway_client.cpp` +- `src/core/agv_calib_core/workshop_orchestrator/src/plan_builder.cpp` +- `src/core/agv_calib_core/workshop_orchestrator/src/control_gateway_client.cpp` **当前任务** - 运控 readiness 检查 @@ -284,8 +304,8 @@ ### 4. 传感器内参标定 **对应代码** -- `src/agv_calib_core/workshop_orchestrator/src/plan_builder.cpp` -- `src/agv_calib_core/workshop_orchestrator/src/sensor_gateway_client.cpp` +- `src/core/agv_calib_core/workshop_orchestrator/src/plan_builder.cpp` +- `src/core/agv_calib_core/workshop_orchestrator/src/sensor_gateway_client.cpp` **当前任务** - 相机内参标定 @@ -302,8 +322,8 @@ ### 5. 传感器外参标定 **对应代码** -- `src/agv_calib_core/workshop_orchestrator/src/plan_builder.cpp` -- `src/agv_calib_core/workshop_orchestrator/src/sensor_gateway_client.cpp` +- `src/core/agv_calib_core/workshop_orchestrator/src/plan_builder.cpp` +- `src/core/agv_calib_core/workshop_orchestrator/src/sensor_gateway_client.cpp` **当前任务** - 相机外参标定 @@ -321,8 +341,8 @@ ### 6. 手眼标定 **对应代码** -- `src/agv_calib_core/workshop_orchestrator/src/plan_builder.cpp` -- `src/agv_calib_core/workshop_orchestrator/src/sensor_gateway_client.cpp` +- `src/core/agv_calib_core/workshop_orchestrator/src/plan_builder.cpp` +- `src/core/agv_calib_core/workshop_orchestrator/src/sensor_gateway_client.cpp` **当前任务** - 眼在手上(EYE_IN_HAND) diff --git a/agv_calib_brain/src/agv_calib_core/workshop_orchestrator/include/workshop_orchestrator_v2/chassis_gateway_client.hpp b/agv_calib_brain/src/core/agv_calib_core/workshop_orchestrator/include/workshop_orchestrator_v2/chassis_gateway_client.hpp similarity index 100% rename from agv_calib_brain/src/agv_calib_core/workshop_orchestrator/include/workshop_orchestrator_v2/chassis_gateway_client.hpp rename to agv_calib_brain/src/core/agv_calib_core/workshop_orchestrator/include/workshop_orchestrator_v2/chassis_gateway_client.hpp diff --git a/agv_calib_brain/src/agv_calib_core/workshop_orchestrator/include/workshop_orchestrator_v2/control_gateway_client.hpp b/agv_calib_brain/src/core/agv_calib_core/workshop_orchestrator/include/workshop_orchestrator_v2/control_gateway_client.hpp similarity index 100% rename from agv_calib_brain/src/agv_calib_core/workshop_orchestrator/include/workshop_orchestrator_v2/control_gateway_client.hpp rename to agv_calib_brain/src/core/agv_calib_core/workshop_orchestrator/include/workshop_orchestrator_v2/control_gateway_client.hpp diff --git a/agv_calib_brain/src/agv_calib_core/workshop_orchestrator/include/workshop_orchestrator_v2/external_localization_client.hpp b/agv_calib_brain/src/core/agv_calib_core/workshop_orchestrator/include/workshop_orchestrator_v2/external_localization_client.hpp similarity index 100% rename from agv_calib_brain/src/agv_calib_core/workshop_orchestrator/include/workshop_orchestrator_v2/external_localization_client.hpp rename to agv_calib_brain/src/core/agv_calib_core/workshop_orchestrator/include/workshop_orchestrator_v2/external_localization_client.hpp diff --git a/agv_calib_brain/src/agv_calib_core/workshop_orchestrator/include/workshop_orchestrator_v2/plan_builder.hpp b/agv_calib_brain/src/core/agv_calib_core/workshop_orchestrator/include/workshop_orchestrator_v2/plan_builder.hpp similarity index 70% rename from agv_calib_brain/src/agv_calib_core/workshop_orchestrator/include/workshop_orchestrator_v2/plan_builder.hpp rename to agv_calib_brain/src/core/agv_calib_core/workshop_orchestrator/include/workshop_orchestrator_v2/plan_builder.hpp index e407663..cf7c8f4 100644 --- a/agv_calib_brain/src/agv_calib_core/workshop_orchestrator/include/workshop_orchestrator_v2/plan_builder.hpp +++ b/agv_calib_brain/src/core/agv_calib_core/workshop_orchestrator/include/workshop_orchestrator_v2/plan_builder.hpp @@ -49,13 +49,34 @@ private: uint8_t ability_type) const; // 各阶段的构造方法。 - StagePlan make_external_reference_stage(int order_index, const RequestedCalibrationTask * task_cfg) const; - StagePlan make_chassis_stage(int order_index, const RequestedCalibrationTask * task_cfg) const; - StagePlan make_control_stage(int order_index, const RequestedCalibrationTask * task_cfg) const; - StagePlan make_sensor_intrinsic_stage(int order_index, const RequestedCalibrationTask * task_cfg) const; - StagePlan make_sensor_extrinsic_stage(int order_index, const RequestedCalibrationTask * task_cfg) const; - StagePlan make_hand_eye_stage(int order_index, const RequestedCalibrationTask * task_cfg) const; - StagePlan make_stage_for_task(int order_index, const RequestedCalibrationTask & task) const; + StagePlan make_external_reference_stage( + int order_index, + const VehicleProfile & profile, + const RequestedCalibrationTask * task_cfg) const; + StagePlan make_chassis_stage( + int order_index, + const VehicleProfile & profile, + const RequestedCalibrationTask * task_cfg) const; + StagePlan make_control_stage( + int order_index, + const VehicleProfile & profile, + const RequestedCalibrationTask * task_cfg) const; + StagePlan make_sensor_intrinsic_stage( + int order_index, + const VehicleProfile & profile, + const RequestedCalibrationTask * task_cfg) const; + StagePlan make_sensor_extrinsic_stage( + int order_index, + const VehicleProfile & profile, + const RequestedCalibrationTask * task_cfg) const; + StagePlan make_hand_eye_stage( + int order_index, + const VehicleProfile & profile, + const RequestedCalibrationTask * task_cfg) const; + StagePlan make_stage_for_task( + int order_index, + const VehicleProfile & profile, + const RequestedCalibrationTask & task) const; // 把 RequestedCalibrationTask 中的策略和配置应用到 StagePlan 上。 void apply_task_config(StagePlan & stage, const RequestedCalibrationTask * task_cfg) const; diff --git a/agv_calib_brain/src/agv_calib_core/workshop_orchestrator/include/workshop_orchestrator_v2/precheck_runner.hpp b/agv_calib_brain/src/core/agv_calib_core/workshop_orchestrator/include/workshop_orchestrator_v2/precheck_runner.hpp similarity index 100% rename from agv_calib_brain/src/agv_calib_core/workshop_orchestrator/include/workshop_orchestrator_v2/precheck_runner.hpp rename to agv_calib_brain/src/core/agv_calib_core/workshop_orchestrator/include/workshop_orchestrator_v2/precheck_runner.hpp diff --git a/agv_calib_brain/src/agv_calib_core/workshop_orchestrator/include/workshop_orchestrator_v2/report_builder.hpp b/agv_calib_brain/src/core/agv_calib_core/workshop_orchestrator/include/workshop_orchestrator_v2/report_builder.hpp similarity index 100% rename from agv_calib_brain/src/agv_calib_core/workshop_orchestrator/include/workshop_orchestrator_v2/report_builder.hpp rename to agv_calib_brain/src/core/agv_calib_core/workshop_orchestrator/include/workshop_orchestrator_v2/report_builder.hpp diff --git a/agv_calib_brain/src/agv_calib_core/workshop_orchestrator/include/workshop_orchestrator_v2/sensor_gateway_client.hpp b/agv_calib_brain/src/core/agv_calib_core/workshop_orchestrator/include/workshop_orchestrator_v2/sensor_gateway_client.hpp similarity index 100% rename from agv_calib_brain/src/agv_calib_core/workshop_orchestrator/include/workshop_orchestrator_v2/sensor_gateway_client.hpp rename to agv_calib_brain/src/core/agv_calib_core/workshop_orchestrator/include/workshop_orchestrator_v2/sensor_gateway_client.hpp diff --git a/agv_calib_brain/src/agv_calib_core/workshop_orchestrator/include/workshop_orchestrator_v2/types.hpp b/agv_calib_brain/src/core/agv_calib_core/workshop_orchestrator/include/workshop_orchestrator_v2/types.hpp similarity index 69% rename from agv_calib_brain/src/agv_calib_core/workshop_orchestrator/include/workshop_orchestrator_v2/types.hpp rename to agv_calib_brain/src/core/agv_calib_core/workshop_orchestrator/include/workshop_orchestrator_v2/types.hpp index 8e37f9b..327ca18 100644 --- a/agv_calib_brain/src/agv_calib_core/workshop_orchestrator/include/workshop_orchestrator_v2/types.hpp +++ b/agv_calib_brain/src/core/agv_calib_core/workshop_orchestrator/include/workshop_orchestrator_v2/types.hpp @@ -94,12 +94,46 @@ inline constexpr const char * CHASSIS_PRIMITIVE_TYPE = "primitive_type"; inline constexpr const char * CHASSIS_STRAIGHT_LINE_DISTANCE_M = "straight_line.target_distance_m"; inline constexpr const char * CHASSIS_STRAIGHT_LINE_SPEED_MS = "straight_line.target_speed_ms"; inline constexpr const char * CHASSIS_STRAIGHT_LINE_REVERSE = "straight_line.reverse"; +inline constexpr const char * CHASSIS_ARC_SPEED_MS = "arc.target_speed_ms"; +inline constexpr const char * CHASSIS_ARC_RADIUS_M = "arc.radius_m"; +inline constexpr const char * CHASSIS_ARC_SWEEP_ANGLE_DEG = "arc.sweep_angle_deg"; +inline constexpr const char * CHASSIS_ARC_CLOCKWISE = "arc.clockwise"; +inline constexpr const char * CHASSIS_ROTATION_TARGET_YAW_DEG = "in_place_rotation.target_yaw_deg"; +inline constexpr const char * CHASSIS_ROTATION_TARGET_ANGULAR_VEL_DEG_S = "in_place_rotation.target_angular_vel_deg_s"; +inline constexpr const char * CHASSIS_STEERING_SWEEP_TARGET_ANGLE_DEG = "steering_sweep.target_angle_deg"; +inline constexpr const char * CHASSIS_STEERING_SWEEP_AMPLITUDE_DEG = "steering_sweep.sweep_amplitude_deg"; +inline constexpr const char * CHASSIS_STEERING_SWEEP_FREQUENCY_HZ = "steering_sweep.sweep_frequency_hz"; +inline constexpr const char * CHASSIS_STEERING_SWEEP_DURATION_SEC = "steering_sweep.duration_sec"; +inline constexpr const char * CHASSIS_LATERAL_SPEED_MS = "lateral_translation.target_speed_ms"; +inline constexpr const char * CHASSIS_LATERAL_DISTANCE_M = "lateral_translation.target_distance_m"; +inline constexpr const char * CHASSIS_LATERAL_MOVE_LEFT = "lateral_translation.move_left"; +inline constexpr const char * CHASSIS_DIAGONAL_SPEED_MS = "diagonal_motion.target_speed_ms"; +inline constexpr const char * CHASSIS_DIAGONAL_DISTANCE_M = "diagonal_motion.target_distance_m"; +inline constexpr const char * CHASSIS_DIAGONAL_HEADING_DEG = "diagonal_motion.heading_deg"; +inline constexpr const char * CHASSIS_MODULE_ALIGNMENT_IDS = "module_alignment.module_ids"; +inline constexpr const char * CHASSIS_MODULE_ALIGNMENT_ZERO_DEG = "module_alignment.target_zero_deg"; +inline constexpr const char * CHASSIS_MODULE_ALIGNMENT_TOLERANCE_DEG = "module_alignment.tolerance_deg"; +inline constexpr const char * CHASSIS_COORDINATED_STEERING_IDS = "coordinated_steering.module_ids"; +inline constexpr const char * CHASSIS_COORDINATED_STEERING_TARGET_ANGLE_DEG = "coordinated_steering.target_angle_deg"; +inline constexpr const char * CHASSIS_COORDINATED_STEERING_HOLD_TIME_SEC = "coordinated_steering.hold_time_sec"; inline constexpr const char * COMMON_BRAKE_WHEN_FINISHED = "brake_when_finished"; inline constexpr const char * COMMON_TIMEOUT_SEC = "timeout_sec"; // control inline constexpr const char * CONTROL_TASK_TYPE = "control.task_type"; inline constexpr const char * CONTROL_TRAJECTORY_PREFIX = "traj_pt_"; +inline constexpr const char * CONTROL_STOP_AT_END = "control.stop_at_end"; +inline constexpr const char * CONTROL_TIMEOUT_SEC = "control.timeout_sec"; +inline constexpr const char * CONTROL_VELOCITY_STEP_TARGET_VELOCITY_MS = "velocity_step.target_velocity_ms"; +inline constexpr const char * CONTROL_VELOCITY_STEP_HOLD_TIME_SEC = "velocity_step.hold_time_sec"; +inline constexpr const char * CONTROL_VELOCITY_STEP_SETTLE_BEFORE_STEP_SEC = "velocity_step.settle_before_step_sec"; +inline constexpr const char * CONTROL_ACCEL_DECEL_START_VELOCITY_MS = "accel_decel.start_velocity_ms"; +inline constexpr const char * CONTROL_ACCEL_DECEL_TARGET_VELOCITY_MS = "accel_decel.target_velocity_ms"; +inline constexpr const char * CONTROL_ACCEL_DECEL_TARGET_ACCEL_MS2 = "accel_decel.target_accel_ms2"; +inline constexpr const char * CONTROL_ACCEL_DECEL_HOLD_TIME_SEC = "accel_decel.hold_time_sec"; +inline constexpr const char * CONTROL_STOP_ACCURACY_X_M = "stop_accuracy.target_stop_x_m"; +inline constexpr const char * CONTROL_STOP_ACCURACY_Y_M = "stop_accuracy.target_stop_y_m"; +inline constexpr const char * CONTROL_STOP_ACCURACY_YAW_RAD = "stop_accuracy.target_stop_yaw_rad"; // sensor inline constexpr const char * SENSOR_ID = "sensor.sensor_id"; @@ -107,6 +141,9 @@ inline constexpr const char * SENSOR_TASK_SUBTYPE = "sensor.task_subtype"; inline constexpr const char * CAMERA_INTRINSIC_REQUIRED_IMAGE_COUNT = "camera_intrinsic.required_image_count"; inline constexpr const char * CAMERA_INTRINSIC_TARGET_BOARD_ID = "camera_intrinsic.target_board_id"; inline constexpr const char * CAMERA_INTRINSIC_TIMEOUT_SEC = "camera_intrinsic.timeout_sec"; +inline constexpr const char * IMU_INTRINSIC_REQUIRED_STATIC_SEGMENT_COUNT = "imu_intrinsic.required_static_segment_count"; +inline constexpr const char * IMU_INTRINSIC_REQUIRED_MOTION_SEGMENT_COUNT = "imu_intrinsic.required_motion_segment_count"; +inline constexpr const char * IMU_INTRINSIC_TIMEOUT_SEC = "imu_intrinsic.timeout_sec"; inline constexpr const char * SENSOR_EXTRINSIC_BASE_FRAME_ID = "sensor_extrinsic.base_frame_id"; inline constexpr const char * SENSOR_EXTRINSIC_REQUIRED_SAMPLE_COUNT = "sensor_extrinsic.required_sample_count"; inline constexpr const char * SENSOR_EXTRINSIC_TIMEOUT_SEC = "sensor_extrinsic.timeout_sec"; diff --git a/agv_calib_brain/src/core/agv_calib_core/workshop_orchestrator/include/workshop_orchestrator_v2/wifi6_link_client.hpp b/agv_calib_brain/src/core/agv_calib_core/workshop_orchestrator/include/workshop_orchestrator_v2/wifi6_link_client.hpp new file mode 100644 index 0000000..450323f --- /dev/null +++ b/agv_calib_brain/src/core/agv_calib_core/workshop_orchestrator/include/workshop_orchestrator_v2/wifi6_link_client.hpp @@ -0,0 +1,56 @@ +#pragma once + +#include +#include + +namespace workshop_orchestrator_v2 +{ + +// 对齐 src/communication/win_ubuntu_bridge/*/proto/transport_contract.proto。 +// 这个 client 只用于 orchestrator 预检四条 WiFi6/TCP 链路,不承载专项算法逻辑。 +class Wifi6LinkClient +{ +public: + struct Config + { + std::string vehicle_host{"127.0.0.1"}; + uint16_t vehicle_port{9000}; + int timeout_ms{5000}; + }; + + struct CheckResult + { + bool passed{false}; + std::string message; + }; + + explicit Wifi6LinkClient(Config config); + + CheckResult check_chassis() const; + CheckResult check_control() const; + CheckResult check_sensor() const; + CheckResult check_external_pose() const; + +private: + enum class FrameType : uint32_t + { + CHASSIS_GET_READINESS_REQ = 1, + CHASSIS_GET_READINESS_RSP = 2, + CONTROL_GET_READINESS_REQ = 11, + CONTROL_GET_READINESS_RSP = 12, + SENSOR_GET_READINESS_REQ = 21, + SENSOR_GET_READINESS_RSP = 22, + EXTERNAL_POSE_PUSH_REQ = 31, + EXTERNAL_POSE_PUSH_RSP = 32, + }; + + CheckResult send_json_request( + FrameType request_type, + FrameType response_type, + const std::string & request_payload, + const std::string & link_name) const; + + Config config_; +}; + +} // namespace workshop_orchestrator_v2 diff --git a/agv_calib_brain/src/agv_calib_core/workshop_orchestrator/include/workshop_orchestrator_v2/workshop_orchestrator_v2_node.hpp b/agv_calib_brain/src/core/agv_calib_core/workshop_orchestrator/include/workshop_orchestrator_v2/workshop_orchestrator_v2_node.hpp similarity index 94% rename from agv_calib_brain/src/agv_calib_core/workshop_orchestrator/include/workshop_orchestrator_v2/workshop_orchestrator_v2_node.hpp rename to agv_calib_brain/src/core/agv_calib_core/workshop_orchestrator/include/workshop_orchestrator_v2/workshop_orchestrator_v2_node.hpp index fa28bfc..ba0049a 100644 --- a/agv_calib_brain/src/agv_calib_core/workshop_orchestrator/include/workshop_orchestrator_v2/workshop_orchestrator_v2_node.hpp +++ b/agv_calib_brain/src/core/agv_calib_core/workshop_orchestrator/include/workshop_orchestrator_v2/workshop_orchestrator_v2_node.hpp @@ -9,6 +9,8 @@ #include "rclcpp/rclcpp.hpp" // ROS 2 普通节点基础能力。 #include "rclcpp_action/rclcpp_action.hpp" // ROS 2 action 能力,适合承载"整场任务执行"这种长操作。 +#include "rcl_interfaces/msg/set_parameters_result.hpp" + #include "calibration_workshop_orchestration_interfaces/action/execute_workshop_session.hpp" // 外部要求主控开始跑某次标定任务的 action。 #include "calibration_workshop_orchestration_interfaces/msg/workshop_event.hpp" // 发给界面/日志/上位机的进度通知消息。 #include "calibration_workshop_orchestration_interfaces/msg/workshop_event_type.hpp" // 进度通知里的事件类型枚举。 @@ -28,6 +30,7 @@ #include "workshop_orchestrator_v2/chassis_gateway_client.hpp" // 底盘模块薄 client。 #include "workshop_orchestrator_v2/control_gateway_client.hpp" // 运控模块薄 client。 #include "workshop_orchestrator_v2/sensor_gateway_client.hpp" // 传感器模块薄 client。 +#include "workshop_orchestrator_v2/wifi6_link_client.hpp" // 四条 WiFi6/TCP 链路预检。 namespace workshop_orchestrator_v2 { @@ -91,6 +94,15 @@ private: void execute_session(const std::shared_ptr goal_handle); SessionExecutionContext prepare_session_for_run(const std::string & session_id); WorkshopPrecheckResponse run_precheck_step(const std::string & session_id); + void run_wifi6_link_precheck(const WorkshopSession & session, WorkshopPrecheckResponse & precheck); + void append_precheck_item( + WorkshopPrecheckResponse & precheck, + const std::string & item_code, + const std::string & display_name, + bool passed, + const std::string & message, + uint8_t stage_type, + uint8_t module_type); void mark_session_running(const std::string & session_id); bool handle_cancel_if_requested( const std::shared_ptr goal_handle, @@ -208,6 +220,10 @@ private: std::unique_ptr chassis_gateway_client_; std::unique_ptr control_gateway_client_; std::unique_ptr sensor_gateway_client_; + std::unique_ptr wifi6_link_client_; + bool wifi6_precheck_enabled_{true}; + bool wifi6_precheck_require_all_links_{true}; + rclcpp::node_interfaces::OnSetParametersCallbackHandle::SharedPtr parameter_callback_handle_; // ─── ROS 接口 ─── rclcpp::Service::SharedPtr create_session_service_; diff --git a/agv_calib_brain/src/core/agv_calib_core/workshop_orchestrator/launch/workshop_orchestrator_v2.launch.py b/agv_calib_brain/src/core/agv_calib_core/workshop_orchestrator/launch/workshop_orchestrator_v2.launch.py new file mode 100644 index 0000000..02febe8 --- /dev/null +++ b/agv_calib_brain/src/core/agv_calib_core/workshop_orchestrator/launch/workshop_orchestrator_v2.launch.py @@ -0,0 +1,30 @@ +from launch import LaunchDescription +from launch.actions import DeclareLaunchArgument +from launch.substitutions import LaunchConfiguration +from launch_ros.actions import Node +from launch_ros.parameter_descriptions import ParameterValue + + +def generate_launch_description(): + return LaunchDescription([ + DeclareLaunchArgument("wifi6_vehicle_host", default_value="127.0.0.1"), + DeclareLaunchArgument("wifi6_vehicle_port", default_value="9000"), + DeclareLaunchArgument("wifi6_timeout_ms", default_value="5000"), + DeclareLaunchArgument("wifi6_precheck_enabled", default_value="true"), + DeclareLaunchArgument("wifi6_precheck_require_all_links", default_value="true"), + Node( + package="workshop_orchestrator_v2", + executable="workshop_orchestrator_v2_node", + name="workshop_orchestrator_v2", + output="screen", + parameters=[{ + "wifi6_vehicle_host": LaunchConfiguration("wifi6_vehicle_host"), + "wifi6_vehicle_port": ParameterValue(LaunchConfiguration("wifi6_vehicle_port"), value_type=int), + "wifi6_timeout_ms": ParameterValue(LaunchConfiguration("wifi6_timeout_ms"), value_type=int), + "wifi6_precheck_enabled": ParameterValue( + LaunchConfiguration("wifi6_precheck_enabled"), value_type=bool), + "wifi6_precheck_require_all_links": ParameterValue( + LaunchConfiguration("wifi6_precheck_require_all_links"), value_type=bool), + }], + ) + ]) diff --git a/agv_calib_brain/src/agv_calib_core/workshop_orchestrator/package.xml b/agv_calib_brain/src/core/agv_calib_core/workshop_orchestrator/package.xml similarity index 100% rename from agv_calib_brain/src/agv_calib_core/workshop_orchestrator/package.xml rename to agv_calib_brain/src/core/agv_calib_core/workshop_orchestrator/package.xml diff --git a/agv_calib_brain/src/agv_calib_core/workshop_orchestrator/src/chassis_gateway_client.cpp b/agv_calib_brain/src/core/agv_calib_core/workshop_orchestrator/src/chassis_gateway_client.cpp similarity index 50% rename from agv_calib_brain/src/agv_calib_core/workshop_orchestrator/src/chassis_gateway_client.cpp rename to agv_calib_brain/src/core/agv_calib_core/workshop_orchestrator/src/chassis_gateway_client.cpp index b4c0e99..84b0fda 100644 --- a/agv_calib_brain/src/agv_calib_core/workshop_orchestrator/src/chassis_gateway_client.cpp +++ b/agv_calib_brain/src/core/agv_calib_core/workshop_orchestrator/src/chassis_gateway_client.cpp @@ -1,9 +1,11 @@ #include "workshop_orchestrator_v2/chassis_gateway_client.hpp" #include +#include #include #include #include +#include #include "workshop_orchestrator_v2/types.hpp" @@ -17,6 +19,61 @@ using calibration_workshop_orchestration_interfaces::msg::StagePlan; using calibration_workshop_orchestration_interfaces::msg::StageResultSummary; using calibration_workshop_orchestration_interfaces::msg::WorkshopSession; +namespace +{ + +bool parse_bool_value(const std::string & value) +{ + return value == "true" || value == "1" || value == "TRUE" || value == "True"; +} + +bool parse_double_metadata( + const std::unordered_map & metadata_map, + const std::string & key, + double & output) +{ + const auto it = metadata_map.find(key); + if (it == metadata_map.end()) { + return false; + } + try { + output = std::stod(it->second); + return true; + } catch (...) { + return false; + } +} + +bool parse_string_list_metadata( + const std::unordered_map & metadata_map, + const std::string & key, + std::vector & output) +{ + const auto it = metadata_map.find(key); + if (it == metadata_map.end()) { + return false; + } + + output.clear(); + std::string token; + for (char ch : it->second) { + if (ch == ',' || ch == ';' || std::isspace(static_cast(ch))) { + if (!token.empty()) { + output.push_back(token); + token.clear(); + } + continue; + } + token.push_back(ch); + } + if (!token.empty()) { + output.push_back(token); + } + return !output.empty(); +} + +} // namespace + ChassisGatewayClient::ChassisGatewayClient(rclcpp::Node * node) : node_(node) { @@ -115,35 +172,170 @@ bool ChassisGatewayClient::build_goal( return false; } - if (primitive_it->second != "straight_line") { - failure_reason = "当前最小流仅支持 straight_line 底盘动作原语。"; + if (primitive_it->second == "straight_line") { + double target_distance_m = 0.0; + double target_speed_ms = 0.0; + if (!parse_double_metadata( + metadata_map, metadata_keys::CHASSIS_STRAIGHT_LINE_DISTANCE_M, target_distance_m) || + !parse_double_metadata( + metadata_map, metadata_keys::CHASSIS_STRAIGHT_LINE_SPEED_MS, target_speed_ms)) + { + failure_reason = "straight_line 缺少 target_distance_m 或 target_speed_ms,或参数格式错误。"; + return false; + } + + goal.goal.selected_primitive.value = ChassisMotionPrimitiveType::STRAIGHT_LINE; + goal.goal.straight_line.target_distance_m = target_distance_m; + goal.goal.straight_line.target_speed_ms = target_speed_ms; + const auto reverse_it = metadata_map.find(metadata_keys::CHASSIS_STRAIGHT_LINE_REVERSE); + goal.goal.straight_line.reverse = + reverse_it != metadata_map.end() && parse_bool_value(reverse_it->second); + } else if (primitive_it->second == "arc") { + double target_speed_ms = 0.0; + double radius_m = 0.0; + double sweep_angle_deg = 0.0; + if (!parse_double_metadata( + metadata_map, metadata_keys::CHASSIS_ARC_SPEED_MS, target_speed_ms) || + !parse_double_metadata( + metadata_map, metadata_keys::CHASSIS_ARC_RADIUS_M, radius_m) || + !parse_double_metadata( + metadata_map, metadata_keys::CHASSIS_ARC_SWEEP_ANGLE_DEG, sweep_angle_deg)) + { + failure_reason = "arc 缺少 target_speed_ms、radius_m 或 sweep_angle_deg,或参数格式错误。"; + return false; + } + + goal.goal.selected_primitive.value = ChassisMotionPrimitiveType::ARC; + goal.goal.arc.target_speed_ms = target_speed_ms; + goal.goal.arc.radius_m = radius_m; + goal.goal.arc.sweep_angle_deg = sweep_angle_deg; + const auto clockwise_it = metadata_map.find(metadata_keys::CHASSIS_ARC_CLOCKWISE); + goal.goal.arc.clockwise = + clockwise_it != metadata_map.end() && parse_bool_value(clockwise_it->second); + } else if (primitive_it->second == "in_place_rotation") { + double target_yaw_deg = 0.0; + double target_angular_vel_deg_s = 0.0; + if (!parse_double_metadata( + metadata_map, metadata_keys::CHASSIS_ROTATION_TARGET_YAW_DEG, target_yaw_deg) || + !parse_double_metadata( + metadata_map, metadata_keys::CHASSIS_ROTATION_TARGET_ANGULAR_VEL_DEG_S, + target_angular_vel_deg_s)) + { + failure_reason = "in_place_rotation 缺少 target_yaw_deg 或 target_angular_vel_deg_s,或参数格式错误。"; + return false; + } + + goal.goal.selected_primitive.value = ChassisMotionPrimitiveType::IN_PLACE_ROTATION; + goal.goal.in_place_rotation.target_yaw_deg = target_yaw_deg; + goal.goal.in_place_rotation.target_angular_vel_deg_s = target_angular_vel_deg_s; + } else if (primitive_it->second == "steering_sweep") { + double target_angle_deg = 0.0; + double sweep_amplitude_deg = 0.0; + double sweep_frequency_hz = 0.0; + double duration_sec = 0.0; + if (!parse_double_metadata( + metadata_map, metadata_keys::CHASSIS_STEERING_SWEEP_TARGET_ANGLE_DEG, target_angle_deg) || + !parse_double_metadata( + metadata_map, metadata_keys::CHASSIS_STEERING_SWEEP_AMPLITUDE_DEG, sweep_amplitude_deg) || + !parse_double_metadata( + metadata_map, metadata_keys::CHASSIS_STEERING_SWEEP_FREQUENCY_HZ, sweep_frequency_hz) || + !parse_double_metadata( + metadata_map, metadata_keys::CHASSIS_STEERING_SWEEP_DURATION_SEC, duration_sec)) + { + failure_reason = "steering_sweep 缺少目标角、幅值、频率或持续时间,或参数格式错误。"; + return false; + } + + goal.goal.selected_primitive.value = ChassisMotionPrimitiveType::STEERING_SWEEP; + goal.goal.steering_sweep.target_angle_deg = target_angle_deg; + goal.goal.steering_sweep.sweep_amplitude_deg = sweep_amplitude_deg; + goal.goal.steering_sweep.sweep_frequency_hz = sweep_frequency_hz; + goal.goal.steering_sweep.duration_sec = duration_sec; + } else if (primitive_it->second == "lateral_translation") { + double target_speed_ms = 0.0; + double target_distance_m = 0.0; + if (!parse_double_metadata( + metadata_map, metadata_keys::CHASSIS_LATERAL_SPEED_MS, target_speed_ms) || + !parse_double_metadata( + metadata_map, metadata_keys::CHASSIS_LATERAL_DISTANCE_M, target_distance_m)) + { + failure_reason = "lateral_translation 缺少 target_speed_ms 或 target_distance_m,或参数格式错误。"; + return false; + } + + goal.goal.selected_primitive.value = ChassisMotionPrimitiveType::LATERAL_TRANSLATION; + goal.goal.lateral_translation.target_speed_ms = target_speed_ms; + goal.goal.lateral_translation.target_distance_m = target_distance_m; + const auto move_left_it = metadata_map.find(metadata_keys::CHASSIS_LATERAL_MOVE_LEFT); + goal.goal.lateral_translation.move_left = + move_left_it != metadata_map.end() && parse_bool_value(move_left_it->second); + } else if (primitive_it->second == "diagonal_motion") { + double target_speed_ms = 0.0; + double target_distance_m = 0.0; + double heading_deg = 0.0; + if (!parse_double_metadata( + metadata_map, metadata_keys::CHASSIS_DIAGONAL_SPEED_MS, target_speed_ms) || + !parse_double_metadata( + metadata_map, metadata_keys::CHASSIS_DIAGONAL_DISTANCE_M, target_distance_m) || + !parse_double_metadata( + metadata_map, metadata_keys::CHASSIS_DIAGONAL_HEADING_DEG, heading_deg)) + { + failure_reason = "diagonal_motion 缺少 target_speed_ms、target_distance_m 或 heading_deg,或参数格式错误。"; + return false; + } + + goal.goal.selected_primitive.value = ChassisMotionPrimitiveType::DIAGONAL_MOTION; + goal.goal.diagonal_motion.target_speed_ms = target_speed_ms; + goal.goal.diagonal_motion.target_distance_m = target_distance_m; + goal.goal.diagonal_motion.heading_deg = heading_deg; + } else if (primitive_it->second == "module_alignment") { + std::vector module_ids; + double target_zero_deg = 0.0; + double tolerance_deg = 0.0; + if (!parse_string_list_metadata( + metadata_map, metadata_keys::CHASSIS_MODULE_ALIGNMENT_IDS, module_ids) || + !parse_double_metadata( + metadata_map, metadata_keys::CHASSIS_MODULE_ALIGNMENT_ZERO_DEG, target_zero_deg) || + !parse_double_metadata( + metadata_map, metadata_keys::CHASSIS_MODULE_ALIGNMENT_TOLERANCE_DEG, tolerance_deg)) + { + failure_reason = "module_alignment 缺少 module_ids、target_zero_deg 或 tolerance_deg,或参数格式错误。"; + return false; + } + + goal.goal.selected_primitive.value = ChassisMotionPrimitiveType::MODULE_ALIGNMENT; + goal.goal.module_alignment.module_ids = module_ids; + goal.goal.module_alignment.target_zero_deg = target_zero_deg; + goal.goal.module_alignment.tolerance_deg = tolerance_deg; + } else if (primitive_it->second == "coordinated_steering") { + std::vector module_ids; + double target_angle_deg = 0.0; + double hold_time_sec = 0.0; + if (!parse_string_list_metadata( + metadata_map, metadata_keys::CHASSIS_COORDINATED_STEERING_IDS, module_ids) || + !parse_double_metadata( + metadata_map, metadata_keys::CHASSIS_COORDINATED_STEERING_TARGET_ANGLE_DEG, + target_angle_deg) || + !parse_double_metadata( + metadata_map, metadata_keys::CHASSIS_COORDINATED_STEERING_HOLD_TIME_SEC, hold_time_sec)) + { + failure_reason = "coordinated_steering 缺少 module_ids、target_angle_deg 或 hold_time_sec,或参数格式错误。"; + return false; + } + + goal.goal.selected_primitive.value = ChassisMotionPrimitiveType::COORDINATED_STEERING; + goal.goal.coordinated_steering.module_ids = module_ids; + goal.goal.coordinated_steering.target_angle_deg = target_angle_deg; + goal.goal.coordinated_steering.hold_time_sec = hold_time_sec; + } else { + failure_reason = "不支持的底盘动作原语类型:" + primitive_it->second; return false; } - const auto distance_it = metadata_map.find(metadata_keys::CHASSIS_STRAIGHT_LINE_DISTANCE_M); - const auto speed_it = metadata_map.find(metadata_keys::CHASSIS_STRAIGHT_LINE_SPEED_MS); - if (distance_it == metadata_map.end() || speed_it == metadata_map.end()) { - failure_reason = "straight_line 缺少 target_distance_m 或 target_speed_ms。"; - return false; - } - - goal.goal.selected_primitive.value = ChassisMotionPrimitiveType::STRAIGHT_LINE; - try { - goal.goal.straight_line.target_distance_m = std::stod(distance_it->second); - goal.goal.straight_line.target_speed_ms = std::stod(speed_it->second); - } catch (...) { - failure_reason = "straight_line 参数格式错误,无法解析为数字。"; - return false; - } - - const auto reverse_it = metadata_map.find(metadata_keys::CHASSIS_STRAIGHT_LINE_REVERSE); - goal.goal.straight_line.reverse = - reverse_it != metadata_map.end() && (reverse_it->second == "true" || reverse_it->second == "1"); - goal.goal.brake_when_finished = true; const auto brake_it = metadata_map.find(metadata_keys::COMMON_BRAKE_WHEN_FINISHED); if (brake_it != metadata_map.end()) { - goal.goal.brake_when_finished = brake_it->second == "true" || brake_it->second == "1"; + goal.goal.brake_when_finished = parse_bool_value(brake_it->second); } goal.goal.timeout_sec = 60.0; diff --git a/agv_calib_brain/src/agv_calib_core/workshop_orchestrator/src/control_gateway_client.cpp b/agv_calib_brain/src/core/agv_calib_core/workshop_orchestrator/src/control_gateway_client.cpp similarity index 68% rename from agv_calib_brain/src/agv_calib_core/workshop_orchestrator/src/control_gateway_client.cpp rename to agv_calib_brain/src/core/agv_calib_core/workshop_orchestrator/src/control_gateway_client.cpp index 06b22ae..f44a115 100644 --- a/agv_calib_brain/src/agv_calib_core/workshop_orchestrator/src/control_gateway_client.cpp +++ b/agv_calib_brain/src/core/agv_calib_core/workshop_orchestrator/src/control_gateway_client.cpp @@ -108,8 +108,17 @@ uint8_t ControlGatewayClient::resolve_task_type( if (task_type_it->second == "trajectory_tracking") { return ControllerEvaluationTaskType::TRAJECTORY_TRACKING; } + if (task_type_it->second == "velocity_step") { + return ControllerEvaluationTaskType::VELOCITY_STEP; + } + if (task_type_it->second == "accel_decel") { + return ControllerEvaluationTaskType::ACCELERATION_DECELERATION; + } + if (task_type_it->second == "stop_accuracy") { + return ControllerEvaluationTaskType::STOP_ACCURACY; + } - failure_reason = "当前最小流仅支持 trajectory_tracking 运控任务。"; + failure_reason = "不支持的运控任务类型:" + task_type_it->second; return ControllerEvaluationTaskType::CONTROL_EVALUATION_TASK_UNSPECIFIED; } @@ -140,16 +149,95 @@ bool ControlGatewayClient::build_goal( } goal.goal.selected_task.value = selected_task_type; - goal.goal.trajectory_tracking.stop_at_end = true; // 执行完后停车,保证安全。 - goal.goal.trajectory_tracking.timeout_sec = 60.0; + std::unordered_map metadata_map; + for (const auto & kv : stage.metadata) { + metadata_map[kv.key] = kv.value; + } - // 轨迹必须由流程配置显式提供;编排器不再隐式生成默认轨迹。 - goal.goal.trajectory_tracking.path = build_trajectory_from_metadata(stage, failure_reason); - if (goal.goal.trajectory_tracking.path.empty()) { - if (failure_reason.empty()) { - failure_reason = "运控阶段缺少轨迹输入,无法启动评估任务。"; + if (selected_task_type == ControllerEvaluationTaskType::TRAJECTORY_TRACKING) { + goal.goal.trajectory_tracking.stop_at_end = true; // 执行完后停车,保证安全。 + goal.goal.trajectory_tracking.timeout_sec = 60.0; + + const auto stop_it = metadata_map.find(metadata_keys::CONTROL_STOP_AT_END); + if (stop_it != metadata_map.end()) { + goal.goal.trajectory_tracking.stop_at_end = + stop_it->second == "true" || stop_it->second == "1" || stop_it->second == "TRUE"; + } + + const auto timeout_it = metadata_map.find(metadata_keys::CONTROL_TIMEOUT_SEC); + if (timeout_it != metadata_map.end()) { + try { + goal.goal.trajectory_tracking.timeout_sec = std::stod(timeout_it->second); + } catch (...) { + failure_reason = "control.timeout_sec 格式错误。"; + return false; + } + } + + // 轨迹必须由流程配置显式提供;编排器不再隐式生成默认轨迹。 + goal.goal.trajectory_tracking.path = build_trajectory_from_metadata(stage, failure_reason); + if (goal.goal.trajectory_tracking.path.empty()) { + if (failure_reason.empty()) { + failure_reason = "运控阶段缺少轨迹输入,无法启动评估任务。"; + } + return false; + } + } else if (selected_task_type == ControllerEvaluationTaskType::VELOCITY_STEP) { + const auto velocity_it = metadata_map.find(metadata_keys::CONTROL_VELOCITY_STEP_TARGET_VELOCITY_MS); + const auto hold_it = metadata_map.find(metadata_keys::CONTROL_VELOCITY_STEP_HOLD_TIME_SEC); + const auto settle_it = metadata_map.find(metadata_keys::CONTROL_VELOCITY_STEP_SETTLE_BEFORE_STEP_SEC); + if (velocity_it == metadata_map.end() || hold_it == metadata_map.end() || settle_it == metadata_map.end()) { + failure_reason = "velocity_step 缺少 target_velocity_ms、hold_time_sec 或 settle_before_step_sec。"; + return false; + } + try { + goal.goal.velocity_step.target_velocity_ms = std::stod(velocity_it->second); + goal.goal.velocity_step.hold_time_sec = std::stod(hold_it->second); + goal.goal.velocity_step.settle_before_step_sec = std::stod(settle_it->second); + } catch (...) { + failure_reason = "velocity_step 参数格式错误。"; + return false; + } + } else if (selected_task_type == ControllerEvaluationTaskType::ACCELERATION_DECELERATION) { + const auto start_it = metadata_map.find(metadata_keys::CONTROL_ACCEL_DECEL_START_VELOCITY_MS); + const auto target_it = metadata_map.find(metadata_keys::CONTROL_ACCEL_DECEL_TARGET_VELOCITY_MS); + const auto accel_it = metadata_map.find(metadata_keys::CONTROL_ACCEL_DECEL_TARGET_ACCEL_MS2); + const auto hold_it = metadata_map.find(metadata_keys::CONTROL_ACCEL_DECEL_HOLD_TIME_SEC); + if (start_it == metadata_map.end() || target_it == metadata_map.end() || + accel_it == metadata_map.end() || hold_it == metadata_map.end()) + { + failure_reason = "accel_decel 缺少 start_velocity_ms、target_velocity_ms、target_accel_ms2 或 hold_time_sec。"; + return false; + } + try { + goal.goal.accel_decel.start_velocity_ms = std::stod(start_it->second); + goal.goal.accel_decel.target_velocity_ms = std::stod(target_it->second); + goal.goal.accel_decel.target_accel_ms2 = std::stod(accel_it->second); + goal.goal.accel_decel.hold_time_sec = std::stod(hold_it->second); + } catch (...) { + failure_reason = "accel_decel 参数格式错误。"; + return false; + } + } else if (selected_task_type == ControllerEvaluationTaskType::STOP_ACCURACY) { + const auto x_it = metadata_map.find(metadata_keys::CONTROL_STOP_ACCURACY_X_M); + const auto y_it = metadata_map.find(metadata_keys::CONTROL_STOP_ACCURACY_Y_M); + const auto yaw_it = metadata_map.find(metadata_keys::CONTROL_STOP_ACCURACY_YAW_RAD); + const auto timeout_it = metadata_map.find(metadata_keys::CONTROL_TIMEOUT_SEC); + if (x_it == metadata_map.end() || y_it == metadata_map.end() || + yaw_it == metadata_map.end() || timeout_it == metadata_map.end()) + { + failure_reason = "stop_accuracy 缺少 target_stop_x_m、target_stop_y_m、target_stop_yaw_rad 或 control.timeout_sec。"; + return false; + } + try { + goal.goal.stop_accuracy.target_stop_x_m = std::stod(x_it->second); + goal.goal.stop_accuracy.target_stop_y_m = std::stod(y_it->second); + goal.goal.stop_accuracy.target_stop_yaw_rad = std::stod(yaw_it->second); + goal.goal.stop_accuracy.timeout_sec = std::stod(timeout_it->second); + } catch (...) { + failure_reason = "stop_accuracy 参数格式错误。"; + return false; } - return false; } goal.goal.source_iteration_id = session.session_id; diff --git a/agv_calib_brain/src/agv_calib_core/workshop_orchestrator/src/external_localization_client.cpp b/agv_calib_brain/src/core/agv_calib_core/workshop_orchestrator/src/external_localization_client.cpp similarity index 100% rename from agv_calib_brain/src/agv_calib_core/workshop_orchestrator/src/external_localization_client.cpp rename to agv_calib_brain/src/core/agv_calib_core/workshop_orchestrator/src/external_localization_client.cpp diff --git a/agv_calib_brain/src/agv_calib_core/workshop_orchestrator/src/main.cpp b/agv_calib_brain/src/core/agv_calib_core/workshop_orchestrator/src/main.cpp similarity index 100% rename from agv_calib_brain/src/agv_calib_core/workshop_orchestrator/src/main.cpp rename to agv_calib_brain/src/core/agv_calib_core/workshop_orchestrator/src/main.cpp diff --git a/agv_calib_brain/src/agv_calib_core/workshop_orchestrator/src/plan_builder.cpp b/agv_calib_brain/src/core/agv_calib_core/workshop_orchestrator/src/plan_builder.cpp similarity index 54% rename from agv_calib_brain/src/agv_calib_core/workshop_orchestrator/src/plan_builder.cpp rename to agv_calib_brain/src/core/agv_calib_core/workshop_orchestrator/src/plan_builder.cpp index f2c4c6e..5e6c019 100644 --- a/agv_calib_brain/src/agv_calib_core/workshop_orchestrator/src/plan_builder.cpp +++ b/agv_calib_brain/src/core/agv_calib_core/workshop_orchestrator/src/plan_builder.cpp @@ -4,8 +4,198 @@ namespace workshop_orchestrator_v2 { using calibration_vehicle_profile_interfaces::msg::CalibrationAbilityType; +using calibration_vehicle_profile_interfaces::msg::CameraMountType; +using calibration_vehicle_profile_interfaces::msg::SensorProfile; +using calibration_vehicle_profile_interfaces::msg::SensorType; using calibration_workshop_orchestration_interfaces::msg::StageExecutionPolicy; +namespace +{ + +void add_metadata_if_missing(StagePlan & stage, const std::string & key, const std::string & value) +{ + for (const auto & kv : stage.metadata) { + if (kv.key == key) { + return; + } + } + calibration_common_interfaces::msg::KeyValuePair kv; + kv.key = key; + kv.value = value; + stage.metadata.push_back(kv); +} + +const SensorProfile * find_default_sensor( + const VehicleProfile & profile, + bool require_intrinsic, + bool require_extrinsic) +{ + for (const auto & sensor : profile.sensors) { + if (!sensor.enabled || !sensor.selected_for_this_session) { + continue; + } + if (require_intrinsic && + (!sensor.needs_intrinsic_calibration || !sensor.supports_intrinsic_calibration)) + { + continue; + } + if (require_extrinsic && + (!sensor.needs_extrinsic_calibration || !sensor.supports_extrinsic_calibration)) + { + continue; + } + return &sensor; + } + + for (const auto & sensor : profile.sensors) { + if (!sensor.enabled) { + continue; + } + if (require_intrinsic && !sensor.supports_intrinsic_calibration) { + continue; + } + if (require_extrinsic && !sensor.supports_extrinsic_calibration) { + continue; + } + return &sensor; + } + + return nullptr; +} + +std::string resolve_intrinsic_subtype(const SensorProfile & sensor) +{ + if (sensor.sensor_type.value == SensorType::IMU) { + return "imu_intrinsic"; + } + if (sensor.camera_mount_type.value == CameraMountType::DOWNWARD_MOUNTED || + sensor.sensor_type.value == SensorType::DOWNWARD_CAMERA) + { + return "downward_camera_intrinsic"; + } + return "front_camera_intrinsic"; +} + +std::string resolve_extrinsic_subtype(const SensorProfile & sensor) +{ + if (sensor.sensor_type.value == SensorType::IMU) { + return "imu_extrinsic"; + } + if (sensor.sensor_type.value == SensorType::LIDAR_2D) { + return "lidar_2d_extrinsic"; + } + if (sensor.sensor_type.value == SensorType::LIDAR_3D) { + return "lidar_3d_extrinsic"; + } + if (sensor.camera_mount_type.value == CameraMountType::DOWNWARD_MOUNTED || + sensor.sensor_type.value == SensorType::DOWNWARD_CAMERA) + { + return "downward_camera_extrinsic"; + } + return "front_camera_extrinsic"; +} + +std::string resolve_hand_eye_subtype(const SensorProfile & sensor) +{ + if (sensor.camera_mount_type.value == CameraMountType::EYE_TO_HAND) { + return "eye_to_hand"; + } + return "eye_in_hand"; +} + +void apply_default_external_metadata(StagePlan & stage) +{ + add_metadata_if_missing(stage, metadata_keys::EXTERNAL_STATIC_SAMPLE_COUNT, "10"); + add_metadata_if_missing(stage, metadata_keys::EXTERNAL_DYNAMIC_SAMPLE_COUNT, "20"); + add_metadata_if_missing(stage, metadata_keys::EXTERNAL_REQUIRE_SHORT_MOTION_SEGMENT, "true"); + add_metadata_if_missing(stage, metadata_keys::EXTERNAL_MAX_POSITION_STDDEV_M, "0.05"); + add_metadata_if_missing(stage, metadata_keys::EXTERNAL_MAX_YAW_STDDEV_RAD, "0.05"); + add_metadata_if_missing(stage, metadata_keys::EXTERNAL_MAX_TRACKING_LOSS_RATIO, "0.05"); + add_metadata_if_missing(stage, metadata_keys::EXTERNAL_MAX_TIME_SYNC_OFFSET_MS, "50.0"); + add_metadata_if_missing(stage, metadata_keys::EXTERNAL_TIMEOUT_SEC, "30.0"); +} + +void apply_default_chassis_metadata(StagePlan & stage) +{ + add_metadata_if_missing(stage, metadata_keys::CHASSIS_PRIMITIVE_TYPE, "straight_line"); + add_metadata_if_missing(stage, metadata_keys::CHASSIS_STRAIGHT_LINE_DISTANCE_M, "2.0"); + add_metadata_if_missing(stage, metadata_keys::CHASSIS_STRAIGHT_LINE_SPEED_MS, "0.5"); + add_metadata_if_missing(stage, metadata_keys::COMMON_BRAKE_WHEN_FINISHED, "true"); + add_metadata_if_missing(stage, metadata_keys::COMMON_TIMEOUT_SEC, "60.0"); +} + +void apply_default_control_metadata(StagePlan & stage) +{ + add_metadata_if_missing(stage, metadata_keys::CONTROL_TASK_TYPE, "trajectory_tracking"); + add_metadata_if_missing(stage, metadata_keys::CONTROL_STOP_AT_END, "true"); + add_metadata_if_missing(stage, metadata_keys::CONTROL_TIMEOUT_SEC, "60.0"); + add_metadata_if_missing(stage, std::string(metadata_keys::CONTROL_TRAJECTORY_PREFIX) + "0_x_m", "0.0"); + add_metadata_if_missing(stage, std::string(metadata_keys::CONTROL_TRAJECTORY_PREFIX) + "0_y_m", "0.0"); + add_metadata_if_missing(stage, std::string(metadata_keys::CONTROL_TRAJECTORY_PREFIX) + "0_yaw_rad", "0.0"); + add_metadata_if_missing(stage, std::string(metadata_keys::CONTROL_TRAJECTORY_PREFIX) + "0_speed_ms", "0.3"); + add_metadata_if_missing(stage, std::string(metadata_keys::CONTROL_TRAJECTORY_PREFIX) + "1_x_m", "2.0"); + add_metadata_if_missing(stage, std::string(metadata_keys::CONTROL_TRAJECTORY_PREFIX) + "1_y_m", "0.0"); + add_metadata_if_missing(stage, std::string(metadata_keys::CONTROL_TRAJECTORY_PREFIX) + "1_yaw_rad", "0.0"); + add_metadata_if_missing(stage, std::string(metadata_keys::CONTROL_TRAJECTORY_PREFIX) + "1_speed_ms", "0.3"); +} + +void apply_default_sensor_intrinsic_metadata(StagePlan & stage, const VehicleProfile & profile) +{ + const auto * sensor = find_default_sensor(profile, true, false); + if (!sensor) { + return; + } + + add_metadata_if_missing(stage, metadata_keys::SENSOR_ID, sensor->sensor_id); + const auto subtype = resolve_intrinsic_subtype(*sensor); + add_metadata_if_missing(stage, metadata_keys::SENSOR_TASK_SUBTYPE, subtype); + + if (subtype == "imu_intrinsic") { + add_metadata_if_missing(stage, metadata_keys::IMU_INTRINSIC_REQUIRED_STATIC_SEGMENT_COUNT, "5"); + add_metadata_if_missing(stage, metadata_keys::IMU_INTRINSIC_REQUIRED_MOTION_SEGMENT_COUNT, "5"); + add_metadata_if_missing(stage, metadata_keys::IMU_INTRINSIC_TIMEOUT_SEC, "120.0"); + return; + } + + add_metadata_if_missing(stage, metadata_keys::CAMERA_INTRINSIC_REQUIRED_IMAGE_COUNT, "20"); + add_metadata_if_missing(stage, metadata_keys::CAMERA_INTRINSIC_TIMEOUT_SEC, "120.0"); +} + +void apply_default_sensor_extrinsic_metadata(StagePlan & stage, const VehicleProfile & profile) +{ + const auto * sensor = find_default_sensor(profile, false, true); + if (!sensor) { + return; + } + + add_metadata_if_missing(stage, metadata_keys::SENSOR_ID, sensor->sensor_id); + add_metadata_if_missing(stage, metadata_keys::SENSOR_TASK_SUBTYPE, resolve_extrinsic_subtype(*sensor)); + add_metadata_if_missing(stage, metadata_keys::SENSOR_EXTRINSIC_BASE_FRAME_ID, + profile.base_link_frame.empty() ? "base_link" : profile.base_link_frame); + add_metadata_if_missing(stage, metadata_keys::SENSOR_EXTRINSIC_REQUIRED_SAMPLE_COUNT, "10"); + add_metadata_if_missing(stage, metadata_keys::SENSOR_EXTRINSIC_TIMEOUT_SEC, "180.0"); +} + +void apply_default_hand_eye_metadata(StagePlan & stage, const VehicleProfile & profile) +{ + if (!profile.arm_profile.has_mechanical_arm || profile.arm_profile.arm_id.empty()) { + return; + } + + const auto * sensor = find_default_sensor(profile, false, true); + if (!sensor) { + return; + } + + add_metadata_if_missing(stage, metadata_keys::SENSOR_ID, sensor->sensor_id); + add_metadata_if_missing(stage, metadata_keys::SENSOR_TASK_SUBTYPE, resolve_hand_eye_subtype(*sensor)); + add_metadata_if_missing(stage, metadata_keys::HAND_EYE_ARM_ID, profile.arm_profile.arm_id); + add_metadata_if_missing(stage, metadata_keys::HAND_EYE_REQUIRED_POSE_COUNT, "12"); + add_metadata_if_missing(stage, metadata_keys::HAND_EYE_TIMEOUT_SEC, "180.0"); +} + +} // namespace + bool PlanBuilder::has_capability(const VehicleProfile & profile, uint8_t ability_type) const { for (const auto & cap : profile.capabilities) { @@ -123,25 +313,28 @@ std::string PlanBuilder::make_task_suffix(const std::string & task_code) const return suffix; } -StagePlan PlanBuilder::make_stage_for_task(int order_index, const RequestedCalibrationTask & task) const +StagePlan PlanBuilder::make_stage_for_task( + int order_index, + const VehicleProfile & profile, + const RequestedCalibrationTask & task) const { if (task.stage_type.value == WorkflowStageType::CHASSIS_CALIBRATION_STAGE) { - return make_chassis_stage(order_index, &task); + return make_chassis_stage(order_index, profile, &task); } if (task.stage_type.value == WorkflowStageType::CONTROL_CALIBRATION_STAGE) { - return make_control_stage(order_index, &task); + return make_control_stage(order_index, profile, &task); } if (task.stage_type.value == WorkflowStageType::SENSOR_INTRINSIC_CALIBRATION_STAGE) { - return make_sensor_intrinsic_stage(order_index, &task); + return make_sensor_intrinsic_stage(order_index, profile, &task); } if (task.stage_type.value == WorkflowStageType::SENSOR_EXTRINSIC_CALIBRATION_STAGE) { - return make_sensor_extrinsic_stage(order_index, &task); + return make_sensor_extrinsic_stage(order_index, profile, &task); } if (task.stage_type.value == WorkflowStageType::HAND_EYE_CALIBRATION_STAGE) { - return make_hand_eye_stage(order_index, &task); + return make_hand_eye_stage(order_index, profile, &task); } if (task.stage_type.value == WorkflowStageType::EXTERNAL_REFERENCE_READY_CHECK_STAGE) { - return make_external_reference_stage(order_index, &task); + return make_external_reference_stage(order_index, profile, &task); } StagePlan stage; @@ -196,18 +389,19 @@ std::vector PlanBuilder::build_minimal_plan( continue; } } - stages.push_back(make_stage_for_task(order++, task)); + stages.push_back(make_stage_for_task(order++, profile, task)); } } return stages; } - // 外部真值校核:始终加入(除非 requested_tasks 中显式 enabled=false)。 + // 外部真值校核:默认依赖 external localization 能力;若 requested_tasks 显式要求则仍可纳入。 if (should_include_stage(profile, config, - WorkflowStageType::EXTERNAL_REFERENCE_READY_CHECK_STAGE, 0)) { + WorkflowStageType::EXTERNAL_REFERENCE_READY_CHECK_STAGE, + CalibrationAbilityType::EXTERNAL_LOCALIZATION_CALIBRATION)) { const auto * task_cfg = find_requested_task(config, WorkflowStageType::EXTERNAL_REFERENCE_READY_CHECK_STAGE); - stages.push_back(make_external_reference_stage(order++, task_cfg)); + stages.push_back(make_external_reference_stage(order++, profile, task_cfg)); } // 底盘标定。 @@ -216,7 +410,7 @@ std::vector PlanBuilder::build_minimal_plan( CalibrationAbilityType::CHASSIS_CALIBRATION)) { const auto * task_cfg = find_requested_task(config, WorkflowStageType::CHASSIS_CALIBRATION_STAGE); - stages.push_back(make_chassis_stage(order++, task_cfg)); + stages.push_back(make_chassis_stage(order++, profile, task_cfg)); } // 运控参数标定。 @@ -225,7 +419,7 @@ std::vector PlanBuilder::build_minimal_plan( CalibrationAbilityType::CONTROL_CALIBRATION)) { const auto * task_cfg = find_requested_task(config, WorkflowStageType::CONTROL_CALIBRATION_STAGE); - stages.push_back(make_control_stage(order++, task_cfg)); + stages.push_back(make_control_stage(order++, profile, task_cfg)); } // 传感器内参标定。 @@ -234,7 +428,7 @@ std::vector PlanBuilder::build_minimal_plan( CalibrationAbilityType::SENSOR_CALIBRATION)) { const auto * task_cfg = find_requested_task(config, WorkflowStageType::SENSOR_INTRINSIC_CALIBRATION_STAGE); - stages.push_back(make_sensor_intrinsic_stage(order++, task_cfg)); + stages.push_back(make_sensor_intrinsic_stage(order++, profile, task_cfg)); } // 传感器外参标定。 @@ -243,7 +437,7 @@ std::vector PlanBuilder::build_minimal_plan( CalibrationAbilityType::SENSOR_CALIBRATION)) { const auto * task_cfg = find_requested_task(config, WorkflowStageType::SENSOR_EXTRINSIC_CALIBRATION_STAGE); - stages.push_back(make_sensor_extrinsic_stage(order++, task_cfg)); + stages.push_back(make_sensor_extrinsic_stage(order++, profile, task_cfg)); } // 手眼标定。 @@ -252,13 +446,16 @@ std::vector PlanBuilder::build_minimal_plan( CalibrationAbilityType::HAND_EYE_CALIBRATION)) { const auto * task_cfg = find_requested_task(config, WorkflowStageType::HAND_EYE_CALIBRATION_STAGE); - stages.push_back(make_hand_eye_stage(order++, task_cfg)); + stages.push_back(make_hand_eye_stage(order++, profile, task_cfg)); } return stages; } -StagePlan PlanBuilder::make_external_reference_stage(int order_index, const RequestedCalibrationTask * task_cfg) const +StagePlan PlanBuilder::make_external_reference_stage( + int order_index, + const VehicleProfile & profile, + const RequestedCalibrationTask * task_cfg) const { StagePlan stage; stage.stage_id = "stage_external_reference"; @@ -270,11 +467,19 @@ StagePlan PlanBuilder::make_external_reference_stage(int order_index, const Requ stage.description = "验证并接入车间外部定位真值参考源。需要 session.config 提供 localization_source_id/workcell_zone_id;stage.metadata 需要提供 external.static_sample_count、external.dynamic_sample_count、external.max_position_stddev_m、external.max_yaw_stddev_rad、external.max_tracking_loss_ratio、external.max_time_sync_offset_ms、external.timeout_sec。"; stage.retry_limit = 0; stage.auto_generated = true; + apply_default_external_metadata(stage); apply_task_config(stage, task_cfg); + if (task_cfg) { + stage.stage_id += make_task_suffix(task_cfg->task_code); + apply_task_identity(stage, *task_cfg); + } return stage; } -StagePlan PlanBuilder::make_chassis_stage(int order_index, const RequestedCalibrationTask * task_cfg) const +StagePlan PlanBuilder::make_chassis_stage( + int order_index, + const VehicleProfile & profile, + const RequestedCalibrationTask * task_cfg) const { StagePlan stage; stage.stage_id = "stage_chassis_calibration"; @@ -283,9 +488,10 @@ StagePlan PlanBuilder::make_chassis_stage(int order_index, const RequestedCalibr stage.display_name = "底盘标定"; stage.order_index = order_index; stage.executor_endpoint_name = "/chassis/execute_motion_primitive"; - stage.description = "调用独立底盘标定包执行动作原语任务。stage.metadata 需要提供 primitive_type;当前最小流支持 straight_line,并要求 straight_line.target_distance_m、straight_line.target_speed_ms;可选 straight_line.reverse、brake_when_finished、timeout_sec。"; + stage.description = "调用独立底盘标定包执行动作原语任务。stage.metadata 需要提供 primitive_type;支持 straight_line、arc、in_place_rotation、steering_sweep、lateral_translation、diagonal_motion、module_alignment、coordinated_steering。各动作分别消费对应 metadata 参数,并通用支持 brake_when_finished、timeout_sec。"; stage.retry_limit = 1; stage.auto_generated = true; + apply_default_chassis_metadata(stage); apply_task_config(stage, task_cfg); if (task_cfg) { stage.stage_id += make_task_suffix(task_cfg->task_code); @@ -294,7 +500,10 @@ StagePlan PlanBuilder::make_chassis_stage(int order_index, const RequestedCalibr return stage; } -StagePlan PlanBuilder::make_control_stage(int order_index, const RequestedCalibrationTask * task_cfg) const +StagePlan PlanBuilder::make_control_stage( + int order_index, + const VehicleProfile & profile, + const RequestedCalibrationTask * task_cfg) const { StagePlan stage; stage.stage_id = "stage_control_calibration"; @@ -303,9 +512,10 @@ StagePlan PlanBuilder::make_control_stage(int order_index, const RequestedCalibr stage.display_name = "运控参数调优"; stage.order_index = order_index; stage.executor_endpoint_name = "/control/execute_controller_evaluation"; - stage.description = "调用独立运控标定包执行控制评估任务。stage.metadata 需要提供 control.task_type;当前最小流支持 trajectory_tracking,并要求至少一组 traj_pt_{i}_{x_m|y_m|yaw_rad|speed_ms} 轨迹点。"; + stage.description = "调用独立运控标定包执行控制评估任务。stage.metadata 需要提供 control.task_type;支持 trajectory_tracking、velocity_step、accel_decel、stop_accuracy。trajectory_tracking 需提供 traj_pt_{i}_{x_m|y_m|yaw_rad|speed_ms},其余任务需提供各自目标速度、加速度、停车目标与 timeout 等参数。"; stage.retry_limit = 0; stage.auto_generated = true; + apply_default_control_metadata(stage); apply_task_config(stage, task_cfg); if (task_cfg) { stage.stage_id += make_task_suffix(task_cfg->task_code); @@ -314,7 +524,10 @@ StagePlan PlanBuilder::make_control_stage(int order_index, const RequestedCalibr return stage; } -StagePlan PlanBuilder::make_sensor_intrinsic_stage(int order_index, const RequestedCalibrationTask * task_cfg) const +StagePlan PlanBuilder::make_sensor_intrinsic_stage( + int order_index, + const VehicleProfile & profile, + const RequestedCalibrationTask * task_cfg) const { StagePlan stage; stage.stage_id = "stage_sensor_intrinsic_calibration"; @@ -323,9 +536,10 @@ StagePlan PlanBuilder::make_sensor_intrinsic_stage(int order_index, const Reques stage.display_name = "传感器内参标定"; stage.order_index = order_index; stage.executor_endpoint_name = "/sensor_calibration/execute_task"; - stage.description = "启动独立传感器标定包执行内参标定任务。stage.metadata 需要提供 sensor.sensor_id、camera_intrinsic.required_image_count;可选 camera_intrinsic.target_board_id、camera_intrinsic.timeout_sec。"; + stage.description = "启动独立传感器标定包执行内参标定任务。stage.metadata 需要提供 sensor.sensor_id、sensor.task_subtype;相机内参任务需提供 camera_intrinsic.required_image_count,可选 target_board_id/timeout_sec;IMU 内参任务需提供 imu_intrinsic.required_static_segment_count、imu_intrinsic.required_motion_segment_count,可选 imu_intrinsic.timeout_sec。"; stage.retry_limit = 0; stage.auto_generated = true; + apply_default_sensor_intrinsic_metadata(stage, profile); apply_task_config(stage, task_cfg); if (task_cfg) { stage.stage_id += make_task_suffix(task_cfg->task_code); @@ -334,7 +548,10 @@ StagePlan PlanBuilder::make_sensor_intrinsic_stage(int order_index, const Reques return stage; } -StagePlan PlanBuilder::make_sensor_extrinsic_stage(int order_index, const RequestedCalibrationTask * task_cfg) const +StagePlan PlanBuilder::make_sensor_extrinsic_stage( + int order_index, + const VehicleProfile & profile, + const RequestedCalibrationTask * task_cfg) const { StagePlan stage; stage.stage_id = "stage_sensor_extrinsic_calibration"; @@ -346,6 +563,7 @@ StagePlan PlanBuilder::make_sensor_extrinsic_stage(int order_index, const Reques stage.description = "启动独立传感器标定包执行外参标定任务。stage.metadata 需要提供 sensor.sensor_id、sensor_extrinsic.base_frame_id、sensor_extrinsic.required_sample_count;可选 sensor_extrinsic.timeout_sec。"; stage.retry_limit = 0; stage.auto_generated = true; + apply_default_sensor_extrinsic_metadata(stage, profile); apply_task_config(stage, task_cfg); if (task_cfg) { stage.stage_id += make_task_suffix(task_cfg->task_code); @@ -354,7 +572,10 @@ StagePlan PlanBuilder::make_sensor_extrinsic_stage(int order_index, const Reques return stage; } -StagePlan PlanBuilder::make_hand_eye_stage(int order_index, const RequestedCalibrationTask * task_cfg) const +StagePlan PlanBuilder::make_hand_eye_stage( + int order_index, + const VehicleProfile & profile, + const RequestedCalibrationTask * task_cfg) const { StagePlan stage; stage.stage_id = "stage_hand_eye_calibration"; @@ -366,6 +587,7 @@ StagePlan PlanBuilder::make_hand_eye_stage(int order_index, const RequestedCalib stage.description = "启动独立传感器标定包执行手眼标定任务。stage.metadata 需要提供 sensor.sensor_id、hand_eye.arm_id、hand_eye.required_pose_count;可选 hand_eye.timeout_sec。"; stage.retry_limit = 0; stage.auto_generated = true; + apply_default_hand_eye_metadata(stage, profile); apply_task_config(stage, task_cfg); if (task_cfg) { stage.stage_id += make_task_suffix(task_cfg->task_code); diff --git a/agv_calib_brain/src/core/agv_calib_core/workshop_orchestrator/src/precheck_runner.cpp b/agv_calib_brain/src/core/agv_calib_core/workshop_orchestrator/src/precheck_runner.cpp new file mode 100644 index 0000000..227626e --- /dev/null +++ b/agv_calib_brain/src/core/agv_calib_core/workshop_orchestrator/src/precheck_runner.cpp @@ -0,0 +1,263 @@ +#include "workshop_orchestrator_v2/precheck_runner.hpp" + +#include "calibration_workshop_orchestration_interfaces/msg/precheck_item.hpp" + +namespace workshop_orchestrator_v2 +{ + +bool PrecheckRunner::has_metadata_key(const StagePlan & stage, const std::string & key) const +{ + for (const auto & kv : stage.metadata) { + if (kv.key == key) { + return true; + } + } + return false; +} + +bool PrecheckRunner::has_metadata_prefix(const StagePlan & stage, const std::string & prefix) const +{ + for (const auto & kv : stage.metadata) { + if (kv.key.rfind(prefix, 0) == 0) { + return true; + } + } + return false; +} + +WorkshopPrecheckResponse PrecheckRunner::run(const WorkshopSession & session) const +{ + WorkshopPrecheckResponse response; + response.success = true; + response.error_code = make_error_code(ErrorCode::OK); + response.checked_timestamp_us = now_us(); + + auto append_item = [&](const std::string & code, + const std::string & name, + bool passed, + const std::string & message, + uint8_t stage_type, + uint8_t module_type) { + calibration_workshop_orchestration_interfaces::msg::PrecheckItem item; + item.item_code = code; + item.display_name = name; + item.passed = passed; + item.blocking = true; + item.error_code = make_error_code(passed ? ErrorCode::OK : ErrorCode::INVALID_STATE); + item.message = message; + item.related_stage_type = make_stage_type(stage_type); + item.related_module_type = make_module_type(module_type); + response.items.push_back(item); + if (!passed) { + response.blocking_issue_count += 1; + response.all_passed = false; + } + }; + + response.all_passed = true; + response.blocking_issue_count = 0; + + append_item( + "plan_not_empty", + "Execution plan exists", + !session.stage_plan.empty(), + session.stage_plan.empty() ? "Session has no executable stages." : "Session has executable stages.", + WorkflowStageType::WORKFLOW_STAGE_UNSPECIFIED, + CalibrationModuleType::CALIBRATION_MODULE_UNSPECIFIED); + + for (const auto & stage : session.stage_plan) { + if (stage.module_type.value == CalibrationModuleType::CHASSIS_MODULE) { + const bool has_primitive_type = has_metadata_key(stage, metadata_keys::CHASSIS_PRIMITIVE_TYPE); + bool passed = false; + std::string message = "底盘阶段缺少 primitive_type。"; + + if (has_primitive_type) { + std::string primitive_type; + for (const auto & kv : stage.metadata) { + if (kv.key == metadata_keys::CHASSIS_PRIMITIVE_TYPE) { + primitive_type = kv.value; + break; + } + } + + if (primitive_type == "straight_line") { + passed = + has_metadata_key(stage, metadata_keys::CHASSIS_STRAIGHT_LINE_DISTANCE_M) && + has_metadata_key(stage, metadata_keys::CHASSIS_STRAIGHT_LINE_SPEED_MS); + message = passed ? "底盘直行动作输入完整。" : "straight_line 缺少距离或速度参数。"; + } else if (primitive_type == "arc") { + passed = + has_metadata_key(stage, metadata_keys::CHASSIS_ARC_SPEED_MS) && + has_metadata_key(stage, metadata_keys::CHASSIS_ARC_RADIUS_M) && + has_metadata_key(stage, metadata_keys::CHASSIS_ARC_SWEEP_ANGLE_DEG); + message = passed ? "底盘圆弧动作输入完整。" : "arc 缺少速度、半径或扫角参数。"; + } else if (primitive_type == "in_place_rotation") { + passed = + has_metadata_key(stage, metadata_keys::CHASSIS_ROTATION_TARGET_YAW_DEG) && + has_metadata_key(stage, metadata_keys::CHASSIS_ROTATION_TARGET_ANGULAR_VEL_DEG_S); + message = passed ? "原地旋转动作输入完整。" : "in_place_rotation 缺少角度或角速度参数。"; + } else if (primitive_type == "steering_sweep") { + passed = + has_metadata_key(stage, metadata_keys::CHASSIS_STEERING_SWEEP_TARGET_ANGLE_DEG) && + has_metadata_key(stage, metadata_keys::CHASSIS_STEERING_SWEEP_AMPLITUDE_DEG) && + has_metadata_key(stage, metadata_keys::CHASSIS_STEERING_SWEEP_FREQUENCY_HZ) && + has_metadata_key(stage, metadata_keys::CHASSIS_STEERING_SWEEP_DURATION_SEC); + message = passed ? "舵角扫动动作输入完整。" : "steering_sweep 缺少目标角、幅值、频率或持续时间。"; + } else if (primitive_type == "lateral_translation") { + passed = + has_metadata_key(stage, metadata_keys::CHASSIS_LATERAL_SPEED_MS) && + has_metadata_key(stage, metadata_keys::CHASSIS_LATERAL_DISTANCE_M); + message = passed ? "横移动作输入完整。" : "lateral_translation 缺少速度或距离参数。"; + } else if (primitive_type == "diagonal_motion") { + passed = + has_metadata_key(stage, metadata_keys::CHASSIS_DIAGONAL_SPEED_MS) && + has_metadata_key(stage, metadata_keys::CHASSIS_DIAGONAL_DISTANCE_M) && + has_metadata_key(stage, metadata_keys::CHASSIS_DIAGONAL_HEADING_DEG); + message = passed ? "斜移动作输入完整。" : "diagonal_motion 缺少速度、距离或方向角参数。"; + } else if (primitive_type == "module_alignment") { + passed = + has_metadata_key(stage, metadata_keys::CHASSIS_MODULE_ALIGNMENT_IDS) && + has_metadata_key(stage, metadata_keys::CHASSIS_MODULE_ALIGNMENT_ZERO_DEG) && + has_metadata_key(stage, metadata_keys::CHASSIS_MODULE_ALIGNMENT_TOLERANCE_DEG); + message = passed ? "模块零位检查输入完整。" : "module_alignment 缺少模块列表、零位或容差参数。"; + } else if (primitive_type == "coordinated_steering") { + passed = + has_metadata_key(stage, metadata_keys::CHASSIS_COORDINATED_STEERING_IDS) && + has_metadata_key(stage, metadata_keys::CHASSIS_COORDINATED_STEERING_TARGET_ANGLE_DEG) && + has_metadata_key(stage, metadata_keys::CHASSIS_COORDINATED_STEERING_HOLD_TIME_SEC); + message = passed ? "协同转向动作输入完整。" : "coordinated_steering 缺少模块列表、目标角或保持时间参数。"; + } else { + message = "底盘阶段 primitive_type 不受支持。"; + } + } + + append_item( + stage.stage_id + ".metadata", + stage.display_name + " metadata", + passed, + message, + stage.stage_type.value, + stage.module_type.value); + } else if (stage.module_type.value == CalibrationModuleType::CONTROL_MODULE) { + bool has_task_type = has_metadata_key(stage, metadata_keys::CONTROL_TASK_TYPE); + bool passed = false; + std::string message = "运控阶段缺少 control.task_type。"; + std::string task_type; + if (has_task_type) { + for (const auto & kv : stage.metadata) { + if (kv.key == metadata_keys::CONTROL_TASK_TYPE) { + task_type = kv.value; + break; + } + } + + if (task_type == "trajectory_tracking") { + passed = has_metadata_prefix(stage, metadata_keys::CONTROL_TRAJECTORY_PREFIX); + message = passed ? "轨迹跟踪任务输入完整。" : "trajectory_tracking 缺少轨迹点输入。"; + } else if (task_type == "velocity_step") { + passed = + has_metadata_key(stage, metadata_keys::CONTROL_VELOCITY_STEP_TARGET_VELOCITY_MS) && + has_metadata_key(stage, metadata_keys::CONTROL_VELOCITY_STEP_HOLD_TIME_SEC) && + has_metadata_key(stage, metadata_keys::CONTROL_VELOCITY_STEP_SETTLE_BEFORE_STEP_SEC); + message = passed ? "速度阶跃任务输入完整。" : "velocity_step 缺少目标速度、保持时间或阶跃前静稳时间。"; + } else if (task_type == "accel_decel") { + passed = + has_metadata_key(stage, metadata_keys::CONTROL_ACCEL_DECEL_START_VELOCITY_MS) && + has_metadata_key(stage, metadata_keys::CONTROL_ACCEL_DECEL_TARGET_VELOCITY_MS) && + has_metadata_key(stage, metadata_keys::CONTROL_ACCEL_DECEL_TARGET_ACCEL_MS2) && + has_metadata_key(stage, metadata_keys::CONTROL_ACCEL_DECEL_HOLD_TIME_SEC); + message = passed ? "加减速任务输入完整。" : "accel_decel 缺少起始速度、目标速度、目标加速度或保持时间。"; + } else if (task_type == "stop_accuracy") { + passed = + has_metadata_key(stage, metadata_keys::CONTROL_STOP_ACCURACY_X_M) && + has_metadata_key(stage, metadata_keys::CONTROL_STOP_ACCURACY_Y_M) && + has_metadata_key(stage, metadata_keys::CONTROL_STOP_ACCURACY_YAW_RAD) && + has_metadata_key(stage, metadata_keys::CONTROL_TIMEOUT_SEC); + message = passed ? "停车精度任务输入完整。" : "stop_accuracy 缺少目标停车位姿或 control.timeout_sec。"; + } else { + message = "运控阶段 control.task_type 不受支持。"; + } + } + + append_item( + stage.stage_id + ".metadata", + stage.display_name + " metadata", + passed, + message, + stage.stage_type.value, + stage.module_type.value); + } else if ( + stage.module_type.value == CalibrationModuleType::SENSOR_INTRINSIC_MODULE || + stage.module_type.value == CalibrationModuleType::SENSOR_EXTRINSIC_MODULE || + stage.module_type.value == CalibrationModuleType::HAND_EYE_MODULE) { + bool passed = + has_metadata_key(stage, metadata_keys::SENSOR_ID) && + has_metadata_key(stage, metadata_keys::SENSOR_TASK_SUBTYPE); + std::string message = passed ? "传感器阶段基础输入完整。" : "传感器阶段缺少 sensor.sensor_id 或 sensor.task_subtype。"; + + if (passed && stage.module_type.value == CalibrationModuleType::SENSOR_INTRINSIC_MODULE) { + std::string subtype; + for (const auto & kv : stage.metadata) { + if (kv.key == metadata_keys::SENSOR_TASK_SUBTYPE) { + subtype = kv.value; + break; + } + } + + if (subtype == "imu_intrinsic") { + passed = + has_metadata_key(stage, metadata_keys::IMU_INTRINSIC_REQUIRED_STATIC_SEGMENT_COUNT) && + has_metadata_key(stage, metadata_keys::IMU_INTRINSIC_REQUIRED_MOTION_SEGMENT_COUNT); + message = passed ? "IMU 内参阶段输入完整。" : "IMU 内参阶段缺少静止段数或运动段数。"; + } else { + passed = has_metadata_key(stage, metadata_keys::CAMERA_INTRINSIC_REQUIRED_IMAGE_COUNT); + message = passed ? "相机内参阶段输入完整。" : "相机内参阶段缺少 required_image_count。"; + } + } + + if (passed && stage.module_type.value == CalibrationModuleType::SENSOR_EXTRINSIC_MODULE) { + passed = + has_metadata_key(stage, metadata_keys::SENSOR_EXTRINSIC_BASE_FRAME_ID) && + has_metadata_key(stage, metadata_keys::SENSOR_EXTRINSIC_REQUIRED_SAMPLE_COUNT); + message = passed ? "传感器外参阶段输入完整。" : "传感器外参阶段缺少 base_frame_id 或 required_sample_count。"; + } + + if (passed && stage.module_type.value == CalibrationModuleType::HAND_EYE_MODULE) { + passed = + has_metadata_key(stage, metadata_keys::HAND_EYE_ARM_ID) && + has_metadata_key(stage, metadata_keys::HAND_EYE_REQUIRED_POSE_COUNT); + message = passed ? "手眼阶段输入完整。" : "手眼阶段缺少 arm_id 或 required_pose_count。"; + } + + append_item( + stage.stage_id + ".metadata", + stage.display_name + " metadata", + passed, + message, + stage.stage_type.value, + stage.module_type.value); + } else if (stage.module_type.value == CalibrationModuleType::EXTERNAL_LOCALIZATION_MODULE) { + const bool passed = + has_metadata_key(stage, metadata_keys::EXTERNAL_STATIC_SAMPLE_COUNT) && + has_metadata_key(stage, metadata_keys::EXTERNAL_DYNAMIC_SAMPLE_COUNT) && + has_metadata_key(stage, metadata_keys::EXTERNAL_MAX_POSITION_STDDEV_M) && + has_metadata_key(stage, metadata_keys::EXTERNAL_MAX_YAW_STDDEV_RAD) && + has_metadata_key(stage, metadata_keys::EXTERNAL_MAX_TRACKING_LOSS_RATIO) && + has_metadata_key(stage, metadata_keys::EXTERNAL_MAX_TIME_SYNC_OFFSET_MS) && + has_metadata_key(stage, metadata_keys::EXTERNAL_TIMEOUT_SEC); + append_item( + stage.stage_id + ".metadata", + stage.display_name + " metadata", + passed, + passed ? "external 阶段输入完整。" : "external 阶段缺少真值校核阈值配置。", + stage.stage_type.value, + stage.module_type.value); + } + } + + response.ready_for_start = response.all_passed; + response.message = response.ready_for_start ? "Precheck passed." : "Precheck failed."; + return response; +} + +} // namespace workshop_orchestrator_v2 diff --git a/agv_calib_brain/src/agv_calib_core/workshop_orchestrator/src/report_builder.cpp b/agv_calib_brain/src/core/agv_calib_core/workshop_orchestrator/src/report_builder.cpp similarity index 81% rename from agv_calib_brain/src/agv_calib_core/workshop_orchestrator/src/report_builder.cpp rename to agv_calib_brain/src/core/agv_calib_core/workshop_orchestrator/src/report_builder.cpp index 021d469..a77e063 100644 --- a/agv_calib_brain/src/agv_calib_core/workshop_orchestrator/src/report_builder.cpp +++ b/agv_calib_brain/src/core/agv_calib_core/workshop_orchestrator/src/report_builder.cpp @@ -60,12 +60,30 @@ WorkshopReport ReportBuilder::build( kv.value = overall_success ? "true" : "false"; report.metadata.push_back(kv); + kv.key = "workshop_line_id"; + kv.value = session.workshop_line_id; + report.metadata.push_back(kv); + + kv.key = "vehicle_id"; + kv.value = session.vehicle_profile_snapshot.base_info.vehicle_id; + report.metadata.push_back(kv); + + kv.key = "operator_id"; + kv.value = session.operator_info.operator_id; + report.metadata.push_back(kv); + if (!report.active_parameter_bundle_version.empty()) { kv.key = "active_parameter_bundle_version"; kv.value = report.active_parameter_bundle_version; report.metadata.push_back(kv); } + if (!stage_results.empty()) { + kv.key = "last_stage_id"; + kv.value = stage_results.back().stage_id; + report.metadata.push_back(kv); + } + return report; } diff --git a/agv_calib_brain/src/agv_calib_core/workshop_orchestrator/src/sensor_gateway_client.cpp b/agv_calib_brain/src/core/agv_calib_core/workshop_orchestrator/src/sensor_gateway_client.cpp similarity index 86% rename from agv_calib_brain/src/agv_calib_core/workshop_orchestrator/src/sensor_gateway_client.cpp rename to agv_calib_brain/src/core/agv_calib_core/workshop_orchestrator/src/sensor_gateway_client.cpp index 284c210..5e2b13c 100644 --- a/agv_calib_brain/src/agv_calib_core/workshop_orchestrator/src/sensor_gateway_client.cpp +++ b/agv_calib_brain/src/core/agv_calib_core/workshop_orchestrator/src/sensor_gateway_client.cpp @@ -86,17 +86,32 @@ uint8_t SensorGatewayClient::resolve_task_type( const StagePlan & stage, std::string & failure_reason) const { - switch (stage.module_type.value) { - case CalibrationModuleType::SENSOR_INTRINSIC_MODULE: - return SensorCalibrationTaskType::CAMERA_INTRINSIC; - case CalibrationModuleType::SENSOR_EXTRINSIC_MODULE: - return SensorCalibrationTaskType::SENSOR_TO_BASE_EXTRINSIC; - case CalibrationModuleType::HAND_EYE_MODULE: - return SensorCalibrationTaskType::HAND_EYE; - default: - failure_reason = "当前传感器阶段未映射到具体标定任务类型。"; + if (stage.module_type.value == CalibrationModuleType::SENSOR_INTRINSIC_MODULE) { + std::unordered_map metadata_map; + for (const auto & kv : stage.metadata) { + metadata_map[kv.key] = kv.value; + } + + const auto subtype_it = metadata_map.find(metadata_keys::SENSOR_TASK_SUBTYPE); + if (subtype_it == metadata_map.end()) { + failure_reason = "传感器内参阶段缺少 sensor.task_subtype。"; return SensorCalibrationTaskType::SENSOR_CALIBRATION_TASK_TYPE_UNSPECIFIED; + } + + if (subtype_it->second == "imu_intrinsic") { + return SensorCalibrationTaskType::IMU_INTRINSIC; + } + return SensorCalibrationTaskType::CAMERA_INTRINSIC; } + if (stage.module_type.value == CalibrationModuleType::SENSOR_EXTRINSIC_MODULE) { + return SensorCalibrationTaskType::SENSOR_TO_BASE_EXTRINSIC; + } + if (stage.module_type.value == CalibrationModuleType::HAND_EYE_MODULE) { + return SensorCalibrationTaskType::HAND_EYE; + } + + failure_reason = "当前传感器阶段未映射到具体标定任务类型。"; + return SensorCalibrationTaskType::SENSOR_CALIBRATION_TASK_TYPE_UNSPECIFIED; } uint8_t resolve_task_subtype_from_metadata( @@ -151,8 +166,10 @@ bool task_and_subtype_match( { if (task_type == SensorCalibrationTaskType::CAMERA_INTRINSIC) { return task_subtype == SensorCalibrationTaskSubtype::FRONT_CAMERA_INTRINSIC || - task_subtype == SensorCalibrationTaskSubtype::DOWNWARD_CAMERA_INTRINSIC || - task_subtype == SensorCalibrationTaskSubtype::IMU_INTRINSIC; + task_subtype == SensorCalibrationTaskSubtype::DOWNWARD_CAMERA_INTRINSIC; + } + if (task_type == SensorCalibrationTaskType::IMU_INTRINSIC) { + return task_subtype == SensorCalibrationTaskSubtype::IMU_INTRINSIC; } if (task_type == SensorCalibrationTaskType::SENSOR_TO_BASE_EXTRINSIC) { return task_subtype == SensorCalibrationTaskSubtype::FRONT_CAMERA_EXTRINSIC || @@ -238,6 +255,33 @@ bool SensorGatewayClient::build_goal( return false; } } + goal.goal.camera_intrinsic.reference_target.reference_target_id = stage.stage_id; + } else if (selected_task_type == SensorCalibrationTaskType::IMU_INTRINSIC) { + goal.goal.imu_intrinsic.sensor_id = sensor_id_it->second; + const auto static_count_it = metadata_map.find(metadata_keys::IMU_INTRINSIC_REQUIRED_STATIC_SEGMENT_COUNT); + const auto motion_count_it = metadata_map.find(metadata_keys::IMU_INTRINSIC_REQUIRED_MOTION_SEGMENT_COUNT); + if (static_count_it == metadata_map.end() || motion_count_it == metadata_map.end()) { + failure_reason = "imu_intrinsic 缺少 required_static_segment_count 或 required_motion_segment_count。"; + return false; + } + try { + goal.goal.imu_intrinsic.required_static_segment_count = + static_cast(std::stoul(static_count_it->second)); + goal.goal.imu_intrinsic.required_motion_segment_count = + static_cast(std::stoul(motion_count_it->second)); + } catch (...) { + failure_reason = "imu_intrinsic 参数格式错误。"; + return false; + } + const auto timeout_it = metadata_map.find(metadata_keys::IMU_INTRINSIC_TIMEOUT_SEC); + if (timeout_it != metadata_map.end()) { + try { + goal.goal.imu_intrinsic.timeout_sec = std::stod(timeout_it->second); + } catch (...) { + failure_reason = "imu_intrinsic.timeout_sec 格式错误。"; + return false; + } + } } else if (selected_task_type == SensorCalibrationTaskType::SENSOR_TO_BASE_EXTRINSIC) { goal.goal.sensor_to_base_extrinsic.sensor_id = sensor_id_it->second; const auto base_frame_it = metadata_map.find(metadata_keys::SENSOR_EXTRINSIC_BASE_FRAME_ID); @@ -266,6 +310,7 @@ bool SensorGatewayClient::build_goal( return false; } } + goal.goal.sensor_to_base_extrinsic.reference_target.reference_target_id = stage.stage_id; if (task_subtype == SensorCalibrationTaskSubtype::FRONT_CAMERA_EXTRINSIC || task_subtype == SensorCalibrationTaskSubtype::DOWNWARD_CAMERA_EXTRINSIC) @@ -310,6 +355,7 @@ bool SensorGatewayClient::build_goal( return false; } } + goal.goal.hand_eye.reference_target.reference_target_id = stage.stage_id; if (task_subtype == SensorCalibrationTaskSubtype::EYE_IN_HAND) { goal.goal.hand_eye.mode.value = HandEyeCalibrationMode::EYE_IN_HAND; diff --git a/agv_calib_brain/src/core/agv_calib_core/workshop_orchestrator/src/wifi6_link_client.cpp b/agv_calib_brain/src/core/agv_calib_core/workshop_orchestrator/src/wifi6_link_client.cpp new file mode 100644 index 0000000..849e6e2 --- /dev/null +++ b/agv_calib_brain/src/core/agv_calib_core/workshop_orchestrator/src/wifi6_link_client.cpp @@ -0,0 +1,283 @@ +#include "workshop_orchestrator_v2/wifi6_link_client.hpp" + +#include +#include +#include +#include + +#include +#include +#include + +#include "nlohmann/json.hpp" +#include "workshop_orchestrator_v2/types.hpp" + +namespace workshop_orchestrator_v2 +{ + +namespace +{ + +void write_le32(uint8_t * buf, uint32_t value) +{ + buf[0] = static_cast(value & 0xFF); + buf[1] = static_cast((value >> 8) & 0xFF); + buf[2] = static_cast((value >> 16) & 0xFF); + buf[3] = static_cast((value >> 24) & 0xFF); +} + +uint32_t read_le32(const uint8_t * buf) +{ + return static_cast(buf[0]) | + (static_cast(buf[1]) << 8) | + (static_cast(buf[2]) << 16) | + (static_cast(buf[3]) << 24); +} + +bool write_all(int fd, const uint8_t * buf, size_t len) +{ + size_t written = 0; + while (written < len) { + const ssize_t n = ::write(fd, buf + written, len - written); + if (n <= 0) { + return false; + } + written += static_cast(n); + } + return true; +} + +bool read_all(int fd, uint8_t * buf, size_t len) +{ + size_t got = 0; + while (got < len) { + const ssize_t n = ::read(fd, buf + got, len - got); + if (n <= 0) { + return false; + } + got += static_cast(n); + } + return true; +} + +int connect_tcp(const std::string & host, uint16_t port, int timeout_ms) +{ + const int fd = ::socket(AF_INET, SOCK_STREAM, 0); + if (fd < 0) { + return -1; + } + + struct timeval tv; + tv.tv_sec = timeout_ms / 1000; + tv.tv_usec = (timeout_ms % 1000) * 1000; + ::setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv)); + ::setsockopt(fd, SOL_SOCKET, SO_SNDTIMEO, &tv, sizeof(tv)); + + struct sockaddr_in addr; + std::memset(&addr, 0, sizeof(addr)); + addr.sin_family = AF_INET; + addr.sin_port = htons(port); + if (::inet_pton(AF_INET, host.c_str(), &addr.sin_addr) <= 0) { + ::close(fd); + return -1; + } + if (::connect(fd, reinterpret_cast(&addr), sizeof(addr)) < 0) { + ::close(fd); + return -1; + } + return fd; +} + +std::string endpoint_string(const std::string & host, uint16_t port) +{ + std::ostringstream oss; + oss << host << ":" << port; + return oss.str(); +} + +std::string readiness_payload(const std::string & agent_name) +{ + nlohmann::json request; + request["agent_name"] = agent_name; + request["include_details"] = true; + request["target_resource_ids"] = nlohmann::json::array(); + return request.dump(); +} + +std::string external_pose_probe_payload() +{ + nlohmann::json request; + request["hardware_timestamp_us"] = now_us(); + request["pose_valid"] = false; + request["workshop_pose"] = { + {"x_m", 0.0}, + {"y_m", 0.0}, + {"z_m", 0.0}, + {"roll_rad", 0.0}, + {"pitch_rad", 0.0}, + {"yaw_rad", 0.0}, + }; + request["position_stddev_m"] = 0.0; + request["yaw_stddev_rad"] = 0.0; + request["tracking_loss_ratio"] = 0.0; + request["time_sync_offset_ms"] = 0.0; + request["quality_score"] = 1.0; + request["observed_target_count"] = 0; + request["reference_source_name"] = "workshop_orchestrator_precheck"; + request["active_job_id"] = "wifi6_link_precheck"; + return request.dump(); +} + +bool json_bool(const nlohmann::json & payload, const std::string & key, bool default_value) +{ + const auto it = payload.find(key); + if (it == payload.end() || !it->is_boolean()) { + return default_value; + } + return it->get(); +} + +std::string json_message(const nlohmann::json & payload, const std::string & fallback) +{ + const auto it = payload.find("message"); + if (it == payload.end() || !it->is_string() || it->get().empty()) { + return fallback; + } + return it->get(); +} + +bool optional_bool_passed( + const nlohmann::json & payload, + const std::string & key, + std::string & failure_reason) +{ + const auto it = payload.find(key); + if (it == payload.end() || !it->is_boolean() || it->get()) { + return true; + } + failure_reason = key + "=false"; + return false; +} + +} // namespace + +Wifi6LinkClient::Wifi6LinkClient(Config config) +: config_(std::move(config)) +{ +} + +Wifi6LinkClient::CheckResult Wifi6LinkClient::send_json_request( + FrameType request_type, + FrameType response_type, + const std::string & request_payload, + const std::string & link_name) const +{ + const int fd = connect_tcp(config_.vehicle_host, config_.vehicle_port, config_.timeout_ms); + const auto endpoint = endpoint_string(config_.vehicle_host, config_.vehicle_port); + if (fd < 0) { + return {false, link_name + " 无法连接 " + endpoint}; + } + + uint8_t header[8]; + write_le32(header, static_cast(request_type)); + write_le32(header + 4, static_cast(request_payload.size())); + bool ok = write_all(fd, header, sizeof(header)); + if (ok && !request_payload.empty()) { + ok = write_all(fd, reinterpret_cast(request_payload.data()), request_payload.size()); + } + if (!ok) { + ::close(fd); + return {false, link_name + " 向 " + endpoint + " 发送请求失败"}; + } + + uint8_t response_header[8]; + if (!read_all(fd, response_header, sizeof(response_header))) { + ::close(fd); + return {false, link_name + " 等待 " + endpoint + " 响应超时或连接断开"}; + } + const auto actual_type = static_cast(read_le32(response_header)); + const auto payload_len = read_le32(response_header + 4); + std::string response_payload(payload_len, '\0'); + if (payload_len > 0 && + !read_all(fd, reinterpret_cast(response_payload.data()), payload_len)) + { + ::close(fd); + return {false, link_name + " 读取 " + endpoint + " 响应 payload 失败"}; + } + ::close(fd); + + if (actual_type != response_type) { + return {false, link_name + " 响应帧类型不匹配"}; + } + + try { + const auto payload = nlohmann::json::parse(response_payload.empty() ? "{}" : response_payload); + const bool success = json_bool(payload, "success", false); + if (!success) { + return {false, json_message(payload, link_name + " readiness=false")}; + } + std::string readiness_failure; + const char * readiness_keys[] = { + "agent_ready", + "estop_released", + "vehicle_safe_to_move", + "motion_control_ready", + "trajectory_executor_ready", + "vehicle_feedback_ready", + "control_output_ready", + "external_pose_feedback_ready", + "capture_pipeline_ready", + "sensor_data_stream_ready", + }; + for (const auto * key : readiness_keys) { + if (!optional_bool_passed(payload, key, readiness_failure)) { + return {false, json_message(payload, link_name + " " + readiness_failure)}; + } + } + return {true, json_message(payload, link_name + " 链路正常")}; + } catch (const std::exception & exc) { + return {false, link_name + " 响应 JSON 解析失败: " + std::string(exc.what())}; + } +} + +Wifi6LinkClient::CheckResult Wifi6LinkClient::check_chassis() const +{ + auto result = send_json_request( + FrameType::CHASSIS_GET_READINESS_REQ, + FrameType::CHASSIS_GET_READINESS_RSP, + readiness_payload("chassis"), + "底盘 WiFi6 链路"); + if (!result.passed) { + return result; + } + return result; +} + +Wifi6LinkClient::CheckResult Wifi6LinkClient::check_control() const +{ + return send_json_request( + FrameType::CONTROL_GET_READINESS_REQ, + FrameType::CONTROL_GET_READINESS_RSP, + readiness_payload("control"), + "运控 WiFi6 链路"); +} + +Wifi6LinkClient::CheckResult Wifi6LinkClient::check_sensor() const +{ + return send_json_request( + FrameType::SENSOR_GET_READINESS_REQ, + FrameType::SENSOR_GET_READINESS_RSP, + readiness_payload("sensor"), + "传感器 WiFi6 链路"); +} + +Wifi6LinkClient::CheckResult Wifi6LinkClient::check_external_pose() const +{ + return send_json_request( + FrameType::EXTERNAL_POSE_PUSH_REQ, + FrameType::EXTERNAL_POSE_PUSH_RSP, + external_pose_probe_payload(), + "外部真值位姿 WiFi6 链路"); +} + +} // namespace workshop_orchestrator_v2 diff --git a/agv_calib_brain/src/agv_calib_core/workshop_orchestrator/src/workshop_orchestrator_v2_node.cpp b/agv_calib_brain/src/core/agv_calib_core/workshop_orchestrator/src/workshop_orchestrator_v2_node.cpp similarity index 83% rename from agv_calib_brain/src/agv_calib_core/workshop_orchestrator/src/workshop_orchestrator_v2_node.cpp rename to agv_calib_brain/src/core/agv_calib_core/workshop_orchestrator/src/workshop_orchestrator_v2_node.cpp index 47f4b19..b0e776f 100644 --- a/agv_calib_brain/src/agv_calib_core/workshop_orchestrator/src/workshop_orchestrator_v2_node.cpp +++ b/agv_calib_brain/src/core/agv_calib_core/workshop_orchestrator/src/workshop_orchestrator_v2_node.cpp @@ -4,9 +4,11 @@ #include // 拼接 session_id 时要用字符串流。 #include // 把整场执行放到后台线程时要用 std::thread。 +#include "calibration_workshop_orchestration_interfaces/msg/approval_state.hpp" +#include "calibration_workshop_orchestration_interfaces/msg/precheck_item.hpp" + namespace workshop_orchestrator_v2 { - using calibration_workshop_orchestration_interfaces::msg::WorkshopEvent; // 进度通知消息短名。 using calibration_workshop_orchestration_interfaces::msg::WorkshopEventType; // 进度通知类型短名。 using calibration_workshop_orchestration_interfaces::srv::AcknowledgeManualStep; // 人工确认接口短名。 @@ -20,6 +22,27 @@ using calibration_workshop_orchestration_interfaces::srv::ResumeWorkshopSession; WorkshopOrchestratorV2Node::WorkshopOrchestratorV2Node(const rclcpp::NodeOptions & options) : Node("workshop_orchestrator_v2", options) // 创建一个名为 workshop_orchestrator_v2 的 ROS 节点。 { + wifi6_precheck_enabled_ = declare_parameter("wifi6_precheck_enabled", true); + wifi6_precheck_require_all_links_ = declare_parameter("wifi6_precheck_require_all_links", true); + Wifi6LinkClient::Config wifi6_config; + wifi6_config.vehicle_host = declare_parameter("wifi6_vehicle_host", "127.0.0.1"); + wifi6_config.vehicle_port = static_cast(declare_parameter("wifi6_vehicle_port", 9000)); + wifi6_config.timeout_ms = declare_parameter("wifi6_timeout_ms", 5000); + wifi6_link_client_ = std::make_unique(wifi6_config); + parameter_callback_handle_ = this->add_on_set_parameters_callback( + [this](const std::vector & parameters) { + rcl_interfaces::msg::SetParametersResult result; + result.successful = true; + for (const auto & parameter : parameters) { + if (parameter.get_name() == "wifi6_precheck_enabled") { + wifi6_precheck_enabled_ = parameter.as_bool(); + } else if (parameter.get_name() == "wifi6_precheck_require_all_links") { + wifi6_precheck_require_all_links_ = parameter.as_bool(); + } + } + return result; + }); + event_publisher_ = this->create_publisher("/workshop_v2/events", 10); // 建一个 topic 发布器,后面把“任务进度通知”都发到这里。 create_session_service_ = this->create_service( @@ -99,6 +122,9 @@ void WorkshopOrchestratorV2Node::handle_create_session( response->response.session = record.session; // 把整条任务快照一起返回,外部不用马上再查一次。 publish_event(record, WorkshopEventType::SESSION_STATE_CHANGED, "Session created."); // 发一条“新任务已经创建”通知,界面和日志系统可以立刻显示。 + if (request->request.auto_build_execution_plan) { + publish_event(record, WorkshopEventType::EXECUTION_PLAN_BUILT, "Execution plan built."); + } } void WorkshopOrchestratorV2Node::handle_get_session( @@ -369,15 +395,127 @@ WorkshopOrchestratorV2Node::SessionExecutionContext WorkshopOrchestratorV2Node:: WorkshopPrecheckResponse WorkshopOrchestratorV2Node::run_precheck_step(const std::string & session_id) { + WorkshopSession session_snapshot; + { + std::lock_guard lock(mutex_); + session_snapshot = sessions_.at(session_id).session; + } + + auto precheck = precheck_runner_.run(session_snapshot); // 先检查计划和 stage metadata。 + run_wifi6_link_precheck(session_snapshot, precheck); // 再检查车间工控机 <-> 车端电脑四条 WiFi6/TCP 链路。 + std::lock_guard lock(mutex_); auto & record = sessions_.at(session_id); - const auto precheck = precheck_runner_.run(record.session); // 真正执行开跑前检查:当前主要看“有没有步骤可跑”。 record.last_precheck = precheck; // 把这次预检结果存下来,后面查“为什么没开跑”时可以直接返回。 record.session.updated_timestamp_us = now_us(); publish_event(record, WorkshopEventType::PRECHECK_COMPLETED, precheck.message); // 给外部发通知:预检做完了,并把结论文本一起带出去。 return precheck; } +void WorkshopOrchestratorV2Node::append_precheck_item( + WorkshopPrecheckResponse & precheck, + const std::string & item_code, + const std::string & display_name, + bool passed, + const std::string & message, + uint8_t stage_type, + uint8_t module_type) +{ + calibration_workshop_orchestration_interfaces::msg::PrecheckItem item; + item.item_code = item_code; + item.display_name = display_name; + item.passed = passed; + item.blocking = true; + item.error_code = make_error_code(passed ? ErrorCode::OK : ErrorCode::NETWORK_LOSS); + item.message = message; + item.related_stage_type = make_stage_type(stage_type); + item.related_module_type = make_module_type(module_type); + precheck.items.push_back(item); + if (!passed) { + precheck.blocking_issue_count += 1; + precheck.all_passed = false; + precheck.success = false; + precheck.error_code = make_error_code(ErrorCode::NETWORK_LOSS); + } +} + +void WorkshopOrchestratorV2Node::run_wifi6_link_precheck( + const WorkshopSession & session, + WorkshopPrecheckResponse & precheck) +{ + if (!wifi6_precheck_enabled_ || !wifi6_link_client_) { + return; + } + + bool needs_chassis = wifi6_precheck_require_all_links_; + bool needs_control = wifi6_precheck_require_all_links_; + bool needs_sensor = wifi6_precheck_require_all_links_; + bool needs_external_pose = wifi6_precheck_require_all_links_; + if (!wifi6_precheck_require_all_links_) { + for (const auto & stage : session.stage_plan) { + needs_chassis = needs_chassis || stage.module_type.value == CalibrationModuleType::CHASSIS_MODULE; + needs_control = needs_control || stage.module_type.value == CalibrationModuleType::CONTROL_MODULE; + needs_sensor = needs_sensor || + stage.module_type.value == CalibrationModuleType::SENSOR_INTRINSIC_MODULE || + stage.module_type.value == CalibrationModuleType::SENSOR_EXTRINSIC_MODULE || + stage.module_type.value == CalibrationModuleType::HAND_EYE_MODULE; + needs_external_pose = needs_external_pose || + stage.module_type.value == CalibrationModuleType::CONTROL_MODULE || + stage.module_type.value == CalibrationModuleType::EXTERNAL_LOCALIZATION_MODULE; + } + } + + if (needs_chassis) { + const auto result = wifi6_link_client_->check_chassis(); + append_precheck_item( + precheck, + "wifi6.chassis", + "底盘 WiFi6/TCP 链路", + result.passed, + result.message, + WorkflowStageType::CHASSIS_CALIBRATION_STAGE, + CalibrationModuleType::CHASSIS_MODULE); + } + if (needs_control) { + const auto result = wifi6_link_client_->check_control(); + append_precheck_item( + precheck, + "wifi6.control", + "运控 WiFi6/TCP 链路", + result.passed, + result.message, + WorkflowStageType::CONTROL_CALIBRATION_STAGE, + CalibrationModuleType::CONTROL_MODULE); + } + if (needs_sensor) { + const auto result = wifi6_link_client_->check_sensor(); + append_precheck_item( + precheck, + "wifi6.sensor", + "传感器 WiFi6/TCP 链路", + result.passed, + result.message, + WorkflowStageType::SENSOR_INTRINSIC_CALIBRATION_STAGE, + CalibrationModuleType::SENSOR_INTRINSIC_MODULE); + } + if (needs_external_pose) { + const auto result = wifi6_link_client_->check_external_pose(); + append_precheck_item( + precheck, + "wifi6.external_pose", + "外部真值位姿 WiFi6/TCP 链路", + result.passed, + result.message, + WorkflowStageType::EXTERNAL_REFERENCE_READY_CHECK_STAGE, + CalibrationModuleType::EXTERNAL_LOCALIZATION_MODULE); + } + + precheck.ready_for_start = precheck.all_passed; + precheck.message = precheck.ready_for_start ? + "Precheck passed: metadata and WiFi6 links are ready." : + "Precheck failed: metadata or WiFi6 link check failed."; +} + void WorkshopOrchestratorV2Node::mark_session_running(const std::string & session_id) { std::lock_guard lock(mutex_); @@ -441,6 +579,19 @@ bool WorkshopOrchestratorV2Node::run_all_stages( return false; } if (attempt < stage.retry_limit) { + { + std::lock_guard lock(mutex_); + auto & record = sessions_.at(context.session_id); + record.session.updated_timestamp_us = now_us(); + publish_event( + record, + WorkshopEventType::SESSION_STATE_CHANGED, + "Stage retry scheduled.", + stage.stage_id, + JobState::PENDING, + stage.stage_type.value, + stage.module_type.value); + } continue; } break; @@ -480,6 +631,8 @@ bool WorkshopOrchestratorV2Node::run_single_stage( result.stage_id = stage.stage_id; // 先把结果和当前步骤绑定起来。 result.stage_type = stage.stage_type; // 带回这一步属于哪类步骤。 result.module_type = stage.module_type; // 带回这一步属于哪个模块。 + result.final_job_state = make_job_state(JobState::RUNNING); + result.approval_state.value = calibration_workshop_orchestration_interfaces::msg::ApprovalState::APPROVAL_STATE_UNSPECIFIED; if (!dispatch_stage_to_module(session_id, stage, result, failure_reason)) { if (failure_reason.empty()) { @@ -489,11 +642,16 @@ bool WorkshopOrchestratorV2Node::run_single_stage( if (result.summary.empty()) { result.summary = failure_reason; // 把失败原因同步写进阶段结果,方便后面进报告。 } - if (result.final_job_state.state == JobState::JOB_STATE_UNSPECIFIED) { + if (result.final_job_state.state == JobState::JOB_STATE_UNSPECIFIED || + result.final_job_state.state == JobState::RUNNING) + { result.final_job_state = make_job_state(JobState::FAILED); // 下层没写状态时,这里明确标成失败。 } result.success = false; result.auto_acceptance_passed = false; + if (result.failure_root_cause.empty()) { + result.failure_root_cause = failure_reason; + } record_stage_result(session_id, result); mark_stage_failed(session_id, stage, failure_reason); // 发一条“这一步失败了”的通知。 return false; @@ -503,6 +661,14 @@ bool WorkshopOrchestratorV2Node::run_single_stage( if (failure_reason.empty()) { failure_reason = result.summary.empty() ? "Stage failed." : result.summary; // 执行器返回失败但没写原因时,这里补齐失败原因。 } + if (result.final_job_state.state == JobState::JOB_STATE_UNSPECIFIED || + result.final_job_state.state == JobState::RUNNING) + { + result.final_job_state = make_job_state(JobState::FAILED); + } + if (result.failure_root_cause.empty()) { + result.failure_root_cause = failure_reason; + } record_stage_result(session_id, result); mark_stage_failed(session_id, stage, failure_reason); return false; @@ -771,7 +937,9 @@ void WorkshopOrchestratorV2Node::mark_stage_failed( stage.stage_id, JobState::FAILED, stage.stage_type.value, - stage.module_type.value); + stage.module_type.value, + false, + calibration_workshop_orchestration_interfaces::msg::ApprovalState::APPROVAL_STATE_UNSPECIFIED); } void WorkshopOrchestratorV2Node::mark_stage_started(const std::string & session_id, const StagePlan & stage) @@ -787,7 +955,9 @@ void WorkshopOrchestratorV2Node::mark_stage_started(const std::string & session_ stage.stage_id, JobState::RUNNING, stage.stage_type.value, - stage.module_type.value); + stage.module_type.value, + false, + calibration_workshop_orchestration_interfaces::msg::ApprovalState::APPROVAL_STATE_UNSPECIFIED); } void WorkshopOrchestratorV2Node::mark_stage_completed( @@ -802,11 +972,13 @@ void WorkshopOrchestratorV2Node::mark_stage_completed( publish_event( record, WorkshopEventType::STAGE_COMPLETED, - "Stage completed.", + result.summary.empty() ? "Stage completed." : result.summary, stage.stage_id, - JobState::SUCCEEDED, + result.final_job_state.state == JobState::JOB_STATE_UNSPECIFIED ? JobState::SUCCEEDED : result.final_job_state.state, stage.stage_type.value, - stage.module_type.value); + stage.module_type.value, + false, + result.approval_state.value); } void WorkshopOrchestratorV2Node::wait_if_paused(const std::string & session_id) @@ -834,7 +1006,17 @@ void WorkshopOrchestratorV2Node::record_stage_result( { std::lock_guard lock(mutex_); auto & record = sessions_.at(session_id); - record.stage_results.push_back(result); + bool replaced = false; + for (auto & existing : record.stage_results) { + if (existing.stage_id == result.stage_id) { + existing = result; + replaced = true; + break; + } + } + if (!replaced) { + record.stage_results.push_back(result); + } } void WorkshopOrchestratorV2Node::finish_session_precheck_failed( @@ -848,7 +1030,6 @@ void WorkshopOrchestratorV2Node::finish_session_precheck_failed( auto & record = sessions_.at(context.session_id); record.session.state = make_session_state(WorkshopSessionState::FAILED); record.session.active_stage_id.clear(); - record.session.active_stage_id.clear(); record.session.updated_timestamp_us = now_us(); record.report = report_builder_.build( record.session, @@ -856,14 +1037,14 @@ void WorkshopOrchestratorV2Node::finish_session_precheck_failed( false, context.started_timestamp_us, now_us(), - "Precheck failed."); + record.last_precheck.message.empty() ? "Precheck failed." : record.last_precheck.message); record.executing = false; record.report_ready = true; publish_event(record, WorkshopEventType::REPORT_READY, "Report ready: precheck failed."); - result->result.success = true; - result->result.error_code = make_error_code(ErrorCode::OK); - result->result.message = "Precheck failed."; + result->result.success = false; + result->result.error_code = make_error_code(ErrorCode::VALIDATION_FAILED); + result->result.message = record.last_precheck.message.empty() ? "Precheck failed." : record.last_precheck.message; result->result.report = record.report; } goal_handle->abort(result); @@ -892,8 +1073,8 @@ void WorkshopOrchestratorV2Node::finish_session_failed( record.report_ready = true; publish_event(record, WorkshopEventType::REPORT_READY, "Report ready: session failed."); - result->result.success = true; - result->result.error_code = make_error_code(ErrorCode::OK); + result->result.success = false; + result->result.error_code = make_error_code(ErrorCode::INVALID_STATE); result->result.message = failure_reason; result->result.report = record.report; } @@ -952,8 +1133,8 @@ void WorkshopOrchestratorV2Node::finish_session_canceled( record.report_ready = true; publish_event(record, WorkshopEventType::SESSION_STATE_CHANGED, "Session canceled."); - result->result.success = true; - result->result.error_code = make_error_code(ErrorCode::OK); + result->result.success = false; + result->result.error_code = make_error_code(ErrorCode::ROLLBACK_REQUIRED); result->result.message = "Canceled."; result->result.report = record.report; } @@ -994,4 +1175,4 @@ std::string WorkshopOrchestratorV2Node::make_session_id() const return oss.str(); } -} // namespace workshop_orchestrator_v2 \ No newline at end of file +} // namespace workshop_orchestrator_v2 diff --git a/agv_calib_brain/src/deployment/checklists/sim_to_site_checklist.md b/agv_calib_brain/src/deployment/checklists/sim_to_site_checklist.md new file mode 100644 index 0000000..ff19473 --- /dev/null +++ b/agv_calib_brain/src/deployment/checklists/sim_to_site_checklist.md @@ -0,0 +1,18 @@ +# Simulation To Site Checklist + +Before promoting a simulation-validated setup to site deployment, verify: + +- ROS 2 service/action/topic names match the deployment profile. +- `vehicle_id` is stable and checked by gateway and vehicle-side agent. +- `base_link` definition matches the real vehicle, especially rear axle center. +- Sensor frame IDs match the real URDF/TF tree. +- Calibration target dimensions are measured onsite and not copied from defaults. +- Vehicle-side agent owns low-level motion control. +- Workshop PC cannot publish directly to actuator topics. +- Emergency stop was tested from gateway and vehicle-side agent. +- Disconnect or timeout causes vehicle stop. +- WiFi or wired network timeout behavior was tested. +- Vehicle path is clear at the real site. +- First site run is low-speed and supervised. +- Calibration results include vehicle ID, config version, operator, and timestamp. + diff --git a/agv_calib_brain/src/deployment/profiles/sim_workshop.yaml b/agv_calib_brain/src/deployment/profiles/sim_workshop.yaml new file mode 100644 index 0000000..48b1be2 --- /dev/null +++ b/agv_calib_brain/src/deployment/profiles/sim_workshop.yaml @@ -0,0 +1,73 @@ +mode: sim +vehicle_id: demo_agv_001 + +workshop_pc: + gateway: + vehicle_host: 127.0.0.1 + vehicle_port: 9000 + timeout_ms: 5000 + +vehicle_agent: + type: isaac_vehicle_agent_sim + bind_host: 0.0.0.0 + private_cmd_vel_topic: /vehicle/demo_agv_001/actuator/cmd_vel + external_pose_topic: /isaac/external_localization/telemetry + external_pose_transport: wifi6_tcp + chassis_telemetry_topic: /chassis/telemetry + internal_ackermann_command_topic: /vehicle/demo_agv_001/internal/ackermann_cmd + internal_state_topic: /vehicle/demo_agv_001/internal/state + internal_health_topic: /vehicle/demo_agv_001/internal/health + internal_time_sync_topic: /vehicle/demo_agv_001/internal/time_sync_status + wheel_base_m: 0.80 + max_steering_angle_rad: 0.60 + internal_command_timeout_sec: 0.5 + +vehicle_sensor_agent: + type: isaac_vehicle_sensor_agent_sim + bind_host: 0.0.0.0 + front_camera_sensor_id: demo_front_camera + down_camera_sensor_id: demo_down_camera + lidar_3d_sensor_id: demo_lidar_3d + lidar_2d_sensor_id: demo_lidar_2d + imu_sensor_id: demo_imu + front_camera_topic: /sensor/front_camera/image_raw + down_camera_topic: /sensor/down_camera/image_raw + lidar_3d_topic: /sensor/lidar_3d/pointcloud + lidar_2d_topic: /sensor/lidar_2d/scan + imu_topic: /sensor/imu/data + sensor_link_status_topic: /vehicle/demo_agv_001/internal/sensor_link_status + max_frame_payload_bytes: 262144 + +external_pose_bridge: + type: external_pose_wifi6_bridge + source_topic: /isaac/external_localization/telemetry + reference_source_name: isaac_external_truth + max_hz: 30.0 + +workshop_sensor_ingest: + type: workshop_sensor_ingest_sim + publish_prefix: /workshop/vehicle_sensor + poll_hz: 15.0 + max_payload_bytes: 4194304 + +isaac: + scene_script: src/simulation/isaac_workshop_sim/scripts/build_calibration_room.py + scene_manifest_path: .isaac_cache/workshop_scene_manifest.json + cmd_vel_topic: /vehicle/demo_agv_001/actuator/cmd_vel + +frames: + map: workshop + base_link: rear_axle_center + +targets: + down_camera_charuco: + square_size_m: 0.09 + squares_x: 30 + squares_y: 10 + lidar_2d_corner_bay: + reserved_for: lidar_2d_extrinsic + +safety: + max_speed_mps: 0.3 + command_timeout_sec: 1.0 + stop_on_disconnect: true diff --git a/agv_calib_brain/src/deployment/profiles/site_template.yaml b/agv_calib_brain/src/deployment/profiles/site_template.yaml new file mode 100644 index 0000000..f397a31 --- /dev/null +++ b/agv_calib_brain/src/deployment/profiles/site_template.yaml @@ -0,0 +1,59 @@ +mode: site +vehicle_id: replace_with_real_vehicle_id + +workshop_pc: + gateway: + vehicle_host: 192.168.10.42 + vehicle_port: 9000 + timeout_ms: 5000 + +vehicle_agent: + type: real_vehicle_agent + vendor_interface: replace_with_can_plc_sdk_or_controller + external_pose_transport: wifi6 + internal_ackermann_command_topic: /vehicle/replace_with_real_vehicle_id/internal/ackermann_cmd + internal_state_topic: /vehicle/replace_with_real_vehicle_id/internal/state + internal_health_topic: /vehicle/replace_with_real_vehicle_id/internal/health + internal_time_sync_topic: /vehicle/replace_with_real_vehicle_id/internal/time_sync_status + wheel_base_m: measured_on_site + max_steering_angle_rad: measured_on_site + +vehicle_sensor_agent: + type: real_vehicle_sensor_agent + front_camera_sensor_id: replace_with_front_camera_id + down_camera_sensor_id: replace_with_down_camera_id + lidar_3d_sensor_id: replace_with_lidar_3d_id + lidar_2d_sensor_id: replace_with_lidar_2d_id + imu_sensor_id: replace_with_imu_id + sensor_data_transport: wifi6 + sensor_link_status_topic: /vehicle/replace_with_real_vehicle_id/internal/sensor_link_status + +external_pose_bridge: + type: external_pose_wifi6_bridge + source_topic: replace_with_external_truth_ros_topic + reference_source_name: replace_with_external_truth_source + max_hz: 30.0 + +workshop_sensor_ingest: + type: workshop_sensor_ingest + publish_prefix: /workshop/vehicle_sensor + poll_hz: 15.0 + max_payload_bytes: 4194304 + +frames: + map: workshop + base_link: rear_axle_center + +targets: + down_camera_charuco: + square_size_m: measured_on_site + squares_x: measured_on_site + squares_y: measured_on_site + lidar_2d_corner_bay: + measured_planes: required + +safety: + max_speed_mps: 0.1 + command_timeout_sec: 1.0 + stop_on_disconnect: true + emergency_stop_required: true diff --git a/agv_calib_brain/src/docs/sim_to_site_code_boundary.md b/agv_calib_brain/src/docs/sim_to_site_code_boundary.md new file mode 100644 index 0000000..0c68258 --- /dev/null +++ b/agv_calib_brain/src/docs/sim_to_site_code_boundary.md @@ -0,0 +1,34 @@ +# Simulation To Site Code Boundary + +Use simulation to validate interfaces, task flow, target layouts, and safety +state machines. Do not move Isaac-specific code into site deployment. + +Reusable across simulation and site: + +- ROS 2 interfaces +- TCP frame protocol +- `vehicle_agent_gateway` +- workshop orchestration flow +- calibration algorithm services +- manifest/profile schema +- safety state machine semantics +- reports and logs + +Simulation only: + +- Isaac scene construction +- simulated vehicle URDF/USD assets +- simulated sensor creation +- synthetic telemetry publishers +- simulated target geometry generation +- WiFi/link fault injection proxy when it is not part of real deployment + +Site only: + +- real vehicle SDK or PLC/CAN integration +- real network addressing and certificates +- real emergency stop wiring and validation +- measured target dimensions +- measured sensor extrinsics and vehicle profile +- site acceptance logs + diff --git a/agv_calib_brain/src/simulation/README.md b/agv_calib_brain/src/simulation/README.md new file mode 100644 index 0000000..e86d604 --- /dev/null +++ b/agv_calib_brain/src/simulation/README.md @@ -0,0 +1,145 @@ +# 仿真代码 + +这个目录只放现场部署前使用的仿真验证代码。 + +仿真阶段要验证三件事: + +- Isaac 车间、车辆、传感器和标定靶布置是否合理。 +- 车间工控机只能通过 gateway 与“车端电脑”通信,不能直接发布底层执行器命令。 +- 仿真车端和未来真实 Windows 车端使用同一套请求/响应边界。 + +## 环境功能和验收边界 + +| 组件 | 作用 | 验证什么 | +| --- | --- | --- | +| `isaac_workshop_sim` | 构建标定车间、阿克曼小车、传感器和标定靶 | 验证真值位姿、前视相机、下视相机、3D 雷达、2D 雷达、IMU 是否能从 Isaac 发布出来 | +| `isaac_vehicle_unified_agent_sim.py` | 模拟 Windows 车端电脑统一 WiFi6/TCP 入口 | 验证底盘、运控、外部真值位姿和传感器请求都从同一个车端端口进入 | +| `vehicle_agent_sim` | 独立底盘/运控/外部位姿后端,保留作调试工具 | 验证车端控制逻辑本身,不作为默认一键闭环入口 | +| `vehicle_sensor_agent_sim` | 独立传感器采集后端,保留作调试工具 | 验证车端能订阅 Isaac 传感器,并按传感器 ID 提供最新数据 | +| `vehicle_wifi6_gateway_sim.py` | 旧版分域后端聚合 gateway,保留作兼容工具 | 默认一键闭环不再使用它 | +| `external_pose_wifi6_bridge_sim.py` | 模拟外部真值系统通过 WiFi6 给车端提供定位 | 验证车端使用外部真值位姿,而不是直接订阅 Isaac 内部 topic | +| `workshop_sensor_ingest_sim.py` | 模拟车间侧从车端拉取传感器数据 | 验证车端传感器数据能进入车间工控机侧 ROS topic | +| `smoke_test_workshop_orchestrator.py` | 车间总控端到端验收 | 验证创建 session、阶段调度、WiFi6 预检、阶段结果和报告生成 | + +当前 smoke test 的通过条件主要是“进程、接口、通信和编排可用”。它不等价于真实标定算法已经完成: + +- 底盘阿克曼算法目前是模板输出,验证的是底盘任务分发和结果回传。 +- 运控 Pure Pursuit 阶段会检查控制遥测数量并计算示例误差,但还不是完整参数优化闭环。 +- 前视/下视相机内参阶段目前是算法模板,验证的是传感器标定任务调度和 readiness。 +- 2D 雷达角落、墙面棋盘格、下视相机 3D 台阶是否产生足够高质量观测,需要后续专门采样质量脚本验证。 + +## 目录 + +- `isaac_workshop_sim/` + Isaac 标定车间构建脚本、仿真车辆 URDF、仿真传感器和场景 manifest。 + +- `vehicle_agent_sim/` + 仿真车端电脑。它监听与真实 Windows 小车一致的 TCP 端口,并把被接受的动作转换成私有 Isaac 控制话题。 + +- `legacy_calibration_sim/` + 旧版 Gazebo/ROS 仿真包,先保留作参考。 + +- `tools/` + 仿真启动和验收工具,包括外部真值位姿 WiFi6/TCP 桥、车间侧传感器 ingest、TCP smoke test。 + +## 一键生成启动命令 + +默认读取 `src/deployment/profiles/sim_workshop.yaml`: + +```bash +python3 src/simulation/tools/launch_sim_stack.py --dry-run +``` + +启动完整仿真链路: + +```bash +python3 src/simulation/tools/launch_sim_stack.py +``` + +只启动仿真车端: + +```bash +python3 src/simulation/tools/launch_sim_stack.py --component vehicle-agent +``` + +只启动外部真值位姿桥: + +```bash +python3 src/simulation/tools/launch_sim_stack.py --component external-pose-bridge +``` + +只启动车间侧传感器接入: + +```bash +python3 src/simulation/tools/launch_sim_stack.py --component sensor-ingest +``` + +只启动 Isaac 场景: + +```bash +python3 src/simulation/tools/launch_sim_stack.py --component isaac +``` + +## 推荐测试顺序 + +1. 清理旧节点: + +```bash +./stop_isaac_real_sim_stack.sh --list +./stop_isaac_real_sim_stack.sh --kill +``` + +2. 启动完整 Isaac 链路并执行默认验收: + +```bash +./run_isaac_real_sim_test.sh --headless +``` + +3. 分阶段定位问题: + +```bash +./run_isaac_real_sim_test.sh --headless --tasks external +./run_isaac_real_sim_test.sh --headless --tasks external,chassis +./run_isaac_real_sim_test.sh --headless --tasks external,chassis,control +./run_isaac_real_sim_test.sh --headless --tasks external,chassis,control,sensor_intrinsic +``` + +## TCP 快速验收 + +仿真车端启动后,可以先不动车,只检查 readiness 和急停: + +```bash +python3 src/simulation/tools/smoke_test_vehicle_agent.py +``` + +如果需要让车执行一个很短的直线动作: + +```bash +python3 src/simulation/tools/smoke_test_vehicle_agent.py --motion +``` + +验证轨迹跟踪时,外部真值位姿默认通过 `workshop_pc.gateway.vehicle_port` 进入车端统一入口,不再让车端直接订阅 Isaac 位姿 topic: + +```bash +python3 src/simulation/tools/smoke_test_vehicle_agent.py --trajectory --fake-telemetry +``` + +传感器链路验收: + +```bash +python3 src/simulation/tools/smoke_test_vehicle_sensor_agent.py --fake-sensors +``` + +## 边界 + +共享协议和 ROS 2 contract 不放在这里,而在: + +- `src/communication/win_ubuntu_bridge/*_interfaces` +- `src/communication/win_ubuntu_bridge/vehicle_agent_gateway` + +核心编排和标定算法不放在这里,而在: + +- `src/core/agv_calib_core/workshop_orchestrator` +- `src/core/agv_calib_core/*_calibration_service` + +仿真代码不要复制到真实车端部署目录。真实车端只复用接口语义,不复用 Isaac API。 diff --git a/agv_calib_brain/src/simulation/isaac_workshop_sim/README.md b/agv_calib_brain/src/simulation/isaac_workshop_sim/README.md new file mode 100644 index 0000000..3ae9805 --- /dev/null +++ b/agv_calib_brain/src/simulation/isaac_workshop_sim/README.md @@ -0,0 +1,41 @@ +# Isaac 标定车间仿真 + +这个模块负责构建部署前验证用的 Isaac 标定车间。 + +包含: + +- 车间几何结构。 +- 前轮转向、后轮驱动的阿克曼小车 URDF。 +- 车载 3D LiDAR、2D LiDAR、IMU、前视相机、下视相机。 +- 墙面棋盘格阵列。 +- 2D LiDAR 外参标定角落。 +- 下视相机 3D ChArUco 内参标定台。 +- 底盘标定路线和停车标记。 +- 场景 manifest 生成。 + +默认启动: + +```bash +python3 src/simulation/tools/launch_sim_stack.py --component isaac +``` + +直接启动场景脚本: + +```bash +python3 src/simulation/isaac_workshop_sim/scripts/build_calibration_room.py \ + --vehicle-id demo_agv_001 \ + --cmd-vel-topic /vehicle/demo_agv_001/actuator/cmd_vel \ + --scene-manifest-path .isaac_cache/workshop_scene_manifest.json +``` + +headless 模式: + +```bash +python3 src/simulation/tools/launch_sim_stack.py --component isaac --headless +``` + +边界规则: + +- Isaac API 只能留在这里。 +- 真实车辆 SDK、PLC、CAN、厂商控制器对接代码不能放进这里。 +- 场景中可复用的信息通过 manifest 和 deployment profile 输出。 diff --git a/agv_calib_brain/src/simulation/isaac_workshop_sim/assets/urdf/ackermann_front_steer_rear_drive.urdf b/agv_calib_brain/src/simulation/isaac_workshop_sim/assets/urdf/ackermann_front_steer_rear_drive.urdf new file mode 100644 index 0000000..dfa90f8 --- /dev/null +++ b/agv_calib_brain/src/simulation/isaac_workshop_sim/assets/urdf/ackermann_front_steer_rear_drive.urdf @@ -0,0 +1,348 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/agv_calib_brain/src/simulation/isaac_workshop_sim/scripts/build_calibration_room.py b/agv_calib_brain/src/simulation/isaac_workshop_sim/scripts/build_calibration_room.py new file mode 100644 index 0000000..cbc5aaa --- /dev/null +++ b/agv_calib_brain/src/simulation/isaac_workshop_sim/scripts/build_calibration_room.py @@ -0,0 +1,2420 @@ +import argparse +import json +import math +import os +import time +import xml.etree.ElementTree as ET +from pathlib import Path + + +def parse_args(): + parser = argparse.ArgumentParser(description="Isaac 标定车间构建脚本") + parser.add_argument("--headless", action="store_true", help="以无界面模式启动 Isaac Sim") + parser.add_argument("--room-length", type=float, default=10.0, help="车间长度(米)") + parser.add_argument("--room-width", type=float, default=6.0, help="车间宽度(米)") + parser.add_argument("--room-height", type=float, default=3.5, help="车间高度(米)") + parser.add_argument("--wall-thickness", type=float, default=0.2, help="墙体厚度(米)") + parser.add_argument("--light-intensity", type=float, default=30000.0, help="顶灯强度") + parser.add_argument("--checkerboard-rows", type=int, default=6, help="棋盘格行数") + parser.add_argument("--checkerboard-cols", type=int, default=9, help="棋盘格列数") + parser.add_argument("--checkerboard-square-size", type=float, default=0.12, help="棋盘格单格边长(米)") + parser.add_argument("--camera-resolution-width", type=int, default=1280, help="相机宽度") + parser.add_argument("--camera-resolution-height", type=int, default=720, help="相机高度") + parser.add_argument("--camera-frequency", type=float, default=20.0, help="相机频率(Hz)") + 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("--cmd-vel-topic", type=str, default="/cmd_vel", help="车辆速度控制话题") + parser.add_argument("--lidar-config", type=str, default="Example_Rotary", help="Isaac LiDAR 配置名") + parser.add_argument("--disable-camera", action="store_true", help="不创建顶置相机") + parser.add_argument("--disable-lidars", action="store_true", help="不创建四角激光雷达") + parser.add_argument("--disable-vehicle-camera", action="store_true", help="不创建车载前视相机") + parser.add_argument("--disable-vehicle-lidar", action="store_true", help="不创建车载 3D 激光雷达") + parser.add_argument("--disable-down-camera", action="store_true", help="不创建车载下视相机") + parser.add_argument("--disable-vehicle-2d-lidar", action="store_true", help="不创建车载 2D 激光雷达") + parser.add_argument("--disable-vehicle-imu", action="store_true", help="不创建车载 IMU") + parser.add_argument("--vehicle-camera-topic", type=str, default="/sensor/front_camera/image_raw", help="车载前视相机图像话题") + parser.add_argument("--down-camera-topic", type=str, default="/sensor/down_camera/image_raw", help="车载下视相机图像话题") + parser.add_argument("--vehicle-lidar-topic", type=str, default="/sensor/lidar_3d/pointcloud", help="车载 3D 激光雷达点云话题") + parser.add_argument("--vehicle-2d-lidar-topic", type=str, default="/sensor/lidar_2d/scan", help="车载 2D 激光雷达 LaserScan 话题") + parser.add_argument("--vehicle-imu-topic", type=str, default="/sensor/imu/data", help="车载 IMU 话题") + parser.add_argument("--vehicle-camera-x", type=float, default=0.82, help="车载前视相机相对 base_link X 坐标") + parser.add_argument("--vehicle-camera-y", type=float, default=0.0, help="车载前视相机相对 base_link Y 坐标") + parser.add_argument("--vehicle-camera-z", type=float, default=0.32, help="车载前视相机相对 base_link Z 坐标") + parser.add_argument("--down-camera-x", type=float, default=0.40, help="车载下视相机相对 base_link X 坐标") + parser.add_argument("--down-camera-y", type=float, default=0.0, help="车载下视相机相对 base_link Y 坐标") + parser.add_argument("--down-camera-z", type=float, default=0.30, help="车载下视相机相对 base_link Z 坐标") + parser.add_argument("--disable-down-camera-intrinsic-target", action="store_true", help="不创建下视相机 3D ChArUco 内参标定台") + parser.add_argument("--down-camera-target-x", type=float, default=0.0, help="下视相机内参标定台中心 X 坐标") + parser.add_argument("--down-camera-target-y", type=float, default=-2.15, help="下视相机内参标定台中心 Y 坐标") + parser.add_argument("--down-camera-target-length", type=float, default=2.70, help="下视相机内参标定台长度(米)") + parser.add_argument("--down-camera-target-width", type=float, default=0.90, help="下视相机内参标定台宽度(米)") + parser.add_argument("--down-camera-target-max-height", type=float, default=0.045, help="下视相机内参标定台最高点高度(米)") + parser.add_argument("--down-camera-target-squares-x", type=int, default=30, help="下视相机 ChArUco 纹理 X 方向格数") + parser.add_argument("--down-camera-target-squares-y", type=int, default=10, help="下视相机 ChArUco 纹理 Y 方向格数") + parser.add_argument("--vehicle-lidar-x", type=float, default=0.55, help="车载 3D LiDAR 相对 base_link X 坐标") + parser.add_argument("--vehicle-lidar-y", type=float, default=0.0, help="车载 LiDAR 相对 base_link Y 坐标") + parser.add_argument("--vehicle-lidar-z", type=float, default=0.46, help="车载 3D LiDAR 相对 base_link Z 坐标") + parser.add_argument("--vehicle-2d-lidar-x", type=float, default=0.70, help="车载 2D LiDAR 相对 base_link X 坐标") + parser.add_argument("--vehicle-2d-lidar-y", type=float, default=0.0, help="车载 2D LiDAR 相对 base_link Y 坐标") + parser.add_argument("--vehicle-2d-lidar-z", type=float, default=0.24, help="车载 2D LiDAR 相对 base_link Z 坐标") + parser.add_argument("--vehicle-imu-x", type=float, default=0.40, help="车载 IMU 相对 base_link X 坐标") + parser.add_argument("--vehicle-imu-y", type=float, default=0.0, help="车载 IMU 相对 base_link Y 坐标") + parser.add_argument("--vehicle-imu-z", type=float, default=0.26, help="车载 IMU 相对 base_link Z 坐标") + parser.add_argument("--disable-2d-lidar-targets", action="store_true", help="不创建 2D 激光雷达角落外参标定靶标") + parser.add_argument("--lidar-2d-target-width", type=float, default=0.82, help="2D LiDAR 靶标水平宽度(米)") + parser.add_argument("--lidar-2d-target-height", type=float, default=1.60, help="2D LiDAR 靶标有效高度(米)") + parser.add_argument("--lidar-2d-target-thickness", type=float, default=0.04, help="2D LiDAR 靶标板厚(米)") + parser.add_argument("--lidar-2d-target-bottom-z", type=float, default=0.08, help="2D LiDAR 靶标底部高度(米)") + parser.add_argument("--lidar-2d-target-slope-angle-deg", type=float, default=45.0, help="2D LiDAR 斜板相对垂直基准面的夹角(度)") + parser.add_argument("--disable-external-telemetry", action="store_true", help="不发布 external 仿真遥测") + parser.add_argument("--external-telemetry-topic", type=str, default="/isaac/external_localization/telemetry", help="external 仿真遥测话题") + parser.add_argument("--external-reference-source-name", type=str, default="isaac_sim_truth_source", help="external 真值源名称") + parser.add_argument("--external-workcell-zone-id", type=str, default="isaac_workcell_zone_a", help="external 所属工位 ID") + parser.add_argument("--external-publish-hz", type=float, default=20.0, help="external 遥测发布频率(Hz)") + parser.add_argument("--external-position-noise-stddev-m", type=float, default=0.005, help="位置噪声标准差(米)") + parser.add_argument("--external-yaw-noise-stddev-rad", type=float, default=0.003, help="航向噪声标准差(弧度)") + parser.add_argument("--external-position-stddev-m", type=float, default=0.01, help="上报的位置标准差(米)") + parser.add_argument("--external-yaw-stddev-rad", type=float, default=0.01, help="上报的航向标准差(弧度)") + parser.add_argument("--external-tracking-loss-ratio", type=float, default=0.0, help="上报的跟踪丢失比例") + parser.add_argument("--external-time-sync-offset-ms", type=float, default=2.0, help="上报的时间同步偏差(毫秒)") + 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-pose-drop-rate", type=float, default=0.0, help="位姿失效率,取值 [0,1]") + 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-hz", type=float, default=20.0, help="底盘标定遥测发布频率(Hz)") + parser.add_argument( + "--chassis-type", + choices=["differential", "ackermann", "single_steer", "multi_steer"], + default="ackermann", + help="仿真底盘类型,用于遥测消息", + ) + parser.add_argument("--wheel-radius", type=float, default=0.10, help="遥测换算用轮半径(米)") + parser.add_argument("--wheel-track", type=float, default=0.52, help="差速/多轮底盘轮距(米)") + parser.add_argument("--wheel-base", type=float, default=0.80, help="阿克曼底盘轴距(米)") + parser.add_argument("--odom-noise-stddev-m", type=float, default=0.002, help="底盘里程计位置噪声标准差(米)") + parser.add_argument("--yaw-noise-stddev-rad", type=float, default=0.001, help="底盘里程计 yaw 噪声标准差(弧度)") + 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-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-y-m", type=float, default=0.0, help="运控直线参考轨迹的 Y 坐标(米)") + parser.add_argument("--control-parameter-version", type=str, default="isaac_control_baseline_v1", help="当前控制参数版本") + parser.add_argument("--disable-sensor-telemetry", action="store_true", help="不发布传感器标定遥测") + parser.add_argument("--sensor-telemetry-topic", type=str, default="/sensor_calibration/telemetry", help="传感器标定遥测话题") + parser.add_argument("--sensor-telemetry-hz", type=float, default=10.0, help="传感器标定遥测发布频率(Hz)") + parser.add_argument("--front-camera-sensor-id", type=str, default="demo_front_camera", help="前视相机传感器 ID") + parser.add_argument("--sensor-target-sample-count", type=int, default=20, help="传感器标定目标样本数") + 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("--disable-calibration-boards", action="store_true", help="不创建多姿态棋盘格标定目标阵列") + parser.add_argument("--disable-calibration-fixtures", action="store_true", help="不创建地面标定路线和停车目标标识") + parser.add_argument("--straight-track-length", type=float, default=5.0, help="底盘直线标定路线长度(米)") + parser.add_argument("--straight-track-width", type=float, default=0.7, help="底盘直线标定路线宽度(米)") + parser.add_argument("--arc-track-radius", type=float, default=1.6, 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-initial-x", type=float, default=0.0, help="车辆初始 X 坐标") + parser.add_argument("--vehicle-initial-y", type=float, default=0.0, help="车辆初始 Y 坐标") + parser.add_argument("--vehicle-initial-z", type=float, default=0.0, help="车辆初始 Z 坐标") + parser.add_argument( + "--vehicle-source", + choices=["urdf"], + default="urdf", + help="车辆来源;当前固定为 URDF 路径直接导入", + ) + parser.add_argument("--max-steer-angle-deg", type=float, default=32.0, help="阿克曼前轮最大转角(度)") + parser.add_argument("--max-speed-ms", type=float, default=1.2, help="阿克曼小车最大车速(m/s)") + parser.add_argument("--urdf-path", type=str, default="", help="车辆 URDF 路径;默认使用仓库内置阿克曼小车 URDF") + parser.add_argument("--usd-path", type=str, default="", help=argparse.SUPPRESS) + parser.add_argument("--disable-urdf-visual-origin-fix", action="store_true", help=argparse.SUPPRESS) + parser.add_argument("--force-rebuild-usd", action="store_true", help=argparse.SUPPRESS) + parser.add_argument("--scene-manifest-path", type=str, default="", help="输出车间场景 manifest JSON 的路径") + args, _ = parser.parse_known_args() + return args + + +ARGS = parse_args() +SCRIPT_PATH = Path(__file__).resolve() + + +def find_project_root(script_path): + for parent in [script_path.parent, *script_path.parents]: + if (parent / "src" / "simulation" / "isaac_workshop_sim").is_dir(): + return parent + raise RuntimeError(f"无法从脚本路径定位项目根目录: {script_path}") + + +PROJECT_ROOT = find_project_root(SCRIPT_PATH) +ISAAC_SIM_ROOT = SCRIPT_PATH.parents[1] +WORKSPACE_ROOT = PROJECT_ROOT.parent +MODELS_ROOT = WORKSPACE_ROOT / "models" +ASSETS_ROOT = ISAAC_SIM_ROOT / "assets" +DEFAULT_ACKERMANN_URDF_PATH = ASSETS_ROOT / "urdf" / "ackermann_front_steer_rear_drive.urdf" +GENERATED_ROOT = PROJECT_ROOT / ".isaac_cache" +GENERATED_ROOT.mkdir(parents=True, exist_ok=True) + + +def validate_args(args): + numeric_positive_fields = { + "room_length": args.room_length, + "room_width": args.room_width, + "room_height": args.room_height, + "wall_thickness": args.wall_thickness, + "checkerboard_square_size": args.checkerboard_square_size, + "camera_focal_length": args.camera_focal_length, + } + for name, value in numeric_positive_fields.items(): + if value <= 0: + raise ValueError(f"{name} 必须大于 0,当前值={value}") + + if args.checkerboard_rows <= 0 or args.checkerboard_cols <= 0: + raise ValueError("checkerboard_rows 和 checkerboard_cols 必须大于 0。") + if args.camera_resolution_width <= 0 or args.camera_resolution_height <= 0: + raise ValueError("camera resolution 必须大于 0。") + if args.camera_frequency <= 0: + raise ValueError("camera_frequency 必须大于 0。") + if args.external_publish_hz < 0 or args.chassis_telemetry_hz < 0 or args.control_telemetry_hz < 0 or args.sensor_telemetry_hz < 0: + raise ValueError("发布频率不能为负数。") + if not 0.0 <= args.external_pose_drop_rate <= 1.0: + raise ValueError("external_pose_drop_rate 必须在 [0,1] 范围内。") + if not 0.0 <= args.external_quality_score <= 1.0: + raise ValueError("external_quality_score 必须在 [0,1] 范围内。") + if not 0.0 <= args.sensor_quality_score <= 1.0: + raise ValueError("sensor_quality_score 必须在 [0,1] 范围内。") + if args.external_observed_target_count < 0 or args.sensor_target_sample_count < 0: + raise ValueError("目标数量/样本数不能为负数。") + if args.wheel_radius <= 0 or args.wheel_track <= 0 or args.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: + 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: + raise ValueError("2D LiDAR 靶标尺寸必须大于 0。") + if args.lidar_2d_target_bottom_z < 0: + raise ValueError("2D LiDAR 靶标底部高度不能为负数。") + if not 5.0 <= args.lidar_2d_target_slope_angle_deg <= 85.0: + raise ValueError("2D LiDAR 斜板角度应在 [5,85] 度范围内。") + if args.down_camera_target_length <= 0 or args.down_camera_target_width <= 0: + raise ValueError("下视相机内参标定台长度和宽度必须大于 0。") + if args.down_camera_target_max_height <= 0: + raise ValueError("下视相机内参标定台最高点必须大于 0。") + if args.down_camera_target_max_height <= 0.012: + raise ValueError("下视相机内参标定台最高点必须高于低平面 0.012m。") + if args.down_camera_target_max_height >= 0.08: + raise ValueError("下视相机内参标定台最高点应小于默认底盘最小离地间隙 0.08m。") + if args.down_camera_target_squares_x < 4 or args.down_camera_target_squares_y < 4: + raise ValueError("下视相机 ChArUco 纹理格数至少为 4x4。") + down_target_square_x = args.down_camera_target_length / args.down_camera_target_squares_x + down_target_square_y = args.down_camera_target_width / args.down_camera_target_squares_y + square_aspect_error = abs(down_target_square_x - down_target_square_y) / max(down_target_square_x, down_target_square_y) + if square_aspect_error > 0.05: + raise ValueError("下视相机 ChArUco 物理单格应接近正方形,请同步调整 target 尺寸和 squares 数。") + target_half_x = args.down_camera_target_length / 2.0 + target_half_y = args.down_camera_target_width / 2.0 + if abs(args.down_camera_target_x) + target_half_x > args.room_length / 2.0: + raise ValueError("下视相机内参标定台 X 方向超出车间范围。") + if abs(args.down_camera_target_y) + target_half_y > args.room_width / 2.0: + raise ValueError("下视相机内参标定台 Y 方向超出车间范围。") + + if args.max_steer_angle_deg <= 0 or args.max_speed_ms <= 0: + raise ValueError("max_steer_angle_deg 和 max_speed_ms 必须大于 0。") + + urdf_path = Path(args.urdf_path).expanduser().resolve() if args.urdf_path else DEFAULT_ACKERMANN_URDF_PATH + if not urdf_path.exists(): + raise FileNotFoundError(f"未找到车辆 URDF: {urdf_path}") + + +validate_args(ARGS) +os.environ["OMNI_KIT_ACCEPT_EULA"] = "YES" + +from isaacsim import SimulationApp + +simulation_app = SimulationApp({"headless": ARGS.headless}) + +from omni.isaac.core.utils.extensions import enable_extension + +enable_extension("omni.isaac.ros2_bridge") +enable_extension("omni.isaac.sensor") +simulation_app.update() + +import numpy as np +from PIL import Image +import omni.graph.core as og +import omni.kit.commands +import omni.usd +import omni.replicator.core as rep +from pxr import Gf, PhysxSchema, Sdf, UsdGeom, UsdShade, Vt + +from omni.isaac.core import World +from omni.isaac.core.objects import FixedCuboid +from omni.isaac.core.robots import Robot +from omni.isaac.core.utils.prims import create_prim +from omni.isaac.core.utils.rotations import euler_angles_to_quat +from omni.isaac.core.utils.viewports import set_camera_view + +try: + import rclpy +except ImportError: + rclpy = None + +try: + from calibration_external_localization_interfaces.msg import ExternalLocalizationTelemetry +except ImportError: + ExternalLocalizationTelemetry = None + +try: + from calibration_sensor_interfaces.msg import CaptureIntentType, SensorCalibrationTelemetry +except ImportError: + CaptureIntentType = None + SensorCalibrationTelemetry = None + +try: + from calibration_vehicle_profile_interfaces.msg import ( + ChassisType, + ControllerAlgorithmType, + SensorType, + ) +except ImportError: + ChassisType = None + ControllerAlgorithmType = None + SensorType = None + +try: + from calibration_chassis_interfaces.msg import ChassisTelemetry, WheelModuleState +except ImportError: + ChassisTelemetry = None + WheelModuleState = None + +try: + from calibration_control_interfaces.msg import ControlTelemetry +except ImportError: + ControlTelemetry = None + +try: + from sensor_msgs.msg import Imu, LaserScan +except ImportError: + Imu = None + LaserScan = None + + +def clamp(value, low, high): + return max(low, min(high, value)) + + +def resolve_topic(prefix, suffix): + normalized_prefix = prefix.rstrip("/") + normalized_suffix = suffix if suffix.startswith("/") else f"/{suffix}" + return f"{normalized_prefix}{normalized_suffix}" if normalized_prefix else normalized_suffix + + +def quat_wxyz_to_yaw(quat_wxyz): + w, x, y, z = quat_wxyz + siny_cosp = 2.0 * (w * z + x * y) + cosy_cosp = 1.0 - 2.0 * (y * y + z * z) + return math.atan2(siny_cosp, cosy_cosp) + + +def normalize_angle(angle): + return math.atan2(math.sin(angle), math.cos(angle)) + + +def chassis_type_value(name): + if ChassisType is None: + return 0 + mapping = { + "ackermann": ChassisType.ACKERMANN, + "differential": ChassisType.DIFFERENTIAL, + "single_steer": ChassisType.SINGLE_STEER_WHEEL, + "multi_steer": ChassisType.MULTI_STEER_WHEEL, + } + return mapping.get(name, ChassisType.CHASSIS_TYPE_UNSPECIFIED) + + +def controller_algorithm_value(name): + if ControllerAlgorithmType is None: + return 0 + mapping = { + "pid": ControllerAlgorithmType.PID, + "mpc": ControllerAlgorithmType.MPC, + "lqr": ControllerAlgorithmType.LQR, + "pure_pursuit": ControllerAlgorithmType.PURE_PURSUIT, + } + return mapping.get(name, ControllerAlgorithmType.CONTROLLER_ALGORITHM_UNSPECIFIED) + + +def vehicle_state_from_isaac(agv): + position, quat = agv.get_world_pose() + yaw_rad = quat_wxyz_to_yaw(quat) + linear_velocity = agv.get_linear_velocity() + try: + angular_velocity = agv.get_angular_velocity() + except Exception: + angular_velocity = np.array([0.0, 0.0, 0.0]) + return position, yaw_rad, linear_velocity, angular_velocity + + +def make_mesh_path_absolute(mesh_filename, source_dir): + mesh_path = Path(mesh_filename) + if mesh_path.is_absolute(): + return str(mesh_path) + return str((source_dir / mesh_path).resolve()) + + +def prepare_isaac_urdf(source_urdf_path, output_urdf_path): + tree = ET.parse(source_urdf_path) + root = tree.getroot() + source_dir = source_urdf_path.parent + + local_mesh_links = { + "left_steering_hinge", + "right_steering_hinge", + "left_wheel", + "right_wheel", + "left_rear_wheel", + "right_rear_wheel", + "camera", + "laser", + } + + for link in root.findall("link"): + link_name = link.get("name", "") + for section_name in ("visual", "collision"): + section = link.find(section_name) + if section is None: + continue + mesh = section.find("geometry/mesh") + if mesh is not None and mesh.get("filename"): + mesh.set("filename", make_mesh_path_absolute(mesh.get("filename"), source_dir)) + if link_name in local_mesh_links: + origin = section.find("origin") + if origin is None: + origin = ET.SubElement(section, "origin") + origin.set("xyz", "0 0 0") + origin.set("rpy", "0 0 0") + + output_urdf_path.parent.mkdir(parents=True, exist_ok=True) + tree.write(output_urdf_path, encoding="utf-8", xml_declaration=True) + return output_urdf_path + + +def create_checkerboard_image(filepath, rows=6, cols=9, square_size_px=500): + width = cols * square_size_px + height = rows * square_size_px + img = np.ones((height, width, 3), dtype=np.uint8) * 255 + + for r in range(rows): + for c in range(cols): + if (r + c) % 2 == 1: + img[r * square_size_px:(r + 1) * square_size_px, c * square_size_px:(c + 1) * square_size_px] = 0 + + border = square_size_px + img_with_border = np.pad( + img, + pad_width=((border, border), (border, border), (0, 0)), + mode="constant", + constant_values=255, + ) + + abs_filepath = Path(filepath).resolve() + abs_filepath.parent.mkdir(parents=True, exist_ok=True) + Image.fromarray(img_with_border).save(abs_filepath) + usd_filepath = str(abs_filepath).replace("\\", "/") + print(f"[*] 棋盘格纹理已生成: {usd_filepath}") + return usd_filepath + + +def create_charuco_image(filepath, squares_x=30, squares_y=10, square_size_px=90): + width = squares_x * square_size_px + height = squares_y * square_size_px + abs_filepath = Path(filepath).resolve() + abs_filepath.parent.mkdir(parents=True, exist_ok=True) + + try: + import cv2 + dictionary = cv2.aruco.getPredefinedDictionary(cv2.aruco.DICT_4X4_250) + try: + board = cv2.aruco.CharucoBoard((squares_x, squares_y), 1.0, 0.70, dictionary) + except TypeError: + board = cv2.aruco.CharucoBoard_create(squares_x, squares_y, 1.0, 0.70, dictionary) + if hasattr(board, "generateImage"): + img = board.generateImage((width, height), marginSize=0) + else: + img = board.draw((width, height), marginSize=0) + if len(img.shape) == 2: + img = np.repeat(img[:, :, None], 3, axis=2) + Image.fromarray(img).save(abs_filepath) + usd_filepath = str(abs_filepath).replace("\\", "/") + print(f"[*] ChArUco 纹理已生成: {usd_filepath}") + return usd_filepath + except Exception as exc: + print(f"[WARN] OpenCV ChArUco 生成失败,使用内置 ChArUco 风格纹理: {exc}") + + img = np.ones((height, width, 3), dtype=np.uint8) * 255 + marker_cells = 6 + marker_margin = max(3, square_size_px // 7) + marker_size = square_size_px - 2 * marker_margin + marker_cell_px = max(1, marker_size // marker_cells) + marker_px = marker_cell_px * marker_cells + + def marker_bits(marker_id): + marker = np.ones((marker_cells, marker_cells), dtype=np.uint8) * 255 + marker[0, :] = 0 + marker[-1, :] = 0 + marker[:, 0] = 0 + marker[:, -1] = 0 + state = (marker_id + 1) * 1103515245 + 12345 + for r in range(1, marker_cells - 1): + for c in range(1, marker_cells - 1): + state = (state * 1664525 + 1013904223 + r * 97 + c * 193) & 0xFFFFFFFF + marker[r, c] = 0 if (state & 1) else 255 + return marker + + marker_id = 0 + for r in range(squares_y): + for c in range(squares_x): + y0 = r * square_size_px + x0 = c * square_size_px + if (r + c) % 2 == 1: + img[y0:y0 + square_size_px, x0:x0 + square_size_px] = 0 + continue + marker = marker_bits(marker_id) + marker_img = np.kron(marker, np.ones((marker_cell_px, marker_cell_px), dtype=np.uint8)) + marker_img = marker_img[:marker_px, :marker_px] + marker_rgb = np.repeat(marker_img[:, :, None], 3, axis=2) + marker_y = y0 + (square_size_px - marker_px) // 2 + marker_x = x0 + (square_size_px - marker_px) // 2 + img[marker_y:marker_y + marker_px, marker_x:marker_x + marker_px] = marker_rgb + marker_id += 1 + + Image.fromarray(img).save(abs_filepath) + usd_filepath = str(abs_filepath).replace("\\", "/") + print(f"[*] ChArUco 风格纹理已生成: {usd_filepath}") + return usd_filepath + + +def add_corner_rotary_lidars(room_length, room_width, height, lidar_config, topic_prefix): + offset = 0.3 + 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 + graph_path = "/World/ROS2_Lidar_Graph" + nodes = [ + ("OnTick", "omni.graph.action.OnTick"), + ("ReadSimTime", "omni.isaac.core_nodes.IsaacReadSimulationTime"), + ("PublishTF", "omni.isaac.ros2_bridge.ROS2PublishTransformTree"), + ] + connections = [ + ("OnTick.outputs:tick", "PublishTF.inputs:execIn"), + ("ReadSimTime.outputs:simulationTime", "PublishTF.inputs:timeStamp"), + ] + set_values = [] + lidar_paths = [] + + for cfg in lidar_configs: + lidar_path = f"/World/Sensors/Lidar_{cfg['name']}" + lidar_paths.append(lidar_path) + quat = euler_angles_to_quat(np.array([0, 15.0, cfg["yaw"]]), degrees=True) + orientation = Gf.Quatd(quat[0], quat[1], quat[2], quat[3]) + omni.kit.commands.execute( + "IsaacSensorCreateRtxLidar", + path=lidar_path, + parent=None, + config=lidar_config, + translation=Gf.Vec3d(*cfg["pos"]), + orientation=orientation, + ) + render_product = rep.create.render_product(lidar_path, [1, 1]) + helper_name = f"ROS2LidarHelper_{cfg['name']}" + nodes.append((helper_name, "omni.isaac.ros2_bridge.ROS2RtxLidarHelper")) + connections.append(("OnTick.outputs:tick", f"{helper_name}.inputs:execIn")) + set_values.extend([ + (f"{helper_name}.inputs:renderProductPath", str(render_product.path)), + (f"{helper_name}.inputs:topicName", f"{topic_prefix}/{cfg['name'].lower()}/pointcloud"), + (f"{helper_name}.inputs:frameId", f"Lidar_{cfg['name']}"), + (f"{helper_name}.inputs:type", "point_cloud"), + (f"{helper_name}.inputs:fullScan", True), + ]) + + set_values.append(("PublishTF.inputs:targetPrims", lidar_paths)) + og.Controller.edit( + {"graph_path": graph_path, "evaluator_name": "execution"}, + {keys.CREATE_NODES: nodes, keys.CONNECT: connections, keys.SET_VALUES: set_values}, + ) + + +def create_raw_usd_material(stage, mat_path, tex_path): + material = UsdShade.Material.Define(stage, mat_path) + pbr_shader = UsdShade.Shader.Define(stage, f"{mat_path}/PBRShader") + pbr_shader.CreateIdAttr("UsdPreviewSurface") + pbr_shader.CreateInput("roughness", Sdf.ValueTypeNames.Float).Set(1.0) + pbr_shader.CreateInput("metallic", Sdf.ValueTypeNames.Float).Set(0.0) + + tex_sampler = UsdShade.Shader.Define(stage, f"{mat_path}/diffuseTexture") + tex_sampler.CreateIdAttr("UsdUVTexture") + tex_sampler.CreateInput("file", Sdf.ValueTypeNames.Asset).Set(Sdf.AssetPath(tex_path)) + tex_sampler.CreateInput("magFilter", Sdf.ValueTypeNames.Token).Set("nearest") + tex_sampler.CreateInput("minFilter", Sdf.ValueTypeNames.Token).Set("nearest") + + st_reader = UsdShade.Shader.Define(stage, f"{mat_path}/stReader") + st_reader.CreateIdAttr("UsdPrimvarReader_float2") + st_reader.CreateInput("varname", Sdf.ValueTypeNames.Token).Set("st") + + tex_sampler.CreateInput("st", Sdf.ValueTypeNames.Float2).ConnectToSource(st_reader.ConnectableAPI(), "result") + pbr_shader.CreateInput("diffuseColor", Sdf.ValueTypeNames.Color3f).ConnectToSource(tex_sampler.ConnectableAPI(), "rgb") + material.CreateSurfaceOutput().ConnectToSource(pbr_shader.ConnectableAPI(), "surface") + return material + + +def create_textured_board(stage, prim_path, width, height, center, euler_rot_deg, usd_material): + mesh = UsdGeom.Mesh.Define(stage, prim_path) + half_width, half_height = width / 2.0, height / 2.0 + mesh.GetPointsAttr().Set(Vt.Vec3fArray([ + Gf.Vec3f(-half_width, -half_height, 0), + Gf.Vec3f(half_width, -half_height, 0), + Gf.Vec3f(half_width, half_height, 0), + Gf.Vec3f(-half_width, half_height, 0), + ])) + mesh.GetFaceVertexCountsAttr().Set([4]) + mesh.GetFaceVertexIndicesAttr().Set([0, 1, 2, 3]) + mesh.GetNormalsAttr().Set([Gf.Vec3f(0, 0, 1)] * 4) + mesh.SetNormalsInterpolation(UsdGeom.Tokens.vertex) + + primvars_api = UsdGeom.PrimvarsAPI(mesh) + st_primvar = primvars_api.CreatePrimvar("st", Sdf.ValueTypeNames.TexCoord2fArray, UsdGeom.Tokens.vertex) + st_primvar.Set([Gf.Vec2f(0, 0), Gf.Vec2f(1, 0), Gf.Vec2f(1, 1), Gf.Vec2f(0, 1)]) + mesh.GetExtentAttr().Set([Gf.Vec3f(-half_width, -half_height, -0.01), Gf.Vec3f(half_width, half_height, 0.01)]) + + xform = UsdGeom.Xformable(mesh) + xform.AddTranslateOp().Set(Gf.Vec3d(*center)) + xform.AddRotateXYZOp().Set(Gf.Vec3f(*euler_rot_deg)) + UsdShade.MaterialBindingAPI.Apply(mesh.GetPrim()).Bind(usd_material) + return mesh + + +def create_textured_top_strip(stage, prim_path, x_edges, y_min, y_max, z_values, usd_material): + mesh = UsdGeom.Mesh.Define(stage, prim_path) + x0 = x_edges[0] + x1 = x_edges[-1] + total_length = max(x1 - x0, 1e-6) + + points = [] + st_values = [] + for x, z in zip(x_edges, z_values): + u = (x - x0) / total_length + points.append(Gf.Vec3f(x, y_min, z)) + points.append(Gf.Vec3f(x, y_max, z)) + st_values.append(Gf.Vec2f(u, 0.0)) + st_values.append(Gf.Vec2f(u, 1.0)) + + face_vertex_counts = [] + face_vertex_indices = [] + for index in range(len(x_edges) - 1): + face_vertex_counts.append(4) + face_vertex_indices.extend([2 * index, 2 * (index + 1), 2 * (index + 1) + 1, 2 * index + 1]) + + mesh.GetPointsAttr().Set(Vt.Vec3fArray(points)) + mesh.GetFaceVertexCountsAttr().Set(face_vertex_counts) + mesh.GetFaceVertexIndicesAttr().Set(face_vertex_indices) + mesh.GetExtentAttr().Set([ + Gf.Vec3f(min(x_edges), y_min, min(z_values) - 0.005), + Gf.Vec3f(max(x_edges), y_max, max(z_values) + 0.005), + ]) + + primvars_api = UsdGeom.PrimvarsAPI(mesh) + st_primvar = primvars_api.CreatePrimvar("st", Sdf.ValueTypeNames.TexCoord2fArray, UsdGeom.Tokens.vertex) + st_primvar.Set(st_values) + UsdShade.MaterialBindingAPI.Apply(mesh.GetPrim()).Bind(usd_material) + return mesh + + +def add_down_camera_intrinsic_target(world, stage, args, material): + length = args.down_camera_target_length + width = args.down_camera_target_width + center_x = args.down_camera_target_x + center_y = args.down_camera_target_y + z_low = 0.012 + z_high = args.down_camera_target_max_height + x_start = center_x - length / 2.0 + x_low_end = x_start + length * 0.22 + x_ramp_end = x_start + length * 0.74 + x_end = center_x + length / 2.0 + y_min = center_y - width / 2.0 + y_max = center_y + width / 2.0 + base_thickness = 0.006 + + world.scene.add(FixedCuboid( + prim_path="/World/Workshop/DownCameraIntrinsicTarget/Base", + name="down_camera_intrinsic_target_base", + position=np.array([center_x, center_y, base_thickness / 2.0]), + scale=np.array([length + 0.08, width + 0.08, base_thickness]), + color=np.array([0.045, 0.050, 0.052]), + )) + + create_textured_top_strip( + stage, + "/World/Workshop/DownCameraIntrinsicTarget/CharucoRampSurface", + [x_start, x_low_end, x_ramp_end, x_end], + y_min, + y_max, + [z_low, z_low, z_high, z_high], + material, + ) + + specs = [{ + "id": "down_camera_3d_charuco_ramp", + "target_type": "down_camera_intrinsic", + "pattern": "charuco", + "purpose": "intrinsic_calibration_with_depth_and_pose_gradient", + "center": [center_x, center_y, (z_low + z_high) / 2.0], + "length_m": length, + "width_m": width, + "squares_x": args.down_camera_target_squares_x, + "squares_y": args.down_camera_target_squares_y, + "square_size_m": width / args.down_camera_target_squares_y, + "min_height_m": z_low, + "max_height_m": z_high, + "recommended_drive_axis": "+x", + "recommended_speed_mps": 0.10, + "surfaces": [ + { + "id": "low_flat_charuco", + "type": "flat", + "x_range_m": [x_start, x_low_end], + "z_range_m": [z_low, z_low], + }, + { + "id": "continuous_slope_charuco", + "type": "continuous_slope", + "x_range_m": [x_low_end, x_ramp_end], + "z_range_m": [z_low, z_high], + }, + { + "id": "high_flat_charuco", + "type": "flat", + "x_range_m": [x_ramp_end, x_end], + "z_range_m": [z_high, z_high], + }, + ], + }] + return specs + + +def lidar_2d_target_wall_slots(args): + if args.disable_2d_lidar_targets: + return {} + + width = args.lidar_2d_target_width + height = args.lidar_2d_target_height + gap = 0.12 + edge_margin = 0.18 + bay_margin = 0.07 + bay_width = max(1.90, 2.0 * width + gap + 2.0 * bay_margin) + bay_z_min = 0.0 + bay_z_max = args.room_height + center_spacing = width + gap + + def group_offsets(span, side): + usable_min = -span / 2.0 + edge_margin + usable_max = span / 2.0 - edge_margin + usable_width = max(0.0, usable_max - usable_min) + effective_bay_width = min(bay_width, usable_width) if usable_width > 0.0 else bay_width + if usable_width <= effective_bay_width: + axis_min = usable_min + axis_max = usable_max + elif side == "max": + axis_max = usable_max + axis_min = axis_max - effective_bay_width + else: + axis_min = usable_min + axis_max = axis_min + effective_bay_width + group_center = (axis_min + axis_max) / 2.0 + vertical_offset = group_center - center_spacing / 2.0 + slope_offset = group_center + center_spacing / 2.0 + return vertical_offset, slope_offset, axis_min, axis_max + + front_vertical, front_slope, front_min, front_max = group_offsets(args.room_width, "min") + _, _, right_min, right_max = group_offsets(args.room_length, "max") + right_vertical = (right_min + right_max) / 2.0 + + slots = { + "front": { + "vertical_offset": front_vertical, + "slope_offset": front_slope, + "reserved_axis_min": front_min, + "reserved_axis_max": front_max, + "reserved_z_min": bay_z_min, + "reserved_z_max": bay_z_max, + }, + "right": { + "vertical_offset": right_vertical, + "reserved_axis_min": right_min, + "reserved_axis_max": right_max, + "reserved_z_min": bay_z_min, + "reserved_z_max": bay_z_max, + }, + } + return slots + + +def lidar_2d_checkerboard_reserved_zones(args): + slots = lidar_2d_target_wall_slots(args) + reserved_zones = {} + for wall, slot in slots.items(): + reserved_zones[wall] = [{ + "axis_min": slot["reserved_axis_min"], + "axis_max": slot["reserved_axis_max"], + "z_min": slot["reserved_z_min"], + "z_max": slot["reserved_z_max"], + "reason": "2d_lidar_extrinsic_corner_bay", + }] + return reserved_zones + + +def calibration_board_layout(args, board_width, board_height): + wall_standoff = 0.015 + front_x = args.room_length / 2.0 - wall_standoff + back_x = -args.room_length / 2.0 + wall_standoff + left_y = args.room_width / 2.0 - wall_standoff + right_y = -args.room_width / 2.0 + wall_standoff + horizontal_gap = 0.18 + vertical_gap = 0.16 + side_margin = 0.35 + bottom_margin = 0.32 + top_margin = 0.28 + + def axis_positions(span, item_size, margin, gap): + available = span - 2.0 * margin + count = max(1, int((available + gap) // (item_size + gap))) + if count == 1: + return [0.0] + used = count * item_size + (count - 1) * gap + start = -used / 2.0 + item_size / 2.0 + return [start + index * (item_size + gap) for index in range(count)] + + def z_positions(): + available = args.room_height - bottom_margin - top_margin + count = max(1, int((available + vertical_gap) // (board_height + vertical_gap))) + if count == 1: + return [bottom_margin + board_height / 2.0] + used = count * board_height + (count - 1) * vertical_gap + start = bottom_margin + board_height / 2.0 + max(0.0, available - used) / 2.0 + return [start + index * (board_height + vertical_gap) for index in range(count)] + + zs = z_positions() + front_back_offsets = axis_positions(args.room_width, board_width, side_margin, horizontal_gap) + side_offsets = axis_positions(args.room_length, board_width, side_margin, horizontal_gap) + reserved_zones = lidar_2d_checkerboard_reserved_zones(args) + + board_specs = [] + + def overlaps_reserved_zone(wall, offset, z): + board_axis_min = offset - board_width / 2.0 + board_axis_max = offset + board_width / 2.0 + board_z_min = z - board_height / 2.0 + board_z_max = z + board_height / 2.0 + for zone in reserved_zones.get(wall, []): + axis_overlaps = board_axis_min < zone["axis_max"] and board_axis_max > zone["axis_min"] + z_overlaps = board_z_min < zone["z_max"] and board_z_max > zone["z_min"] + if axis_overlaps and z_overlaps: + return True + return False + + def add_wall_grid(wall, fixed_value, offsets, rotation_deg): + for row, z in enumerate(zs): + for col, offset in enumerate(offsets): + if overlaps_reserved_zone(wall, offset, z): + continue + if wall == "front": + position = [fixed_value, offset, z] + elif wall == "back": + position = [fixed_value, offset, z] + elif wall == "left": + position = [offset, fixed_value, z] + else: + position = [offset, fixed_value, z] + + board_specs.append({ + "id": f"{wall}_wall_r{row:02d}_c{col:02d}", + "wall": wall, + "mount": "flush", + "purpose": "wall_checkerboard_array", + "position": position, + "rotation_deg": rotation_deg, + }) + + add_wall_grid("front", front_x, front_back_offsets, [90, 0, 90]) + add_wall_grid("back", back_x, front_back_offsets, [90, 0, -90]) + add_wall_grid("left", left_y, side_offsets, [90, 0, 0]) + add_wall_grid("right", right_y, side_offsets, [90, 0, 180]) + + return board_specs + + +def add_calibration_boards(stage, args, board_width, board_height, material): + board_specs = calibration_board_layout(args, board_width, board_height) + for spec in board_specs: + create_textured_board( + stage, + f"/World/Workshop/CalibrationBoards/{spec['id']}", + board_width, + board_height, + spec["position"], + spec["rotation_deg"], + material, + ) + return board_specs + + +def add_lidar_2d_panel(world, prim_path, name, position, scale, color, rotation_deg=None): + orientation = None + if rotation_deg is not None: + orientation = np.array(euler_angles_to_quat(np.array(rotation_deg), degrees=True)) + world.scene.add(FixedCuboid( + prim_path=prim_path, + name=name, + position=np.array(position), + orientation=orientation, + scale=np.array(scale), + color=np.array(color), + )) + + +def add_2d_lidar_calibration_targets(world, args): + thickness = args.lidar_2d_target_thickness + width = args.lidar_2d_target_width + height = args.lidar_2d_target_height + bottom_z = args.lidar_2d_target_bottom_z + center_z = bottom_z + height / 2.0 + angle_deg = args.lidar_2d_target_slope_angle_deg + angle_rad = math.radians(angle_deg) + sloped_length = height / max(math.cos(angle_rad), 1e-3) + standoff = 0.06 + slots = lidar_2d_target_wall_slots(args) + + front_wall_x = args.room_length / 2.0 + right_wall_y = -args.room_width / 2.0 + + front_base_x = front_wall_x - standoff - thickness / 2.0 + side_right_base_y = right_wall_y + standoff + thickness / 2.0 + + front_slope_center_x = front_wall_x - standoff - height / 2.0 - thickness + + front_vertical_y = slots["front"]["vertical_offset"] + front_slope_y = slots["front"]["slope_offset"] + right_vertical_x = slots["right"]["vertical_offset"] + + vertical_color = [0.92, 0.82, 0.18] + slope_color = [0.12, 0.65, 0.95] + specs = [] + + def add_spec(spec): + specs.append(spec) + return spec + + front_vertical = add_spec({ + "id": "front_vertical_reference_panel", + "wall": "front", + "type": "vertical_reference", + "position": [front_base_x, front_vertical_y, center_z], + "scale": [thickness, width, height], + "rotation_deg": [0.0, 0.0, 0.0], + "nominal_plane": "x = room_length/2 - standoff - thickness", + }) + add_lidar_2d_panel( + world, + "/World/Workshop/Lidar2DCalibrationTargets/FrontVerticalReference", + "front_vertical_reference_panel", + front_vertical["position"], + front_vertical["scale"], + vertical_color, + ) + + front_slope = add_spec({ + "id": "front_45deg_height_encoding_panel", + "wall": "front", + "type": "height_encoding_slope", + "position": [front_slope_center_x, front_slope_y, center_z], + "scale": [thickness, width, sloped_length], + "rotation_deg": [0.0, -angle_deg, 0.0], + "slope_angle_deg": angle_deg, + "height_to_range_sign": "higher_scan_plane_farther_from_front_wall", + }) + add_lidar_2d_panel( + world, + "/World/Workshop/Lidar2DCalibrationTargets/FrontSlope45", + "front_45deg_height_encoding_panel", + front_slope["position"], + front_slope["scale"], + slope_color, + front_slope["rotation_deg"], + ) + + right_vertical = add_spec({ + "id": "right_vertical_reference_panel", + "wall": "right", + "type": "vertical_reference", + "position": [right_vertical_x, side_right_base_y, center_z], + "scale": [width, thickness, height], + "rotation_deg": [0.0, 0.0, 0.0], + "nominal_plane": "y = -room_width/2 + standoff + thickness", + }) + add_lidar_2d_panel( + world, + "/World/Workshop/Lidar2DCalibrationTargets/RightVerticalReference", + "right_vertical_reference_panel", + right_vertical["position"], + right_vertical["scale"], + vertical_color, + ) + + 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], + ) + + +class ExternalTruthTelemetryPublisher: + def __init__(self, args): + self.enabled = not args.disable_external_telemetry + self.topic = args.external_telemetry_topic + self.publish_period_sec = 0.0 if args.external_publish_hz <= 0.0 else 1.0 / args.external_publish_hz + self.reference_source_name = args.external_reference_source_name + self.workcell_zone_id = args.external_workcell_zone_id + self.position_noise_stddev_m = max(0.0, args.external_position_noise_stddev_m) + self.yaw_noise_stddev_rad = max(0.0, args.external_yaw_noise_stddev_rad) + self.position_stddev_m = max(0.0, args.external_position_stddev_m) + self.yaw_stddev_rad = max(0.0, args.external_yaw_stddev_rad) + self.tracking_loss_ratio = clamp(args.external_tracking_loss_ratio, 0.0, 1.0) + self.time_sync_offset_ms = abs(args.external_time_sync_offset_ms) + self.quality_score = clamp(args.external_quality_score, 0.0, 1.0) + self.observed_target_count = max(0, args.external_observed_target_count) + self.pose_drop_rate = clamp(args.external_pose_drop_rate, 0.0, 1.0) + self.last_publish_wall_time = 0.0 + self.rng = np.random.default_rng(args.random_seed) + self.node = None + self.publisher = None + + if not self.enabled: + print("[*] 已禁用 external 仿真遥测发布。") + return + + if rclpy is None or ExternalLocalizationTelemetry is None: + print("[WARN] 未找到 rclpy 或 calibration_external_localization_interfaces,external 遥测发布已禁用。") + self.enabled = False + return + + if not rclpy.ok(): + rclpy.init(args=None) + + self.node = rclpy.create_node("isaac_external_truth_publisher") + self.publisher = self.node.create_publisher(ExternalLocalizationTelemetry, self.topic, 10) + print(f"[*] external 仿真遥测发布已启用: topic={self.topic}, source={self.reference_source_name}, zone={self.workcell_zone_id}") + + def publish(self, agv): + if not self.enabled or self.publisher is None: + return + + now = time.time() + if self.publish_period_sec > 0.0 and (now - self.last_publish_wall_time) < self.publish_period_sec: + return + + position, quat = agv.get_world_pose() + yaw_rad = quat_wxyz_to_yaw(quat) + pose_valid = self.rng.random() >= self.pose_drop_rate + + msg = ExternalLocalizationTelemetry() + msg.hardware_timestamp_us = time.time_ns() // 1000 + msg.pose_valid = pose_valid + msg.workshop_pose.x_m = float(position[0] + self.rng.normal(0.0, self.position_noise_stddev_m)) + msg.workshop_pose.y_m = float(position[1] + self.rng.normal(0.0, self.position_noise_stddev_m)) + msg.workshop_pose.z_m = float(position[2]) + msg.workshop_pose.roll_rad = 0.0 + msg.workshop_pose.pitch_rad = 0.0 + msg.workshop_pose.yaw_rad = float(yaw_rad + self.rng.normal(0.0, self.yaw_noise_stddev_rad)) + msg.position_stddev_m = self.position_stddev_m + msg.yaw_stddev_rad = self.yaw_stddev_rad + msg.tracking_loss_ratio = self.tracking_loss_ratio if pose_valid else max(self.tracking_loss_ratio, 1.0) + msg.time_sync_offset_ms = self.time_sync_offset_ms + msg.quality_score = self.quality_score if pose_valid else 0.0 + msg.observed_target_count = self.observed_target_count if pose_valid else 0 + msg.reference_source_name = self.reference_source_name + msg.active_job_id = f"{self.workcell_zone_id}:isaac_external_truth" + + self.publisher.publish(msg) + self.last_publish_wall_time = now + + def shutdown(self): + if self.node is not None: + self.node.destroy_node() + self.node = None + + +class VehicleImuTopicPublisher: + def __init__(self, args): + self.enabled = not args.disable_vehicle_imu + self.topic = args.vehicle_imu_topic + self.publish_period_sec = 0.0 if args.chassis_telemetry_hz <= 0.0 else 1.0 / args.chassis_telemetry_hz + self.last_publish_wall_time = 0.0 + self.last_sample_wall_time = None + self.last_linear_velocity = None + self.node = None + self.publisher = None + + if not self.enabled: + print("[*] 已禁用车载 IMU 话题发布。") + return + + if rclpy is None or Imu is None: + print("[WARN] 未找到 rclpy 或 sensor_msgs,车载 IMU 话题发布已禁用。") + self.enabled = False + return + + if not rclpy.ok(): + rclpy.init(args=None) + + self.node = rclpy.create_node("isaac_vehicle_imu_topic_publisher") + self.publisher = self.node.create_publisher(Imu, self.topic, 10) + print(f"[*] 车载 IMU 话题发布已启用: topic={self.topic}") + + @staticmethod + def _stamp_now(msg): + now_ns = time.time_ns() + msg.header.stamp.sec = int(now_ns // 1_000_000_000) + msg.header.stamp.nanosec = int(now_ns % 1_000_000_000) + + def publish(self, agv): + if not self.enabled or self.publisher is None or agv is None: + return + + now = time.time() + if self.publish_period_sec > 0.0 and (now - self.last_publish_wall_time) < self.publish_period_sec: + return + + _position, quat = agv.get_world_pose() + yaw_rad = quat_wxyz_to_yaw(quat) + linear_velocity = np.array(agv.get_linear_velocity(), dtype=float) + try: + angular_velocity = np.array(agv.get_angular_velocity(), dtype=float) + except Exception: + angular_velocity = np.array([0.0, 0.0, 0.0], dtype=float) + + if self.last_linear_velocity is None or self.last_sample_wall_time is None: + world_accel = np.array([0.0, 0.0, 0.0], dtype=float) + else: + dt = max(now - self.last_sample_wall_time, 1e-3) + world_accel = (linear_velocity - self.last_linear_velocity) / dt + + cos_yaw = math.cos(yaw_rad) + sin_yaw = math.sin(yaw_rad) + body_accel_x = cos_yaw * world_accel[0] + sin_yaw * world_accel[1] + body_accel_y = -sin_yaw * world_accel[0] + cos_yaw * world_accel[1] + body_ang_x = cos_yaw * angular_velocity[0] + sin_yaw * angular_velocity[1] + body_ang_y = -sin_yaw * angular_velocity[0] + cos_yaw * angular_velocity[1] + + msg = Imu() + self._stamp_now(msg) + msg.header.frame_id = "imu_link" + msg.orientation.w = float(quat[0]) + msg.orientation.x = float(quat[1]) + msg.orientation.y = float(quat[2]) + msg.orientation.z = float(quat[3]) + msg.angular_velocity.x = float(body_ang_x) + msg.angular_velocity.y = float(body_ang_y) + msg.angular_velocity.z = float(angular_velocity[2]) + msg.linear_acceleration.x = float(body_accel_x) + msg.linear_acceleration.y = float(body_accel_y) + msg.linear_acceleration.z = float(world_accel[2] + 9.80665) + msg.orientation_covariance = [0.001, 0.0, 0.0, 0.0, 0.001, 0.0, 0.0, 0.0, 0.001] + msg.angular_velocity_covariance = [0.0025, 0.0, 0.0, 0.0, 0.0025, 0.0, 0.0, 0.0, 0.0025] + msg.linear_acceleration_covariance = [0.01, 0.0, 0.0, 0.0, 0.01, 0.0, 0.0, 0.0, 0.01] + + self.publisher.publish(msg) + self.last_publish_wall_time = now + self.last_sample_wall_time = now + self.last_linear_velocity = linear_velocity + + def shutdown(self): + if self.node is not None: + self.node.destroy_node() + self.node = None + + +class VehicleLaserScanTopicPublisher: + def __init__(self, args, target_specs): + self.enabled = not args.disable_vehicle_2d_lidar + self.args = args + self.target_specs = list(target_specs or []) + self.topic = args.vehicle_2d_lidar_topic + self.publish_period_sec = 0.0 if args.chassis_telemetry_hz <= 0.0 else 1.0 / args.chassis_telemetry_hz + self.angle_min = -math.pi + self.angle_max = math.pi + self.angle_increment = math.radians(1.0) + self.range_min = 0.05 + self.range_max = max(12.0, math.hypot(args.room_length, args.room_width)) + self.last_publish_wall_time = 0.0 + self.node = None + self.publisher = None + + if not self.enabled: + print("[*] 已禁用车载 2D LiDAR LaserScan 发布。") + return + + if rclpy is None or LaserScan is None: + print("[WARN] 未找到 rclpy 或 sensor_msgs,车载 2D LiDAR LaserScan 发布已禁用。") + self.enabled = False + return + + if not rclpy.ok(): + rclpy.init(args=None) + + self.node = rclpy.create_node("isaac_vehicle_lidar_2d_scan_publisher") + self.publisher = self.node.create_publisher(LaserScan, self.topic, 10) + print(f"[*] 车载 2D LiDAR LaserScan 发布已启用: topic={self.topic}") + + @staticmethod + def _stamp_now(msg): + now_ns = time.time_ns() + msg.header.stamp.sec = int(now_ns // 1_000_000_000) + msg.header.stamp.nanosec = int(now_ns % 1_000_000_000) + + @staticmethod + def _axis_hit(origin, direction, fixed_axis, fixed_value, range_axis, range_min, range_max): + denom = direction[fixed_axis] + if abs(denom) < 1e-9: + return None + t = (fixed_value - origin[fixed_axis]) / denom + if t <= 0.0: + return None + crossed = origin[range_axis] + t * direction[range_axis] + if range_min <= crossed <= range_max: + return t + return None + + def _target_segments_at_scan_height(self, scan_z): + segments = [] + for spec in self.target_specs: + position = spec.get("position", [0.0, 0.0, 0.0]) + scale = spec.get("scale", [0.0, 0.0, 0.0]) + if spec.get("wall") == "front": + x = float(position[0]) + if spec.get("type") == "height_encoding_slope": + angle_rad = math.radians(float(spec.get("slope_angle_deg", self.args.lidar_2d_target_slope_angle_deg))) + x -= (float(scan_z) - float(position[2])) * math.tan(angle_rad) + y_min = float(position[1]) - float(scale[1]) / 2.0 + y_max = float(position[1]) + float(scale[1]) / 2.0 + segments.append(("x", x, y_min, y_max)) + elif spec.get("wall") == "right": + y = float(position[1]) + x_min = float(position[0]) - float(scale[0]) / 2.0 + x_max = float(position[0]) + float(scale[0]) / 2.0 + segments.append(("y", y, x_min, x_max)) + return segments + + def _raycast(self, origin_xy, beam_yaw, scan_z): + direction = np.array([math.cos(beam_yaw), math.sin(beam_yaw)], dtype=float) + half_length = self.args.room_length / 2.0 + half_width = self.args.room_width / 2.0 + best_range = self.range_max + best_intensity = 5.0 + + wall_hits = [ + self._axis_hit(origin_xy, direction, 0, half_length, 1, -half_width, half_width), + self._axis_hit(origin_xy, direction, 0, -half_length, 1, -half_width, half_width), + self._axis_hit(origin_xy, direction, 1, half_width, 0, -half_length, half_length), + self._axis_hit(origin_xy, direction, 1, -half_width, 0, -half_length, half_length), + ] + for hit_range in wall_hits: + if hit_range is not None and self.range_min <= hit_range < best_range: + best_range = hit_range + best_intensity = 10.0 + + for axis, value, seg_min, seg_max in self._target_segments_at_scan_height(scan_z): + if axis == "x": + hit_range = self._axis_hit(origin_xy, direction, 0, value, 1, seg_min, seg_max) + else: + hit_range = self._axis_hit(origin_xy, direction, 1, value, 0, seg_min, seg_max) + if hit_range is not None and self.range_min <= hit_range < best_range: + best_range = hit_range + best_intensity = 80.0 + + return float(best_range), float(best_intensity) + + def publish(self, agv): + if not self.enabled or self.publisher is None or agv is None: + return + + now = time.time() + if self.publish_period_sec > 0.0 and (now - self.last_publish_wall_time) < self.publish_period_sec: + return + + position, quat = agv.get_world_pose() + yaw_rad = quat_wxyz_to_yaw(quat) + cos_yaw = math.cos(yaw_rad) + sin_yaw = math.sin(yaw_rad) + lidar_local_x = self.args.vehicle_2d_lidar_x + lidar_local_y = self.args.vehicle_2d_lidar_y + origin_xy = np.array([ + float(position[0]) + cos_yaw * lidar_local_x - sin_yaw * lidar_local_y, + float(position[1]) + sin_yaw * lidar_local_x + cos_yaw * lidar_local_y, + ]) + scan_z = float(position[2]) + self.args.vehicle_2d_lidar_z + + beam_count = int(round((self.angle_max - self.angle_min) / self.angle_increment)) + 1 + ranges = [] + intensities = [] + for index in range(beam_count): + angle = self.angle_min + index * self.angle_increment + hit_range, intensity = self._raycast(origin_xy, yaw_rad + angle, scan_z) + ranges.append(hit_range) + intensities.append(intensity) + + msg = LaserScan() + self._stamp_now(msg) + msg.header.frame_id = "lidar_2d_link" + msg.angle_min = float(self.angle_min) + msg.angle_max = float(self.angle_max) + msg.angle_increment = float(self.angle_increment) + msg.time_increment = 0.0 + msg.scan_time = float(self.publish_period_sec if self.publish_period_sec > 0.0 else 0.05) + msg.range_min = float(self.range_min) + msg.range_max = float(self.range_max) + msg.ranges = ranges + msg.intensities = intensities + + self.publisher.publish(msg) + self.last_publish_wall_time = now + + def shutdown(self): + if self.node is not None: + self.node.destroy_node() + self.node = None + + +class ChassisCalibrationTelemetryPublisher: + def __init__(self, args): + self.enabled = not args.disable_chassis_telemetry + self.topic = args.chassis_telemetry_topic + self.publish_period_sec = 0.0 if args.chassis_telemetry_hz <= 0.0 else 1.0 / args.chassis_telemetry_hz + self.chassis_type = args.chassis_type + self.wheel_radius = args.wheel_radius + self.wheel_track = args.wheel_track + self.wheel_base = args.wheel_base + self.odom_noise_stddev_m = args.odom_noise_stddev_m + self.yaw_noise_stddev_rad = args.yaw_noise_stddev_rad + self.rng = np.random.default_rng(args.random_seed + 17) + self.last_publish_wall_time = 0.0 + self.encoder_ticks = {} + self.node = None + self.publisher = None + + if not self.enabled: + print("[*] 已禁用底盘标定遥测发布。") + return + + if rclpy is None or ChassisTelemetry is None or WheelModuleState is None or ChassisType is None: + print("[WARN] 未找到底盘标定 ROS 消息,底盘遥测发布已禁用。") + self.enabled = False + return + + if not rclpy.ok(): + rclpy.init(args=None) + + self.node = rclpy.create_node("isaac_chassis_calibration_telemetry_publisher") + self.publisher = self.node.create_publisher(ChassisTelemetry, self.topic, 10) + print(f"[*] 底盘标定遥测发布已启用: topic={self.topic}, chassis_type={self.chassis_type}") + + def make_module_state(self, module_id, wheel_speed_rpm, steer_angle_deg, dt): + ticks_per_rev = 2048.0 + previous_ticks = self.encoder_ticks.get(module_id, 0.0) + next_ticks = previous_ticks + wheel_speed_rpm / 60.0 * ticks_per_rev * max(dt, 0.0) + self.encoder_ticks[module_id] = next_ticks + + module = WheelModuleState() + module.module_id = module_id + module.encoder_ticks = int(next_ticks) + module.wheel_speed_rpm = float(wheel_speed_rpm) + module.steer_angle_deg = float(steer_angle_deg) + module.motor_current_amp = float(0.8 + 0.15 * abs(wheel_speed_rpm) / 100.0) + return module + + def build_modules(self, forward_velocity, angular_velocity, dt): + wheel_circumference = 2.0 * math.pi * self.wheel_radius + if self.chassis_type == "differential": + left_speed = forward_velocity - angular_velocity * self.wheel_track / 2.0 + right_speed = forward_velocity + angular_velocity * self.wheel_track / 2.0 + return [ + self.make_module_state("left_drive", left_speed / wheel_circumference * 60.0, 0.0, dt), + self.make_module_state("right_drive", right_speed / wheel_circumference * 60.0, 0.0, dt), + ] + + if self.chassis_type == "ackermann": + steer_rad = 0.0 + if abs(forward_velocity) > 0.02: + steer_rad = math.atan(self.wheel_base * angular_velocity / forward_velocity) + wheel_speed_rpm = forward_velocity / wheel_circumference * 60.0 + steer_deg = math.degrees(steer_rad) + return [ + self.make_module_state("front_left", wheel_speed_rpm, steer_deg, dt), + self.make_module_state("front_right", wheel_speed_rpm, steer_deg, dt), + self.make_module_state("rear_left", wheel_speed_rpm, 0.0, dt), + self.make_module_state("rear_right", wheel_speed_rpm, 0.0, dt), + ] + + wheel_speed_rpm = forward_velocity / wheel_circumference * 60.0 + steer_angle = math.degrees(math.atan2(angular_velocity * self.wheel_track, max(abs(forward_velocity), 0.05))) + module_ids = ["front_left", "front_right", "rear_left", "rear_right"] + return [self.make_module_state(module_id, wheel_speed_rpm, steer_angle, dt) for module_id in module_ids] + + def publish(self, agv): + if not self.enabled or self.publisher is None: + return + + now = time.time() + if self.publish_period_sec > 0.0 and (now - self.last_publish_wall_time) < self.publish_period_sec: + return + + dt = self.publish_period_sec if self.last_publish_wall_time == 0.0 else now - self.last_publish_wall_time + position, yaw_rad, linear_velocity, angular_velocity = vehicle_state_from_isaac(agv) + forward_axis = np.array([math.cos(yaw_rad), math.sin(yaw_rad)]) + lateral_axis = np.array([-math.sin(yaw_rad), math.cos(yaw_rad)]) + planar_velocity = np.array([linear_velocity[0], linear_velocity[1]]) + forward_velocity = float(np.dot(planar_velocity, forward_axis)) + lateral_velocity = float(np.dot(planar_velocity, lateral_axis)) + yaw_rate = float(angular_velocity[2]) + + msg = ChassisTelemetry() + msg.hardware_timestamp_us = time.time_ns() // 1000 + msg.chassis_type.value = chassis_type_value(self.chassis_type) + msg.odom_x_m = float(position[0] + self.rng.normal(0.0, self.odom_noise_stddev_m)) + msg.odom_y_m = float(position[1] + self.rng.normal(0.0, self.odom_noise_stddev_m)) + msg.odom_yaw_rad = float(normalize_angle(yaw_rad + self.rng.normal(0.0, self.yaw_noise_stddev_rad))) + msg.linear_velocity_ms = forward_velocity + msg.angular_velocity_rads = yaw_rate + msg.modules = self.build_modules(forward_velocity, yaw_rate, dt) + msg.estop_engaged = False + msg.driver_error_code = 0 + msg.active_job_id = "isaac_chassis_calibration" + msg.lateral_slip_estimate = lateral_velocity + msg.curvature_estimate = yaw_rate / forward_velocity if abs(forward_velocity) > 0.02 else 0.0 + + self.publisher.publish(msg) + self.last_publish_wall_time = now + + def shutdown(self): + if self.node is not None: + self.node.destroy_node() + self.node = None + + +class ControlCalibrationTelemetryPublisher: + def __init__(self, args): + self.enabled = not args.disable_control_telemetry + 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.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.last_publish_wall_time = 0.0 + self.node = None + self.publisher = None + + if not self.enabled: + print("[*] 已禁用运控评估遥测发布。") + return + + if rclpy is None or ControlTelemetry is None or ControllerAlgorithmType is None: + print("[WARN] 未找到运控标定 ROS 消息,运控遥测发布已禁用。") + self.enabled = False + return + + if not rclpy.ok(): + rclpy.init(args=None) + + self.node = rclpy.create_node("isaac_control_calibration_telemetry_publisher") + self.publisher = self.node.create_publisher(ControlTelemetry, self.topic, 10) + print(f"[*] 运控评估遥测发布已启用: topic={self.topic}, parameter_version={self.parameter_version}") + + def publish(self, agv): + if not self.enabled or self.publisher is None: + return + + now = time.time() + if self.publish_period_sec > 0.0 and (now - self.last_publish_wall_time) < self.publish_period_sec: + return + + 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_velocity = float(np.dot(np.array([linear_velocity[0], linear_velocity[1]]), forward_axis)) + lateral_error = float(position[1] - self.reference_y_m) + heading_error = normalize_angle(yaw_rad) + speed_error = forward_velocity - self.reference_speed_ms + steering_output = clamp(-0.8 * lateral_error - 1.2 * heading_error, -1.0, 1.0) + throttle_output = clamp(-2.0 * speed_error, 0.0, 1.0) + brake_output = clamp(2.0 * speed_error, 0.0, 1.0) + + msg = ControlTelemetry() + msg.hardware_timestamp_us = time.time_ns() // 1000 + msg.odom_x_m = float(position[0]) + msg.odom_y_m = float(position[1]) + msg.odom_yaw_rad = float(yaw_rad) + msg.linear_velocity_ms = forward_velocity + msg.angular_velocity_rads = float(angular_velocity[2]) + msg.lateral_error_m = lateral_error + msg.heading_error_rad = heading_error + msg.speed_error_ms = speed_error + msg.steering_output = steering_output + msg.throttle_output = throttle_output + msg.brake_output = brake_output + msg.saturation_flag = abs(steering_output) > 0.98 or throttle_output > 0.98 or brake_output > 0.98 + msg.active_job_id = "isaac_control_calibration" + msg.parameter_version = self.parameter_version + msg.active_lateral_algorithm.value = controller_algorithm_value("pure_pursuit") + msg.active_longitudinal_algorithm.value = controller_algorithm_value("pid") + + self.publisher.publish(msg) + self.last_publish_wall_time = now + + def shutdown(self): + if self.node is not None: + self.node.destroy_node() + self.node = None + + +class SensorCalibrationTelemetryPublisher: + def __init__(self, args): + self.enabled = not args.disable_sensor_telemetry + self.topic = args.sensor_telemetry_topic + self.publish_period_sec = 0.0 if args.sensor_telemetry_hz <= 0.0 else 1.0 / args.sensor_telemetry_hz + self.sensor_id = args.front_camera_sensor_id + self.target_sample_count = max(0, args.sensor_target_sample_count) + self.quality_score = clamp(args.sensor_quality_score, 0.0, 1.0) + self.target_detected = not args.sensor_target_missing + self.last_publish_wall_time = 0.0 + self.collected_sample_count = 0 + self.node = None + self.publisher = None + + if not self.enabled: + print("[*] 已禁用传感器标定遥测发布。") + return + + if rclpy is None or SensorCalibrationTelemetry is None or CaptureIntentType is None or SensorType is None: + print("[WARN] 未找到传感器标定 ROS 消息,传感器遥测发布已禁用。") + self.enabled = False + return + + if not rclpy.ok(): + rclpy.init(args=None) + + self.node = rclpy.create_node("isaac_sensor_calibration_telemetry_publisher") + self.publisher = self.node.create_publisher(SensorCalibrationTelemetry, self.topic, 10) + print(f"[*] 传感器标定遥测发布已启用: topic={self.topic}, sensor_id={self.sensor_id}") + + def publish(self): + if not self.enabled or self.publisher is None: + return + + now = time.time() + if self.publish_period_sec > 0.0 and (now - self.last_publish_wall_time) < self.publish_period_sec: + return + + if self.target_detected: + self.collected_sample_count += 1 + + msg = SensorCalibrationTelemetry() + msg.hardware_timestamp_us = time.time_ns() // 1000 + msg.sensor_id = self.sensor_id + msg.sensor_type.value = SensorType.FRONT_CAMERA + msg.collected_sample_count = ( + min(self.collected_sample_count, self.target_sample_count) + if self.target_sample_count > 0 + else self.collected_sample_count + ) + msg.target_sample_count = self.target_sample_count + msg.target_detected = self.target_detected + msg.quality_score = self.quality_score if self.target_detected else 0.0 + msg.active_job_id = f"{self.sensor_id}:isaac_sensor_telemetry" + msg.capture_intent.value = CaptureIntentType.CAMERA_EXTRINSIC_CAPTURE + + self.publisher.publish(msg) + self.last_publish_wall_time = now + + def shutdown(self): + if self.node is not None: + self.node.destroy_node() + self.node = None + + +class AckermannUrdfJointDriver: + def __init__(self, args): + self.wheel_base = args.wheel_base + self.wheel_radius = args.wheel_radius + self.max_steer_angle_rad = math.radians(args.max_steer_angle_deg) + self.max_speed_ms = args.max_speed_ms + self.joint_indices = {} + self.warned = False + + def configure(self, robot): + joint_names = [ + "front_left_steer_joint", + "front_right_steer_joint", + "front_left_wheel_joint", + "front_right_wheel_joint", + "rear_left_wheel_joint", + "rear_right_wheel_joint", + ] + for name in joint_names: + try: + self.joint_indices[name] = int(robot.get_dof_index(name)) + except Exception: + continue + + if len(self.joint_indices) < len(joint_names): + missing = sorted(set(joint_names) - set(self.joint_indices)) + print(f"[WARN] 部分阿克曼关节未找到,车轮动画可能不完整: {missing}") + else: + print("[*] 阿克曼 URDF 关节动画已启用:前轮转向,后轮驱动。") + + def steering_from_cmd(self, linear_x, angular_z): + linear_x = clamp(float(linear_x), -self.max_speed_ms, self.max_speed_ms) + angular_z = float(angular_z) + if abs(linear_x) < 0.02: + return clamp(math.copysign(self.max_steer_angle_rad, angular_z), -self.max_steer_angle_rad, self.max_steer_angle_rad) if abs(angular_z) > 0.02 else 0.0 + steer = math.atan(self.wheel_base * angular_z / linear_x) + return clamp(steer, -self.max_steer_angle_rad, self.max_steer_angle_rad) + + def apply(self, robot, linear_x, angular_z): + if not self.joint_indices: + return + + steer = self.steering_from_cmd(linear_x, angular_z) + wheel_velocity = clamp(float(linear_x), -self.max_speed_ms, self.max_speed_ms) / self.wheel_radius + + try: + steer_indices = [ + self.joint_indices[name] + for name in ("front_left_steer_joint", "front_right_steer_joint") + if name in self.joint_indices + ] + if steer_indices: + robot.set_joint_positions(np.array([steer] * len(steer_indices)), joint_indices=np.array(steer_indices)) + + wheel_indices = [ + self.joint_indices[name] + for name in ( + "front_left_wheel_joint", + "front_right_wheel_joint", + "rear_left_wheel_joint", + "rear_right_wheel_joint", + ) + if name in self.joint_indices + ] + if wheel_indices: + robot.set_joint_velocities(np.array([wheel_velocity] * len(wheel_indices)), joint_indices=np.array(wheel_indices)) + except Exception as exc: + if not self.warned: + print(f"[WARN] 阿克曼关节动画写入失败,车辆根位姿控制仍继续: {exc}") + self.warned = True + + +class IsaacWorkshopRuntime: + def __init__(self, args): + self.args = args + self.camera_topic = resolve_topic(args.ros_topic_prefix, "/camera/image_raw") + self.lidar_topic_prefix = resolve_topic(args.ros_topic_prefix, "/lidar") + self.cmd_vel_topic = args.cmd_vel_topic + self.vehicle_camera_topic = args.vehicle_camera_topic + self.down_camera_topic = args.down_camera_topic + self.vehicle_lidar_topic = args.vehicle_lidar_topic + self.vehicle_2d_lidar_topic = args.vehicle_2d_lidar_topic + self.vehicle_imu_topic = args.vehicle_imu_topic + self.checkerboard_texture_path = GENERATED_ROOT / "checkerboard.png" + self.down_camera_charuco_texture_path = GENERATED_ROOT / "down_camera_charuco.png" + self.source_urdf_path = Path(args.urdf_path).expanduser().resolve() if args.urdf_path else DEFAULT_ACKERMANN_URDF_PATH + should_fix_legacy_ack_m = ( + not args.disable_urdf_visual_origin_fix + and self.source_urdf_path.name == "ack_m.urdf" + ) + self.urdf_path = ( + prepare_isaac_urdf(self.source_urdf_path, GENERATED_ROOT / "ack_m_isaac_fixed.urdf") + if should_fix_legacy_ack_m + else self.source_urdf_path + ) + self.usd_path = None + self.vehicle_prim_path = "" + self.vehicle = None + self.sensor_render_products = [] + self.calibration_board_specs = [] + self.lidar_2d_target_specs = [] + self.down_camera_target_specs = [] + self.scene_manifest_path = ( + Path(args.scene_manifest_path).expanduser().resolve() + if args.scene_manifest_path + else GENERATED_ROOT / "workshop_scene_manifest.json" + ) + + if not self.source_urdf_path.exists(): + raise FileNotFoundError(f"未找到车辆 URDF: {self.source_urdf_path}") + + def create_ros_camera_render_product(self, prim_path, position, orientation, focal_length): + create_prim( + prim_path=prim_path, + prim_type="Camera", + position=np.array(position), + orientation=np.array(orientation), + attributes={ + "focalLength": float(focal_length), + }, + ) + render_product = rep.create.render_product( + prim_path, + (self.args.camera_resolution_width, self.args.camera_resolution_height), + ) + self.sensor_render_products.append(render_product) + return str(render_product.path) + + def write_scene_manifest(self): + manifest = { + "vehicle": { + "vehicle_id": self.args.vehicle_id, + "vehicle_source": "urdf_direct", + "source_urdf_path": str(self.source_urdf_path), + "urdf_path": str(self.urdf_path), + "usd_path": None, + "stage_prim_path": self.vehicle_prim_path, + "drive_wheels": ["rear_left_wheel_link", "rear_right_wheel_link"], + "steer_wheels": ["front_left_wheel_link", "front_right_wheel_link"], + "initial_pose": { + "x_m": self.args.vehicle_initial_x, + "y_m": self.args.vehicle_initial_y, + "z_m": self.args.vehicle_initial_z, + "yaw_rad": 0.0, + }, + }, + "workshop": { + "workcell_zone_id": self.args.external_workcell_zone_id, + "room_length_m": self.args.room_length, + "room_width_m": self.args.room_width, + "room_height_m": self.args.room_height, + "checkerboard": { + "rows": self.args.checkerboard_rows, + "cols": self.args.checkerboard_cols, + "square_size_m": self.args.checkerboard_square_size, + "texture_path": str(self.checkerboard_texture_path), + "reserved_wall_zones": lidar_2d_checkerboard_reserved_zones(self.args), + "layout": self.calibration_board_specs, + }, + "down_camera_intrinsic_target": { + "enabled": not self.args.disable_down_camera_intrinsic_target, + "texture_path": str(self.down_camera_charuco_texture_path), + "pattern": "charuco", + "squares_x": self.args.down_camera_target_squares_x, + "squares_y": self.args.down_camera_target_squares_y, + "square_size_x_m": self.args.down_camera_target_length / self.args.down_camera_target_squares_x, + "square_size_y_m": self.args.down_camera_target_width / self.args.down_camera_target_squares_y, + "layout": self.down_camera_target_specs, + }, + }, + "ros_topics": { + "cmd_vel": self.cmd_vel_topic, + "camera_image": self.camera_topic, + "lidar_prefix": self.lidar_topic_prefix, + "front_camera_image": self.vehicle_camera_topic, + "down_camera_image": self.down_camera_topic, + "lidar_3d_pointcloud": self.vehicle_lidar_topic, + "lidar_2d_scan": self.vehicle_2d_lidar_topic, + "imu": self.vehicle_imu_topic, + "external_telemetry": self.args.external_telemetry_topic, + "chassis_telemetry": self.args.chassis_telemetry_topic, + "control_telemetry": self.args.control_telemetry_topic, + "sensor_telemetry": self.args.sensor_telemetry_topic, + }, + "chassis_calibration": { + "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_track_m": self.args.wheel_track, + "wheel_base_m": self.args.wheel_base, + }, + "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, + }, + "external_truth": { + "localization_source_id": self.args.external_reference_source_name, + "reference_source_name": self.args.external_reference_source_name, + "workcell_zone_id": self.args.external_workcell_zone_id, + "expected_position_stddev_m": self.args.external_position_stddev_m, + "expected_yaw_stddev_rad": self.args.external_yaw_stddev_rad, + "expected_time_sync_offset_ms": self.args.external_time_sync_offset_ms, + }, + "sensors": [ + { + "sensor_id": self.args.front_camera_sensor_id, + "sensor_type": "front_camera", + "frame_id": "front_camera_link", + "image_topic": self.vehicle_camera_topic, + "telemetry_topic": self.args.sensor_telemetry_topic, + "mount_pose": { + "x_m": self.args.vehicle_camera_x, + "y_m": self.args.vehicle_camera_y, + "z_m": self.args.vehicle_camera_z, + "roll_rad": 0.0, + "pitch_rad": -math.pi / 2.0, + "yaw_rad": 0.0, + }, + }, + { + "sensor_id": "demo_down_camera", + "sensor_type": "down_camera", + "frame_id": "down_camera_link", + "image_topic": self.down_camera_topic, + "mount_pose": { + "x_m": self.args.down_camera_x, + "y_m": self.args.down_camera_y, + "z_m": self.args.down_camera_z, + "roll_rad": 0.0, + "pitch_rad": 0.0, + "yaw_rad": 0.0, + }, + }, + { + "sensor_id": "demo_lidar_3d", + "sensor_type": "lidar_3d", + "frame_id": "lidar_3d_link", + "pointcloud_topic": self.vehicle_lidar_topic, + "mount_pose": { + "x_m": self.args.vehicle_lidar_x, + "y_m": self.args.vehicle_lidar_y, + "z_m": self.args.vehicle_lidar_z, + "roll_rad": 0.0, + "pitch_rad": 0.0, + "yaw_rad": 0.0, + }, + }, + { + "sensor_id": "demo_lidar_2d", + "sensor_type": "lidar_2d", + "frame_id": "lidar_2d_link", + "scan_topic": self.vehicle_2d_lidar_topic, + "mount_pose": { + "x_m": self.args.vehicle_2d_lidar_x, + "y_m": self.args.vehicle_2d_lidar_y, + "z_m": self.args.vehicle_2d_lidar_z, + "roll_rad": 0.0, + "pitch_rad": 0.0, + "yaw_rad": 0.0, + }, + }, + { + "sensor_id": "demo_imu", + "sensor_type": "imu", + "frame_id": "imu_link", + "imu_topic": self.vehicle_imu_topic, + "mount_pose": { + "x_m": self.args.vehicle_imu_x, + "y_m": self.args.vehicle_imu_y, + "z_m": self.args.vehicle_imu_z, + "roll_rad": 0.0, + "pitch_rad": 0.0, + "yaw_rad": 0.0, + }, + }, + ], + "lidar_2d_calibration_targets": { + "enabled": not self.args.disable_2d_lidar_targets, + "target_width_m": self.args.lidar_2d_target_width, + "target_height_m": self.args.lidar_2d_target_height, + "target_thickness_m": self.args.lidar_2d_target_thickness, + "target_bottom_z_m": self.args.lidar_2d_target_bottom_z, + "slope_angle_deg": self.args.lidar_2d_target_slope_angle_deg, + "targets": self.lidar_2d_target_specs, + }, + "orchestrator_session_config_hint": { + "vehicle_id": self.args.vehicle_id, + "localization_source_id": self.args.external_reference_source_name, + "workcell_zone_id": self.args.external_workcell_zone_id, + "reference_target_id": "isaac_external_truth", + }, + } + + self.scene_manifest_path.parent.mkdir(parents=True, exist_ok=True) + with self.scene_manifest_path.open("w", encoding="utf-8") as fp: + json.dump(manifest, fp, ensure_ascii=False, indent=2) + fp.write("\n") + print(f"[*] 场景 manifest 已写入: {self.scene_manifest_path}") + + def import_vehicle_to_stage(self, world): + status, import_config = omni.kit.commands.execute("URDFCreateImportConfig") + if not status: + raise RuntimeError("创建 URDF import_config 失败。") + + import_config.merge_fixed_joints = False + import_config.convex_decomp = False + import_config.fix_base = False + import_config.make_default_prim = True + if hasattr(import_config, "import_inertia_tensor"): + import_config.import_inertia_tensor = True + + print(f"[*] 直接从 URDF 路径导入车辆: {self.urdf_path}") + status, imported_prim_path = omni.kit.commands.execute( + "URDFParseAndImportFile", + urdf_path=str(self.urdf_path), + import_config=import_config, + ) + if not status or not imported_prim_path: + raise RuntimeError(f"URDF 导入失败: {self.urdf_path}") + + self.vehicle_prim_path = str(imported_prim_path) + self.vehicle = world.scene.add( + Robot( + prim_path=self.vehicle_prim_path, + name="agv_vehicle", + position=np.array([ + self.args.vehicle_initial_x, + self.args.vehicle_initial_y, + self.args.vehicle_initial_z, + ]), + ) + ) + print(f"[*] 车辆已导入 stage: {self.vehicle_prim_path}") + + stage = omni.usd.get_context().get_stage() + physx_rb = PhysxSchema.PhysxRigidBodyAPI.Get(stage, self.vehicle_prim_path) + if physx_rb: + physx_rb.GetSleepThresholdAttr().Set(0.0) + + def find_vehicle_rigid_body_prim_path(self, preferred_names): + stage = omni.usd.get_context().get_stage() + vehicle_root = self.vehicle_prim_path.rstrip("/") + preferred = set(preferred_names) + fallback_path = "" + + for prim in stage.Traverse(): + prim_path = str(prim.GetPath()) + if prim_path != vehicle_root and not prim_path.startswith(f"{vehicle_root}/"): + continue + + rigid_body = PhysxSchema.PhysxRigidBodyAPI.Get(stage, prim_path) + if not rigid_body: + continue + + prim_name = prim.GetName() + if prim_name in preferred: + return prim_path + if not fallback_path: + fallback_path = prim_path + + return fallback_path + + def add_vehicle_sensor_publishers(self): + if ( + self.args.disable_vehicle_camera + and self.args.disable_down_camera + and self.args.disable_vehicle_lidar + and self.args.disable_vehicle_2d_lidar + and self.args.disable_vehicle_imu + ): + return + + keys = og.Controller.Keys + nodes = [("OnTick", "omni.graph.action.OnTick")] + connections = [] + set_values = [] + vehicle_root = self.vehicle_prim_path + + if not self.args.disable_vehicle_camera: + camera_quat = euler_angles_to_quat(np.array([0.0, -90.0, 0.0]), degrees=True) + vehicle_camera_render_product = self.create_ros_camera_render_product( + prim_path=f"{vehicle_root}/SimFrontCamera", + position=[ + self.args.vehicle_camera_x, + self.args.vehicle_camera_y, + self.args.vehicle_camera_z, + ], + orientation=camera_quat, + focal_length=self.args.camera_focal_length, + ) + nodes.append(("VehicleFrontCamera", "omni.isaac.ros2_bridge.ROS2CameraHelper")) + connections.append(("OnTick.outputs:tick", "VehicleFrontCamera.inputs:execIn")) + set_values.extend([ + ("VehicleFrontCamera.inputs:renderProductPath", vehicle_camera_render_product), + ("VehicleFrontCamera.inputs:topicName", self.vehicle_camera_topic), + ("VehicleFrontCamera.inputs:frameId", "front_camera_link"), + ("VehicleFrontCamera.inputs:type", "rgb"), + ]) + + if not self.args.disable_down_camera: + down_camera_render_product = self.create_ros_camera_render_product( + prim_path=f"{vehicle_root}/SimDownCamera", + position=[ + self.args.down_camera_x, + self.args.down_camera_y, + self.args.down_camera_z, + ], + orientation=[1.0, 0.0, 0.0, 0.0], + focal_length=self.args.camera_focal_length, + ) + nodes.append(("VehicleDownCamera", "omni.isaac.ros2_bridge.ROS2CameraHelper")) + connections.append(("OnTick.outputs:tick", "VehicleDownCamera.inputs:execIn")) + set_values.extend([ + ("VehicleDownCamera.inputs:renderProductPath", down_camera_render_product), + ("VehicleDownCamera.inputs:topicName", self.down_camera_topic), + ("VehicleDownCamera.inputs:frameId", "down_camera_link"), + ("VehicleDownCamera.inputs:type", "rgb"), + ]) + + if not self.args.disable_vehicle_lidar: + lidar_path = f"{vehicle_root}/SimLidar3D" + lidar_quat = euler_angles_to_quat(np.array([0.0, 0.0, 0.0]), degrees=True) + omni.kit.commands.execute( + "IsaacSensorCreateRtxLidar", + path=lidar_path, + parent=None, + config=self.args.lidar_config, + translation=Gf.Vec3d( + self.args.vehicle_lidar_x, + self.args.vehicle_lidar_y, + self.args.vehicle_lidar_z, + ), + orientation=Gf.Quatd(lidar_quat[0], lidar_quat[1], lidar_quat[2], lidar_quat[3]), + ) + render_product = rep.create.render_product(lidar_path, [1, 1]) + self.sensor_render_products.append(render_product) + nodes.append(("VehicleLidar3D", "omni.isaac.ros2_bridge.ROS2RtxLidarHelper")) + connections.append(("OnTick.outputs:tick", "VehicleLidar3D.inputs:execIn")) + set_values.extend([ + ("VehicleLidar3D.inputs:renderProductPath", str(render_product.path)), + ("VehicleLidar3D.inputs:topicName", self.vehicle_lidar_topic), + ("VehicleLidar3D.inputs:frameId", "lidar_3d_link"), + ("VehicleLidar3D.inputs:type", "point_cloud"), + ("VehicleLidar3D.inputs:fullScan", True), + ]) + + if not self.args.disable_vehicle_2d_lidar: + lidar_2d_path = f"{vehicle_root}/SimLidar2D" + lidar_2d_quat = euler_angles_to_quat(np.array([0.0, 0.0, 0.0]), degrees=True) + omni.kit.commands.execute( + "IsaacSensorCreateRtxLidar", + path=lidar_2d_path, + parent=None, + config=self.args.lidar_config, + translation=Gf.Vec3d( + self.args.vehicle_2d_lidar_x, + self.args.vehicle_2d_lidar_y, + self.args.vehicle_2d_lidar_z, + ), + orientation=Gf.Quatd(lidar_2d_quat[0], lidar_2d_quat[1], lidar_2d_quat[2], lidar_2d_quat[3]), + ) + render_product_2d = rep.create.render_product(lidar_2d_path, [1, 1]) + self.sensor_render_products.append(render_product_2d) + print(f"[*] 2D LiDAR RTX sensor prim 已创建: {lidar_2d_path}") + print(f"[*] 2D LiDAR ROS topic 由 Isaac 几何扫描发布器输出: {self.vehicle_2d_lidar_topic}") + + if not self.args.disable_vehicle_imu: + imu_parent_path = self.find_vehicle_rigid_body_prim_path(["base_link", "imu_link"]) + if not imu_parent_path: + print("[WARN] 未找到可挂载 IMU 的车辆刚体 link,跳过 Isaac IMU sensor prim 创建。") + imu_parent_path = "" + imu_path = f"{imu_parent_path}/SimImu" if imu_parent_path else "" + if imu_parent_path: + imu_quat = Gf.Quatd(1.0, 0.0, 0.0, 0.0) + status, _sensor = omni.kit.commands.execute( + "IsaacSensorCreateImuSensor", + path="/SimImu", + parent=imu_parent_path, + sensor_period=-1.0, + translation=Gf.Vec3d( + self.args.vehicle_imu_x, + self.args.vehicle_imu_y, + self.args.vehicle_imu_z, + ), + orientation=imu_quat, + ) + if status: + print(f"[*] 车载 IMU sensor prim 已挂载到刚体 link: {imu_parent_path}, sensor={imu_path}") + print(f"[*] IMU ROS topic 由 Isaac 车辆状态发布器输出: {self.vehicle_imu_topic}") + else: + print(f"[WARN] IsaacSensorCreateImuSensor 创建失败,parent={imu_parent_path},IMU ROS topic 将继续由 Isaac 车辆状态发布器输出。") + + og.Controller.edit( + {"graph_path": "/World/ROS2_Vehicle_Sensors_Graph", "evaluator_name": "execution"}, + {keys.CREATE_NODES: nodes, keys.CONNECT: connections, keys.SET_VALUES: set_values}, + ) + + def build_workshop(self): + world = World(stage_units_in_meters=1.0) + room_length = self.args.room_length + room_width = self.args.room_width + room_height = self.args.room_height + wall_thickness = self.args.wall_thickness + floor_color = np.array([0.2, 0.2, 0.2]) + wall_color = np.array([0.8, 0.8, 0.8]) + + world.scene.add(FixedCuboid( + prim_path="/World/Workshop/Floor", + name="floor", + position=np.array([0, 0, -wall_thickness / 2]), + scale=np.array([room_length + 2 * wall_thickness, room_width + 2 * wall_thickness, wall_thickness]), + color=floor_color, + )) + world.scene.add(FixedCuboid( + prim_path="/World/Workshop/Ceiling", + name="ceiling", + position=np.array([0, 0, room_height + wall_thickness / 2]), + scale=np.array([room_length + 2 * wall_thickness, room_width + 2 * wall_thickness, wall_thickness]), + color=wall_color, + )) + world.scene.add(FixedCuboid( + prim_path="/World/Workshop/Wall_Front", + name="wall_front", + position=np.array([room_length / 2 + wall_thickness / 2, 0, room_height / 2]), + scale=np.array([wall_thickness, room_width, room_height]), + color=wall_color, + )) + world.scene.add(FixedCuboid( + prim_path="/World/Workshop/Wall_Back", + name="wall_back", + position=np.array([-room_length / 2 - wall_thickness / 2, 0, room_height / 2]), + scale=np.array([wall_thickness, room_width, room_height]), + color=wall_color, + )) + world.scene.add(FixedCuboid( + prim_path="/World/Workshop/Wall_Left", + name="wall_left", + position=np.array([0, room_width / 2 + wall_thickness / 2, room_height / 2]), + scale=np.array([room_length + 2 * wall_thickness, wall_thickness, room_height]), + color=wall_color, + )) + world.scene.add(FixedCuboid( + prim_path="/World/Workshop/Wall_Right", + name="wall_right", + position=np.array([0, -room_width / 2 - wall_thickness / 2, room_height / 2]), + scale=np.array([room_length + 2 * wall_thickness, wall_thickness, room_height]), + color=wall_color, + )) + + light_positions = [ + (room_length / 4, room_width / 4, room_height - 0.5), + (room_length / 4, -room_width / 4, room_height - 0.5), + (-room_length / 4, room_width / 4, room_height - 0.5), + (-room_length / 4, -room_width / 4, room_height - 0.5), + ] + for index, position in enumerate(light_positions): + create_prim( + prim_path=f"/World/Workshop/Lights/Light_{index}", + prim_type="SphereLight", + position=np.array(position), + attributes={ + "inputs:radius": 0.3, + "inputs:intensity": self.args.light_intensity, + "inputs:color": (1.0, 1.0, 0.95), + }, + ) + + checkerboard_texture = create_checkerboard_image( + self.checkerboard_texture_path, + rows=self.args.checkerboard_rows, + cols=self.args.checkerboard_cols, + square_size_px=500, + ) + stage = omni.usd.get_context().get_stage() + material = create_raw_usd_material(stage, "/World/Workshop/Materials/CheckerboardMat", checkerboard_texture) + down_camera_target_material = None + if not self.args.disable_down_camera_intrinsic_target: + down_camera_charuco_texture = create_charuco_image( + self.down_camera_charuco_texture_path, + squares_x=self.args.down_camera_target_squares_x, + squares_y=self.args.down_camera_target_squares_y, + square_size_px=90, + ) + down_camera_target_material = create_raw_usd_material( + stage, + "/World/Workshop/Materials/DownCameraCharucoMat", + down_camera_charuco_texture, + ) + + board_width = (self.args.checkerboard_cols + 2) * self.args.checkerboard_square_size + board_height = (self.args.checkerboard_rows + 2) * self.args.checkerboard_square_size + if self.args.disable_calibration_boards: + print("[*] 已禁用多姿态棋盘格标定目标阵列。") + else: + self.calibration_board_specs = add_calibration_boards( + stage, + self.args, + board_width, + board_height, + material, + ) + print(f"[*] 已创建 {len(self.calibration_board_specs)} 个多姿态棋盘格标定目标。") + + if self.args.disable_2d_lidar_targets: + print("[*] 已禁用 2D LiDAR 角落外参标定靶标。") + else: + self.lidar_2d_target_specs = add_2d_lidar_calibration_targets(world, self.args) + 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: + print("[*] 已禁用下视相机 3D ChArUco 内参标定台。") + else: + self.down_camera_target_specs = add_down_camera_intrinsic_target( + world, + stage, + self.args, + down_camera_target_material, + ) + print(f"[*] 已创建 {len(self.down_camera_target_specs)} 个下视相机 3D ChArUco 内参标定台。") + + if not self.args.disable_camera: + camera_height = room_height - 0.1 + camera_render_product = self.create_ros_camera_render_product( + prim_path="/World/Workshop/CalibrationCamera", + position=[0.0, 0.0, camera_height], + orientation=[1.0, 0.0, 0.0, 0.0], + focal_length=self.args.camera_focal_length, + ) + + keys = og.Controller.Keys + og.Controller.edit( + {"graph_path": "/World/ROS2_Camera_Graph", "evaluator_name": "execution"}, + { + keys.CREATE_NODES: [ + ("OnTick", "omni.graph.action.OnTick"), + ("ROS2Camera", "omni.isaac.ros2_bridge.ROS2CameraHelper"), + ], + keys.CONNECT: [("OnTick.outputs:tick", "ROS2Camera.inputs:execIn")], + keys.SET_VALUES: [ + ("ROS2Camera.inputs:renderProductPath", camera_render_product), + ("ROS2Camera.inputs:topicName", self.camera_topic), + ("ROS2Camera.inputs:type", "rgb"), + ], + }, + ) + + if not self.args.disable_lidars: + add_corner_rotary_lidars( + room_length=room_length, + room_width=room_width, + height=room_height - 0.2, + lidar_config=self.args.lidar_config, + topic_prefix=self.lidar_topic_prefix, + ) + + self.import_vehicle_to_stage(world) + self.add_vehicle_sensor_publishers() + + keys = og.Controller.Keys + og.Controller.edit( + {"graph_path": "/World/ROS2_Twist_Graph", "evaluator_name": "execution"}, + { + keys.CREATE_NODES: [ + ("OnTick", "omni.graph.action.OnTick"), + ("TwistSub", "omni.isaac.ros2_bridge.ROS2SubscribeTwist"), + ], + keys.CONNECT: [("OnTick.outputs:tick", "TwistSub.inputs:execIn")], + keys.SET_VALUES: [("TwistSub.inputs:topicName", self.cmd_vel_topic)], + }, + ) + + return world + + +def main(): + runtime = IsaacWorkshopRuntime(ARGS) + telemetry_publisher = ExternalTruthTelemetryPublisher(ARGS) + vehicle_imu_publisher = VehicleImuTopicPublisher(ARGS) + chassis_telemetry_publisher = ChassisCalibrationTelemetryPublisher(ARGS) + control_telemetry_publisher = ControlCalibrationTelemetryPublisher(ARGS) + sensor_telemetry_publisher = SensorCalibrationTelemetryPublisher(ARGS) + world = runtime.build_workshop() + vehicle_laser_scan_publisher = VehicleLaserScanTopicPublisher(ARGS, runtime.lidar_2d_target_specs) + runtime.write_scene_manifest() + world.reset() + + if not ARGS.headless: + set_camera_view(eye=np.array([0.001, 0.0, 3.4]), target=np.array([0.0, 0.0, 0.0])) + + print("======================================================") + print(" 🎯 Isaac 标定车间已启动") + print(f" - headless: {ARGS.headless}") + print(f" - 车辆 URDF: {runtime.urdf_path}") + print(f" - 车辆 prim: {runtime.vehicle_prim_path}") + print(f" - 相机 topic: {runtime.camera_topic}") + print(f" - 激光雷达前缀: {runtime.lidar_topic_prefix}") + print(f" - 前视相机 topic: {runtime.vehicle_camera_topic}") + print(f" - 下视相机 topic: {runtime.down_camera_topic}") + print(f" - 3D LiDAR topic: {runtime.vehicle_lidar_topic}") + print(f" - 2D LiDAR topic: {runtime.vehicle_2d_lidar_topic}") + print(f" - IMU topic: {runtime.vehicle_imu_topic}") + print(f" - /cmd_vel topic: {runtime.cmd_vel_topic}") + print(f" - external telemetry topic: {ARGS.external_telemetry_topic}") + print(f" - chassis telemetry topic: {ARGS.chassis_telemetry_topic}") + print(f" - control telemetry topic: {ARGS.control_telemetry_topic}") + print(f" - sensor telemetry topic: {ARGS.sensor_telemetry_topic}") + print(f" - scene manifest: {runtime.scene_manifest_path}") + print("======================================================") + + agv = runtime.vehicle or world.scene.get_object("agv_vehicle") + ackermann_joint_driver = AckermannUrdfJointDriver(ARGS) + ackermann_joint_driver.configure(agv) + render_sensor_outputs = ( + not ARGS.disable_camera + or not ARGS.disable_lidars + or not ARGS.disable_vehicle_camera + or not ARGS.disable_down_camera + or not ARGS.disable_vehicle_lidar + ) + render_frame = (not ARGS.headless) or render_sensor_outputs + if ARGS.headless and render_sensor_outputs: + print("[*] headless 模式下启用离屏渲染,用于相机和 RTX LiDAR render product 发布。") + + try: + while simulation_app.is_running(): + try: + 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: + _, quat = agv.get_world_pose() + current_linear_velocity = agv.get_linear_velocity() + + q = Gf.Quatd(float(quat[0]), float(quat[1]), float(quat[2]), float(quat[3])) + rot_mat = Gf.Matrix3d(Gf.Rotation(q)) + world_linear_velocity = Gf.Vec3d(*lin_vel) * rot_mat + world_angular_velocity = Gf.Vec3d(*ang_vel) * rot_mat + + 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: + print(f"\n[ERROR] 运动学控制循环异常: {exc}") + + world.step(render=render_frame) + telemetry_publisher.publish(agv) + vehicle_imu_publisher.publish(agv) + vehicle_laser_scan_publisher.publish(agv) + chassis_telemetry_publisher.publish(agv) + control_telemetry_publisher.publish(agv) + sensor_telemetry_publisher.publish() + finally: + telemetry_publisher.shutdown() + vehicle_imu_publisher.shutdown() + vehicle_laser_scan_publisher.shutdown() + chassis_telemetry_publisher.shutdown() + control_telemetry_publisher.shutdown() + sensor_telemetry_publisher.shutdown() + if rclpy is not None and rclpy.ok(): + rclpy.shutdown() + simulation_app.close() + + +if __name__ == "__main__": + main() diff --git a/agv_calib_brain/src/calibration_sim/CMakeLists.txt b/agv_calib_brain/src/simulation/legacy_calibration_sim/CMakeLists.txt similarity index 100% rename from agv_calib_brain/src/calibration_sim/CMakeLists.txt rename to agv_calib_brain/src/simulation/legacy_calibration_sim/CMakeLists.txt diff --git a/agv_calib_brain/src/calibration_sim/launch/calibration_system.launch.py b/agv_calib_brain/src/simulation/legacy_calibration_sim/launch/calibration_system.launch.py similarity index 100% rename from agv_calib_brain/src/calibration_sim/launch/calibration_system.launch.py rename to agv_calib_brain/src/simulation/legacy_calibration_sim/launch/calibration_system.launch.py diff --git a/agv_calib_brain/src/calibration_sim/package.xml b/agv_calib_brain/src/simulation/legacy_calibration_sim/package.xml similarity index 100% rename from agv_calib_brain/src/calibration_sim/package.xml rename to agv_calib_brain/src/simulation/legacy_calibration_sim/package.xml diff --git a/agv_calib_brain/src/calibration_sim/urdf/agv.xacro b/agv_calib_brain/src/simulation/legacy_calibration_sim/urdf/agv.xacro similarity index 100% rename from agv_calib_brain/src/calibration_sim/urdf/agv.xacro rename to agv_calib_brain/src/simulation/legacy_calibration_sim/urdf/agv.xacro diff --git a/agv_calib_brain/src/calibration_sim/urdf/calibration_board.xacro b/agv_calib_brain/src/simulation/legacy_calibration_sim/urdf/calibration_board.xacro similarity index 100% rename from agv_calib_brain/src/calibration_sim/urdf/calibration_board.xacro rename to agv_calib_brain/src/simulation/legacy_calibration_sim/urdf/calibration_board.xacro diff --git a/agv_calib_brain/src/calibration_sim/urdf/workshop_sensors.xacro b/agv_calib_brain/src/simulation/legacy_calibration_sim/urdf/workshop_sensors.xacro similarity index 100% rename from agv_calib_brain/src/calibration_sim/urdf/workshop_sensors.xacro rename to agv_calib_brain/src/simulation/legacy_calibration_sim/urdf/workshop_sensors.xacro diff --git a/agv_calib_brain/src/calibration_sim/worlds/calibration_room.world b/agv_calib_brain/src/simulation/legacy_calibration_sim/worlds/calibration_room.world similarity index 100% rename from agv_calib_brain/src/calibration_sim/worlds/calibration_room.world rename to agv_calib_brain/src/simulation/legacy_calibration_sim/worlds/calibration_room.world diff --git a/agv_calib_brain/src/simulation/tools/external_pose_wifi6_bridge.py b/agv_calib_brain/src/simulation/tools/external_pose_wifi6_bridge.py new file mode 100644 index 0000000..0f939e5 --- /dev/null +++ b/agv_calib_brain/src/simulation/tools/external_pose_wifi6_bridge.py @@ -0,0 +1,167 @@ +#!/usr/bin/env python3 +"""Bridge external truth pose from ROS to the simulated WiFi6 TCP vehicle link.""" + +from __future__ import annotations + +import argparse +import json +import socket +import struct +import time +from typing import Any + +import rclpy + +try: + from calibration_external_localization_interfaces.msg import ExternalLocalizationTelemetry +except ImportError: + ExternalLocalizationTelemetry = None + + +EXTERNAL_POSE_PUSH_REQ = 31 +EXTERNAL_POSE_PUSH_RSP = 32 + + +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("connection closed") + chunks.append(chunk) + remaining -= len(chunk) + return b"".join(chunks) + + +def send_request( + host: str, + port: int, + msg_type: int, + payload: dict[str, Any], + timeout_sec: float, +) -> tuple[int, dict[str, Any]]: + encoded = json.dumps(payload, ensure_ascii=False, separators=(",", ":")).encode("utf-8") + with socket.create_connection((host, port), timeout=timeout_sec) as conn: + conn.settimeout(timeout_sec) + conn.sendall(struct.pack(" {args.vehicle_host}:{args.vehicle_port}" + ) + + def rate_limited(self) -> bool: + if self.args.max_hz <= 0.0: + return False + now = time.monotonic() + period = 1.0 / self.args.max_hz + if now - self.last_send_monotonic < period: + return True + self.last_send_monotonic = now + return False + + def on_pose(self, msg: Any) -> None: + if self.rate_limited(): + return + + pose = msg.workshop_pose + source_name = self.args.reference_source_name or str(msg.reference_source_name or "external_truth_wifi6") + payload = { + "hardware_timestamp_us": int(msg.hardware_timestamp_us or now_us()), + "pose_valid": bool(msg.pose_valid), + "workshop_pose": { + "x_m": float(pose.x_m), + "y_m": float(pose.y_m), + "z_m": float(pose.z_m), + "roll_rad": float(pose.roll_rad), + "pitch_rad": float(pose.pitch_rad), + "yaw_rad": float(pose.yaw_rad), + }, + "position_stddev_m": float(msg.position_stddev_m), + "yaw_stddev_rad": float(msg.yaw_stddev_rad), + "tracking_loss_ratio": float(msg.tracking_loss_ratio), + "time_sync_offset_ms": float(msg.time_sync_offset_ms), + "quality_score": float(msg.quality_score), + "observed_target_count": int(msg.observed_target_count), + "reference_source_name": source_name, + "active_job_id": str(msg.active_job_id or ""), + } + + try: + rsp_type, rsp = send_request( + self.args.vehicle_host, + self.args.vehicle_port, + EXTERNAL_POSE_PUSH_REQ, + payload, + self.args.timeout_sec, + ) + if rsp_type != EXTERNAL_POSE_PUSH_RSP or not rsp.get("success", False): + raise RuntimeError(f"unexpected response type={rsp_type}, payload={rsp}") + self.sent_count += 1 + except Exception as exc: + self.fail_count += 1 + now = time.monotonic() + if now - self.last_error_log_monotonic >= self.args.error_log_interval_sec: + self.last_error_log_monotonic = now + self.node.get_logger().warning( + f"failed to push external pose over wifi6_sim_tcp: {exc}; fail_count={self.fail_count}" + ) + + +def parse_args() -> argparse.Namespace: + parser = argparse.ArgumentParser(description="外部真值位姿 ROS -> WiFi6/TCP 车端桥") + parser.add_argument("--source-topic", default="/isaac/external_localization/telemetry") + parser.add_argument("--vehicle-host", default="127.0.0.1") + parser.add_argument("--vehicle-port", type=int, default=9000) + parser.add_argument("--reference-source-name", default="") + parser.add_argument("--max-hz", type=float, default=30.0) + parser.add_argument("--timeout-sec", type=float, default=2.0) + parser.add_argument("--error-log-interval-sec", type=float, default=2.0) + return parser.parse_args() + + +def main() -> int: + args = parse_args() + rclpy.init(args=None) + bridge = None + try: + bridge = ExternalPoseWifi6Bridge(args) + rclpy.spin(bridge.node) + except KeyboardInterrupt: + pass + finally: + if bridge is not None: + bridge.node.destroy_node() + if rclpy.ok(): + rclpy.shutdown() + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/agv_calib_brain/src/simulation/tools/isaac_vehicle_unified_agent_sim.py b/agv_calib_brain/src/simulation/tools/isaac_vehicle_unified_agent_sim.py new file mode 100644 index 0000000..78682c5 --- /dev/null +++ b/agv_calib_brain/src/simulation/tools/isaac_vehicle_unified_agent_sim.py @@ -0,0 +1,248 @@ +#!/usr/bin/env python3 +"""统一车端 agent 仿真。 + +该进程模拟真实 Windows 小车车端电脑:对车间工控机只暴露一个 WiFi6/TCP +入口端口,所有底盘、运控、外部真值位姿和传感器请求都通过 frame_type 分发。 +""" + +from __future__ import annotations + +import argparse +import importlib.util +import json +import socket +import socketserver +import struct +import sys +import threading +from pathlib import Path +from types import ModuleType +from typing import Any + +import rclpy +from rclpy.executors import MultiThreadedExecutor + + +PROJECT_ROOT = Path(__file__).resolve().parents[3] + + +def load_module(name: str, path: Path) -> ModuleType: + spec = importlib.util.spec_from_file_location(name, path) + if spec is None or spec.loader is None: + raise ImportError(f"无法加载模块: {path}") + module = importlib.util.module_from_spec(spec) + sys.modules[name] = module + spec.loader.exec_module(module) + return module + + +vehicle_mod = load_module( + "isaac_vehicle_agent_sim_module", + PROJECT_ROOT / "src" / "simulation" / "vehicle_agent_sim" / "scripts" / "isaac_vehicle_agent_sim.py", +) +sensor_mod = load_module( + "isaac_vehicle_sensor_agent_sim_module", + PROJECT_ROOT / "src" / "simulation" / "vehicle_sensor_agent_sim" / "scripts" / "isaac_vehicle_sensor_agent_sim.py", +) + + +VEHICLE_REQUEST_TYPES = { + vehicle_mod.CHASSIS_GET_READINESS_REQ, + vehicle_mod.CHASSIS_MOTION_PRIMITIVE_REQ, + vehicle_mod.CHASSIS_EMERGENCY_BRAKE_REQ, + vehicle_mod.CONTROL_GET_READINESS_REQ, + vehicle_mod.CONTROL_EVALUATION_REQ, + vehicle_mod.EXTERNAL_POSE_PUSH_REQ, +} + +SENSOR_REQUEST_TYPES = { + sensor_mod.SENSOR_GET_READINESS_REQ, + sensor_mod.SENSOR_GET_LATEST_FRAME_REQ, + sensor_mod.SENSOR_LIST_SENSORS_REQ, +} + + +def recv_exactly(conn: socket.socket, size: int) -> bytes: + data = b"" + while len(data) < size: + chunk = conn.recv(size - len(data)) + if not chunk: + raise ConnectionError("连接提前关闭") + data += chunk + return data + + +def read_frame(conn: socket.socket) -> tuple[int, dict[str, Any]]: + header = recv_exactly(conn, 8) + msg_type, payload_len = struct.unpack(" None: + encoded = json.dumps(payload, ensure_ascii=False, separators=(",", ":")).encode("utf-8") + conn.sendall(struct.pack(" dict[str, Any]: + return { + "success": False, + "error_code": {"code": vehicle_mod.ERROR_INVALID_ARGUMENT}, + "message": message, + } + + +class UnifiedVehicleAgent: + def __init__(self, args: argparse.Namespace) -> None: + self.args = args + self.vehicle_agent = vehicle_mod.IsaacVehicleAgentSim(args) + self.sensor_agent = sensor_mod.IsaacVehicleSensorAgentSim(args) + + def handle_vehicle_request(self, msg_type: int, payload: dict[str, Any]) -> tuple[int, dict[str, Any]]: + if msg_type == vehicle_mod.CHASSIS_GET_READINESS_REQ: + return vehicle_mod.CHASSIS_GET_READINESS_RSP, self.vehicle_agent.readiness_payload("chassis") + if msg_type == vehicle_mod.CHASSIS_MOTION_PRIMITIVE_REQ: + return vehicle_mod.CHASSIS_MOTION_PRIMITIVE_RSP, self.vehicle_agent.handle_motion_primitive(payload) + if msg_type == vehicle_mod.CHASSIS_EMERGENCY_BRAKE_REQ: + self.vehicle_agent.stop() + return vehicle_mod.CHASSIS_EMERGENCY_BRAKE_RSP, { + "success": True, + "error_code": {"code": vehicle_mod.ERROR_OK}, + "message": "Isaac simulated brake executed.", + } + if msg_type == vehicle_mod.CONTROL_GET_READINESS_REQ: + return vehicle_mod.CONTROL_GET_READINESS_RSP, self.vehicle_agent.readiness_payload("control") + if msg_type == vehicle_mod.CONTROL_EVALUATION_REQ: + return vehicle_mod.CONTROL_EVALUATION_RSP, self.vehicle_agent.handle_control_evaluation(payload) + if msg_type == vehicle_mod.EXTERNAL_POSE_PUSH_REQ: + return vehicle_mod.EXTERNAL_POSE_PUSH_RSP, self.vehicle_agent.handle_external_pose_push(payload) + return 0, error_payload(f"unsupported vehicle msg_type={msg_type}") + + def handle_request(self, msg_type: int, payload: dict[str, Any]) -> tuple[int, dict[str, Any]]: + if msg_type in VEHICLE_REQUEST_TYPES: + return self.handle_vehicle_request(msg_type, payload) + if msg_type in SENSOR_REQUEST_TYPES: + return self.sensor_agent.handle_request(msg_type, payload) + return 0, error_payload(f"unsupported unified vehicle frame_type={msg_type}") + + def stop(self) -> None: + self.vehicle_agent.stop() + + +class UnifiedTcpHandler(socketserver.BaseRequestHandler): + def handle(self) -> None: + agent: UnifiedVehicleAgent = self.server.agent + try: + msg_type, payload = read_frame(self.request) + response_type, response_payload = agent.handle_request(msg_type, payload) + except Exception as exc: + response_type = 0 + response_payload = error_payload(str(exc)) + send_frame(self.request, response_type, response_payload) + + +class ThreadingTcpServer(socketserver.ThreadingMixIn, socketserver.TCPServer): + allow_reuse_address = True + daemon_threads = True + + +def build_parser() -> argparse.ArgumentParser: + parser = argparse.ArgumentParser(description="Isaac 统一车端 agent 仿真") + parser.add_argument("--vehicle-id", default="demo_agv_001") + parser.add_argument("--bind-host", default="0.0.0.0") + parser.add_argument("--vehicle-port", type=int, default=9000) + parser.add_argument("--cmd-vel-topic", default="/vehicle/demo_agv_001/actuator/cmd_vel") + parser.add_argument("--external-pose-topic", default="/isaac/external_localization/telemetry") + parser.add_argument( + "--external-pose-transport", + choices=["ros_topic", "wifi6_tcp", "both"], + default="wifi6_tcp", + help="外部真值位姿输入方式;统一车端仿真默认只通过 WiFi6/TCP", + ) + parser.add_argument("--external-pose-port", type=int, default=9000) + parser.add_argument("--external-pose-min-quality", type=float, default=0.1) + parser.add_argument("--chassis-telemetry-topic", default="/chassis/telemetry") + parser.add_argument("--allow-chassis-pose-fallback", action="store_true") + parser.add_argument("--publish-hz", type=float, default=20.0) + parser.add_argument("--max-speed-mps", type=float, default=0.3) + parser.add_argument("--wheel-base-m", type=float, default=0.80) + parser.add_argument("--max-steering-angle-rad", type=float, default=0.60) + parser.add_argument("--internal-command-timeout-sec", type=float, default=0.5) + parser.add_argument("--internal-ackermann-command-topic", default="/vehicle/demo_agv_001/internal/ackermann_cmd") + parser.add_argument("--internal-state-topic", default="/vehicle/demo_agv_001/internal/state") + parser.add_argument("--internal-health-topic", default="/vehicle/demo_agv_001/internal/health") + parser.add_argument("--internal-time-sync-topic", default="/vehicle/demo_agv_001/internal/time_sync_status") + parser.add_argument("--max-angular-speed-rps", type=float, default=1.2) + parser.add_argument("--max-motion-duration-sec", type=float, default=60.0) + parser.add_argument("--pose-wait-timeout-sec", type=float, default=2.0) + parser.add_argument("--pose-stale-timeout-sec", type=float, default=0.5) + parser.add_argument("--trajectory-lookahead-m", type=float, default=0.45) + parser.add_argument("--trajectory-goal-tolerance-m", type=float, default=0.08) + parser.add_argument("--trajectory-slowdown-radius-m", type=float, default=0.5) + parser.add_argument("--trajectory-slowdown-gain", type=float, default=0.8) + parser.add_argument("--default-tracking-speed-mps", type=float, default=0.12) + parser.add_argument("--min-tracking-speed-mps", type=float, default=0.03) + parser.add_argument("--trajectory-max-rms-lateral-error-m", type=float, default=0.20) + parser.add_argument("--trajectory-max-rms-heading-error-rad", type=float, default=0.60) + + parser.add_argument("--max-frame-payload-bytes", type=int, default=262144) + parser.add_argument("--frame-stale-timeout-sec", type=float, default=1.0) + parser.add_argument("--enabled-sensor-ids", nargs="*", default=[], help="只启用指定 sensor_id;默认启用全部") + parser.add_argument("--sensor-link-status-topic", default="/vehicle/demo_agv_001/internal/sensor_link_status") + parser.add_argument("--front-camera-sensor-id", default="demo_front_camera") + parser.add_argument("--down-camera-sensor-id", default="demo_down_camera") + parser.add_argument("--lidar-3d-sensor-id", default="demo_lidar_3d") + parser.add_argument("--lidar-2d-sensor-id", default="demo_lidar_2d") + parser.add_argument("--imu-sensor-id", default="demo_imu") + parser.add_argument("--front-camera-topic", default="/sensor/front_camera/image_raw") + parser.add_argument("--down-camera-topic", default="/sensor/down_camera/image_raw") + parser.add_argument("--lidar-3d-topic", default="/sensor/lidar_3d/pointcloud") + parser.add_argument("--lidar-2d-topic", default="/sensor/lidar_2d/scan") + parser.add_argument("--imu-topic", default="/sensor/imu/data") + return parser + + +def main() -> int: + args = build_parser().parse_args() + rclpy.init(args=None) + agent = UnifiedVehicleAgent(args) + server = None + executor = MultiThreadedExecutor() + executor.add_node(agent.vehicle_agent.node) + executor.add_node(agent.sensor_agent.node) + try: + server = ThreadingTcpServer((args.bind_host, args.vehicle_port), UnifiedTcpHandler) + server.agent = agent + threading.Thread(target=server.serve_forever, daemon=True).start() + print(f"[*] unified vehicle agent sim listening on {args.bind_host}:{args.vehicle_port}") + print(f"[*] publishing private actuator commands to {args.cmd_vel_topic}") + print("[*] subscribed vehicle sensor topics:") + for sensor in agent.sensor_agent.sensors.values(): + print(f" - {sensor.sensor_id}: {sensor.topic}") + while rclpy.ok(): + executor.spin_once(timeout_sec=0.1) + except OSError as exc: + print(f"[ERROR] unified vehicle agent network startup failed: {exc}") + return 1 + except KeyboardInterrupt: + print("[*] unified vehicle agent shutdown requested.") + finally: + if server is not None: + server.shutdown() + server.server_close() + try: + if rclpy.ok(): + agent.stop() + except Exception as exc: + print(f"[WARN] unified vehicle agent stop during shutdown failed: {exc}") + executor.remove_node(agent.vehicle_agent.node) + executor.remove_node(agent.sensor_agent.node) + agent.vehicle_agent.node.destroy_node() + agent.sensor_agent.node.destroy_node() + if rclpy.ok(): + rclpy.shutdown() + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/agv_calib_brain/src/simulation/tools/launch_sim_stack.py b/agv_calib_brain/src/simulation/tools/launch_sim_stack.py new file mode 100755 index 0000000..f71b908 --- /dev/null +++ b/agv_calib_brain/src/simulation/tools/launch_sim_stack.py @@ -0,0 +1,358 @@ +#!/usr/bin/env python3 +"""Launch the Isaac workshop simulation stack from a deployment profile.""" + +from __future__ import annotations + +import argparse +import os +import shlex +import signal +import subprocess +import sys +import time +from pathlib import Path +from typing import Iterable + +from sim_profile import PROJECT_ROOT, as_float, as_int, get_nested, load_profile, repo_path, validate_sim_profile + + +def with_workspace_setup(command: list[str]) -> list[str]: + setup_file = PROJECT_ROOT / "install" / "setup.bash" + if setup_file.exists(): + return ["bash", "-lc", f"source {shlex.quote(str(setup_file))} && {shlex.join(command)}"] + return command + + +def build_vehicle_agent_command(profile: dict, python_executable: str) -> list[str]: + gateway = profile["workshop_pc"]["gateway"] + vehicle_agent = profile["vehicle_agent"] + sensor_agent = profile.get("vehicle_sensor_agent", {}) + command = [ + python_executable, + str(PROJECT_ROOT / "src" / "simulation" / "tools" / "isaac_vehicle_unified_agent_sim.py"), + "--vehicle-id", + str(profile["vehicle_id"]), + "--bind-host", + str(vehicle_agent.get("bind_host", "0.0.0.0")), + "--vehicle-port", + str(as_int(gateway.get("vehicle_port", 9000), "workshop_pc.gateway.vehicle_port")), + "--cmd-vel-topic", + str(vehicle_agent["private_cmd_vel_topic"]), + "--external-pose-topic", + str(vehicle_agent.get("external_pose_topic", "/isaac/external_localization/telemetry")), + "--external-pose-transport", + str(vehicle_agent.get("external_pose_transport", "both")), + "--chassis-telemetry-topic", + str(vehicle_agent.get("chassis_telemetry_topic", "/chassis/telemetry")), + "--max-speed-mps", + str(as_float(get_nested(profile, "safety.max_speed_mps", 0.3), "safety.max_speed_mps")), + "--wheel-base-m", + str(as_float(get_nested(profile, "vehicle_agent.wheel_base_m", 0.8), "vehicle_agent.wheel_base_m")), + "--max-steering-angle-rad", + str(as_float(get_nested(profile, "vehicle_agent.max_steering_angle_rad", 0.6), "vehicle_agent.max_steering_angle_rad")), + "--internal-command-timeout-sec", + str(as_float(get_nested(profile, "vehicle_agent.internal_command_timeout_sec", 0.5), "vehicle_agent.internal_command_timeout_sec")), + "--internal-ackermann-command-topic", + str(vehicle_agent.get("internal_ackermann_command_topic", f"/vehicle/{profile['vehicle_id']}/internal/ackermann_cmd")), + "--internal-state-topic", + str(vehicle_agent.get("internal_state_topic", f"/vehicle/{profile['vehicle_id']}/internal/state")), + "--internal-health-topic", + str(vehicle_agent.get("internal_health_topic", f"/vehicle/{profile['vehicle_id']}/internal/health")), + "--internal-time-sync-topic", + str(vehicle_agent.get("internal_time_sync_topic", f"/vehicle/{profile['vehicle_id']}/internal/time_sync_status")), + "--max-frame-payload-bytes", + str(as_int(sensor_agent.get("max_frame_payload_bytes", 262144), "vehicle_sensor_agent.max_frame_payload_bytes")), + "--front-camera-sensor-id", + str(sensor_agent.get("front_camera_sensor_id", "demo_front_camera")), + "--down-camera-sensor-id", + str(sensor_agent.get("down_camera_sensor_id", "demo_down_camera")), + "--lidar-3d-sensor-id", + str(sensor_agent.get("lidar_3d_sensor_id", "demo_lidar_3d")), + "--lidar-2d-sensor-id", + str(sensor_agent.get("lidar_2d_sensor_id", "demo_lidar_2d")), + "--imu-sensor-id", + str(sensor_agent.get("imu_sensor_id", "demo_imu")), + "--front-camera-topic", + str(sensor_agent.get("front_camera_topic", "/sensor/front_camera/image_raw")), + "--down-camera-topic", + str(sensor_agent.get("down_camera_topic", "/sensor/down_camera/image_raw")), + "--lidar-3d-topic", + str(sensor_agent.get("lidar_3d_topic", "/sensor/lidar_3d/pointcloud")), + "--lidar-2d-topic", + str(sensor_agent.get("lidar_2d_topic", "/sensor/lidar_2d/scan")), + "--imu-topic", + str(sensor_agent.get("imu_topic", "/sensor/imu/data")), + "--sensor-link-status-topic", + str(sensor_agent.get("sensor_link_status_topic", f"/vehicle/{profile['vehicle_id']}/internal/sensor_link_status")), + ] + return with_workspace_setup(command) + + +def build_vehicle_sensor_agent_command(profile: dict, python_executable: str) -> list[str]: + gateway = profile["workshop_pc"]["gateway"] + sensor_agent = profile.get("vehicle_sensor_agent", {}) + command = [ + python_executable, + str(PROJECT_ROOT / "src" / "simulation" / "vehicle_sensor_agent_sim" / "scripts" / "isaac_vehicle_sensor_agent_sim.py"), + "--bind-host", + str(sensor_agent.get("bind_host", "0.0.0.0")), + "--sensor-port", + str(as_int(gateway.get("sensor_port", 9003), "workshop_pc.gateway.sensor_port")), + "--max-frame-payload-bytes", + str(as_int(sensor_agent.get("max_frame_payload_bytes", 262144), "vehicle_sensor_agent.max_frame_payload_bytes")), + "--front-camera-sensor-id", + str(sensor_agent.get("front_camera_sensor_id", "demo_front_camera")), + "--down-camera-sensor-id", + str(sensor_agent.get("down_camera_sensor_id", "demo_down_camera")), + "--lidar-3d-sensor-id", + str(sensor_agent.get("lidar_3d_sensor_id", "demo_lidar_3d")), + "--lidar-2d-sensor-id", + str(sensor_agent.get("lidar_2d_sensor_id", "demo_lidar_2d")), + "--imu-sensor-id", + str(sensor_agent.get("imu_sensor_id", "demo_imu")), + "--front-camera-topic", + str(sensor_agent.get("front_camera_topic", "/sensor/front_camera/image_raw")), + "--down-camera-topic", + str(sensor_agent.get("down_camera_topic", "/sensor/down_camera/image_raw")), + "--lidar-3d-topic", + str(sensor_agent.get("lidar_3d_topic", "/sensor/lidar_3d/pointcloud")), + "--lidar-2d-topic", + str(sensor_agent.get("lidar_2d_topic", "/sensor/lidar_2d/scan")), + "--imu-topic", + str(sensor_agent.get("imu_topic", "/sensor/imu/data")), + "--sensor-link-status-topic", + str(sensor_agent.get("sensor_link_status_topic", f"/vehicle/{profile['vehicle_id']}/internal/sensor_link_status")), + ] + return with_workspace_setup(command) + + +def build_vehicle_wifi6_gateway_command(profile: dict, python_executable: str) -> list[str]: + gateway = profile["workshop_pc"]["gateway"] + command = [ + python_executable, + str(PROJECT_ROOT / "src" / "simulation" / "tools" / "vehicle_wifi6_gateway_sim.py"), + "--bind-host", + str(gateway.get("vehicle_bind_host", "0.0.0.0")), + "--vehicle-port", + str(as_int(gateway.get("vehicle_port", 9000), "workshop_pc.gateway.vehicle_port")), + "--timeout-sec", + str(as_float(gateway.get("timeout_ms", 5000), "workshop_pc.gateway.timeout_ms") / 1000.0), + "--chassis-host", + str(gateway.get("chassis_host", "127.0.0.1")), + "--chassis-port", + str(as_int(gateway.get("chassis_port", 9001), "workshop_pc.gateway.chassis_port")), + "--control-host", + str(gateway.get("control_host", "127.0.0.1")), + "--control-port", + str(as_int(gateway.get("control_port", 9002), "workshop_pc.gateway.control_port")), + "--sensor-host", + str(gateway.get("sensor_host", "127.0.0.1")), + "--sensor-port", + str(as_int(gateway.get("sensor_port", 9003), "workshop_pc.gateway.sensor_port")), + "--external-pose-host", + str(gateway.get("external_pose_host", "127.0.0.1")), + "--external-pose-port", + str(as_int(gateway.get("external_pose_port", 9004), "workshop_pc.gateway.external_pose_port")), + ] + return with_workspace_setup(command) + + +def build_external_pose_bridge_command(profile: dict, python_executable: str) -> list[str]: + gateway = profile["workshop_pc"]["gateway"] + bridge = profile.get("external_pose_bridge", {}) + command = [ + python_executable, + str(PROJECT_ROOT / "src" / "simulation" / "tools" / "external_pose_wifi6_bridge.py"), + "--source-topic", + str(bridge.get("source_topic", get_nested(profile, "vehicle_agent.external_pose_topic", "/isaac/external_localization/telemetry"))), + "--vehicle-host", + str(gateway.get("vehicle_host", "127.0.0.1")), + "--vehicle-port", + str(as_int(gateway.get("vehicle_port", 9000), "workshop_pc.gateway.vehicle_port")), + "--reference-source-name", + str(bridge.get("reference_source_name", "isaac_external_truth")), + "--max-hz", + str(as_float(bridge.get("max_hz", 30.0), "external_pose_bridge.max_hz")), + "--timeout-sec", + str(as_float(gateway.get("timeout_ms", 5000), "workshop_pc.gateway.timeout_ms") / 1000.0), + ] + return with_workspace_setup(command) + + +def build_workshop_sensor_ingest_command(profile: dict, python_executable: str) -> list[str]: + gateway = profile["workshop_pc"]["gateway"] + ingest = profile.get("workshop_sensor_ingest", {}) + command = [ + python_executable, + str(PROJECT_ROOT / "src" / "simulation" / "tools" / "workshop_sensor_ingest_sim.py"), + "--sensor-host", + str(gateway.get("vehicle_host", "127.0.0.1")), + "--sensor-port", + str(as_int(gateway.get("vehicle_port", 9000), "workshop_pc.gateway.vehicle_port")), + "--publish-prefix", + str(ingest.get("publish_prefix", "/workshop/vehicle_sensor")), + "--poll-hz", + str(as_float(ingest.get("poll_hz", 15.0), "workshop_sensor_ingest.poll_hz")), + "--max-payload-bytes", + str(as_int(ingest.get("max_payload_bytes", 4194304), "workshop_sensor_ingest.max_payload_bytes")), + "--timeout-sec", + str(as_float(gateway.get("timeout_ms", 5000), "workshop_pc.gateway.timeout_ms") / 1000.0), + ] + return with_workspace_setup(command) + + +def build_isaac_command(profile: dict, python_executable: str, headless: bool, extra_args: Iterable[str]) -> list[str]: + isaac = profile["isaac"] + targets = profile.get("targets", {}) + down_camera = targets.get("down_camera_charuco", {}) + command = [ + python_executable, + str(repo_path(isaac["scene_script"])), + "--vehicle-id", + str(profile["vehicle_id"]), + "--cmd-vel-topic", + str(isaac["cmd_vel_topic"]), + "--vehicle-camera-topic", + str(get_nested(profile, "vehicle_sensor_agent.front_camera_topic", "/sensor/front_camera/image_raw")), + "--down-camera-topic", + str(get_nested(profile, "vehicle_sensor_agent.down_camera_topic", "/sensor/down_camera/image_raw")), + "--vehicle-lidar-topic", + str(get_nested(profile, "vehicle_sensor_agent.lidar_3d_topic", "/sensor/lidar_3d/pointcloud")), + "--vehicle-2d-lidar-topic", + str(get_nested(profile, "vehicle_sensor_agent.lidar_2d_topic", "/sensor/lidar_2d/scan")), + "--vehicle-imu-topic", + str(get_nested(profile, "vehicle_sensor_agent.imu_topic", "/sensor/imu/data")), + "--scene-manifest-path", + str(repo_path(isaac["scene_manifest_path"])), + ] + if headless: + command.append("--headless") + if down_camera.get("squares_x") is not None: + command.extend(["--down-camera-target-squares-x", str(down_camera["squares_x"])]) + if down_camera.get("squares_y") is not None: + command.extend(["--down-camera-target-squares-y", str(down_camera["squares_y"])]) + command.extend(extra_args) + return with_workspace_setup(command) + + +def build_gateway_command(profile: dict) -> list[str]: + gateway = profile["workshop_pc"]["gateway"] + launch_args = [ + "ros2", + "launch", + "vehicle_agent_gateway", + "vehicle_agent_gateway.launch.py", + f"chassis_host:={gateway['vehicle_host']}", + f"chassis_port:={gateway['vehicle_port']}", + f"chassis_timeout_ms:={gateway.get('timeout_ms', 5000)}", + f"control_host:={gateway['vehicle_host']}", + f"control_port:={gateway['vehicle_port']}", + f"control_timeout_ms:={gateway.get('timeout_ms', 5000)}", + ] + return with_workspace_setup(launch_args) + + +def print_command(name: str, command: list[str]) -> None: + print(f"[{name}] {shlex.join(command)}") + + +def terminate_processes(processes: list[subprocess.Popen]) -> None: + for process in processes: + if process.poll() is None: + process.terminate() + deadline = time.monotonic() + 5.0 + for process in processes: + while process.poll() is None and time.monotonic() < deadline: + time.sleep(0.1) + if process.poll() is None: + process.kill() + + +def parse_args() -> argparse.Namespace: + parser = argparse.ArgumentParser(description="按仿真 profile 启动 Isaac 标定车间链路") + parser.add_argument("--profile", default="", help="仿真 profile 路径,默认使用 src/deployment/profiles/sim_workshop.yaml") + parser.add_argument( + "--component", + choices=[ + "all", + "vehicle-agent", + "sensor-agent", + "wifi6-gateway", + "external-pose-bridge", + "sensor-ingest", + "gateway", + "isaac", + ], + default="all", + help="只启动某个组件,默认启动全部", + ) + parser.add_argument("--python", default=sys.executable, help="用于启动 Python 脚本的解释器") + parser.add_argument("--headless", action="store_true", help="以 headless 模式启动 Isaac") + parser.add_argument("--dry-run", action="store_true", help="只打印命令,不启动进程") + parser.add_argument( + "--isaac-arg", + action="append", + default=[], + help="额外透传给 build_calibration_room.py 的参数,可重复传入", + ) + return parser.parse_args() + + +def main() -> int: + args = parse_args() + profile = load_profile(args.profile or None) + validate_sim_profile(profile) + + commands: list[tuple[str, list[str]]] = [] + if args.component in ("all", "vehicle-agent"): + commands.append(("vehicle-agent", build_vehicle_agent_command(profile, args.python))) + if args.component == "sensor-agent": + commands.append(("sensor-agent", build_vehicle_sensor_agent_command(profile, args.python))) + if args.component == "wifi6-gateway": + commands.append(("wifi6-gateway", build_vehicle_wifi6_gateway_command(profile, args.python))) + if args.component in ("all", "external-pose-bridge"): + commands.append(("external-pose-bridge", build_external_pose_bridge_command(profile, args.python))) + if args.component in ("all", "sensor-ingest"): + commands.append(("sensor-ingest", build_workshop_sensor_ingest_command(profile, args.python))) + if args.component in ("all", "gateway"): + commands.append(("gateway", build_gateway_command(profile))) + if args.component in ("all", "isaac"): + commands.append(("isaac", build_isaac_command(profile, args.python, args.headless, args.isaac_arg))) + + for name, command in commands: + print_command(name, command) + if args.dry_run: + return 0 + + env = os.environ.copy() + processes: list[subprocess.Popen] = [] + + def handle_signal(signum, _frame): + print(f"\n收到信号 {signum},正在停止仿真栈...") + terminate_processes(processes) + raise SystemExit(128 + signum) + + signal.signal(signal.SIGINT, handle_signal) + signal.signal(signal.SIGTERM, handle_signal) + + try: + for name, command in commands: + print(f"[*] 启动 {name}") + processes.append(subprocess.Popen(command, cwd=PROJECT_ROOT, env=env)) + time.sleep(0.8) + + while processes: + for process in processes: + return_code = process.poll() + if return_code is not None: + print(f"[WARN] 子进程退出: pid={process.pid}, return_code={return_code}") + terminate_processes(processes) + return return_code + time.sleep(0.5) + finally: + terminate_processes(processes) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/agv_calib_brain/src/simulation/tools/sim_profile.py b/agv_calib_brain/src/simulation/tools/sim_profile.py new file mode 100755 index 0000000..ca88f07 --- /dev/null +++ b/agv_calib_brain/src/simulation/tools/sim_profile.py @@ -0,0 +1,98 @@ +#!/usr/bin/env python3 +"""Shared helpers for simulation profiles.""" + +from __future__ import annotations + +from pathlib import Path +from typing import Any + +import yaml + + +PROJECT_ROOT = Path(__file__).resolve().parents[3] +DEFAULT_PROFILE = PROJECT_ROOT / "src" / "deployment" / "profiles" / "sim_workshop.yaml" + + +def load_profile(profile_path: str | Path | None = None) -> dict[str, Any]: + path = Path(profile_path).expanduser().resolve() if profile_path else DEFAULT_PROFILE + if not path.exists(): + raise FileNotFoundError(f"未找到仿真 profile: {path}") + + with path.open("r", encoding="utf-8") as fp: + profile = yaml.safe_load(fp) or {} + + if not isinstance(profile, dict): + raise ValueError(f"profile 顶层必须是 dict: {path}") + if profile.get("mode") != "sim": + raise ValueError(f"profile mode 必须是 sim,当前为: {profile.get('mode')!r}") + return profile + + +def get_nested(data: dict[str, Any], path: str, default: Any = None) -> Any: + current: Any = data + for part in path.split("."): + if not isinstance(current, dict) or part not in current: + return default + current = current[part] + return current + + +def require_nested(data: dict[str, Any], path: str) -> Any: + value = get_nested(data, path) + if value is None: + raise KeyError(f"profile 缺少字段: {path}") + return value + + +def repo_path(value: str | Path) -> Path: + path = Path(value).expanduser() + return path if path.is_absolute() else PROJECT_ROOT / path + + +def as_int(value: Any, field_name: str) -> int: + try: + return int(value) + except (TypeError, ValueError) as exc: + raise ValueError(f"{field_name} 必须是整数,当前为: {value!r}") from exc + + +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 validate_sim_profile(profile: dict[str, Any]) -> None: + required_fields = [ + "vehicle_id", + "workshop_pc.gateway.vehicle_host", + "workshop_pc.gateway.vehicle_port", + "vehicle_agent.private_cmd_vel_topic", + "isaac.scene_script", + "isaac.scene_manifest_path", + "isaac.cmd_vel_topic", + "safety.max_speed_mps", + ] + for field in required_fields: + require_nested(profile, field) + + scene_script = repo_path(require_nested(profile, "isaac.scene_script")) + if not scene_script.exists(): + raise FileNotFoundError(f"Isaac 场景脚本不存在: {scene_script}") + + agent_topic = require_nested(profile, "vehicle_agent.private_cmd_vel_topic") + isaac_topic = require_nested(profile, "isaac.cmd_vel_topic") + if agent_topic != isaac_topic: + raise ValueError( + "vehicle_agent.private_cmd_vel_topic 必须与 isaac.cmd_vel_topic 一致," + f"当前为 {agent_topic!r} vs {isaac_topic!r}" + ) + + vehicle_port = as_int(require_nested(profile, "workshop_pc.gateway.vehicle_port"), "vehicle_port") + if not 1 <= vehicle_port <= 65535: + raise ValueError(f"vehicle_port 超出端口范围: {vehicle_port}") + + max_speed = as_float(require_nested(profile, "safety.max_speed_mps"), "safety.max_speed_mps") + if max_speed <= 0.0: + raise ValueError("safety.max_speed_mps 必须大于 0") diff --git a/agv_calib_brain/src/simulation/tools/smoke_test_vehicle_agent.py b/agv_calib_brain/src/simulation/tools/smoke_test_vehicle_agent.py new file mode 100755 index 0000000..1eee39e --- /dev/null +++ b/agv_calib_brain/src/simulation/tools/smoke_test_vehicle_agent.py @@ -0,0 +1,354 @@ +#!/usr/bin/env python3 +"""TCP smoke test for the simulated vehicle-side agent.""" + +from __future__ import annotations + +import argparse +import json +import socket +import struct +import sys +import threading +import time +from typing import Any + +from sim_profile import as_float, as_int, get_nested, load_profile, validate_sim_profile + + +CHASSIS_GET_READINESS_REQ = 1 +CHASSIS_GET_READINESS_RSP = 2 +CHASSIS_MOTION_PRIMITIVE_REQ = 3 +CHASSIS_MOTION_PRIMITIVE_RSP = 4 +CHASSIS_EMERGENCY_BRAKE_REQ = 5 +CHASSIS_EMERGENCY_BRAKE_RSP = 6 +CONTROL_GET_READINESS_REQ = 11 +CONTROL_GET_READINESS_RSP = 12 +CONTROL_EVALUATION_REQ = 13 +CONTROL_EVALUATION_RSP = 14 +EXTERNAL_POSE_PUSH_REQ = 31 +EXTERNAL_POSE_PUSH_RSP = 32 + + +def now_us() -> int: + return int(time.time() * 1_000_000) + + +def recv_exactly(conn: socket.socket, size: int) -> bytes: + data = b"" + while len(data) < size: + chunk = conn.recv(size - len(data)) + if not chunk: + raise ConnectionError("连接提前关闭") + data += chunk + return data + + +def send_request(host: str, port: int, msg_type: int, payload: dict[str, Any], timeout_sec: float) -> tuple[int, dict[str, Any]]: + encoded = json.dumps(payload, ensure_ascii=False, separators=(",", ":")).encode("utf-8") + with socket.create_connection((host, port), timeout=timeout_sec) as conn: + conn.settimeout(timeout_sec) + conn.sendall(struct.pack(" None: + if actual_type != expected_type: + raise AssertionError(f"{name}: 响应类型错误,期望 {expected_type},实际 {actual_type}") + if not payload.get("success", False): + raise AssertionError(f"{name}: success=false, payload={payload}") + print(f"[PASS] {name}: {payload.get('message', '')}") + + +def parse_args() -> argparse.Namespace: + parser = argparse.ArgumentParser(description="仿真车端 TCP 接口快速验收") + parser.add_argument("--profile", default="", help="仿真 profile 路径") + parser.add_argument("--host", default="", help="覆盖 profile 中的车端 host") + parser.add_argument("--timeout-sec", type=float, default=5.0) + parser.add_argument("--motion", action="store_true", help="发送一个小距离直线动作,默认不动车") + parser.add_argument("--control-eval", action="store_true", help="发送一个短时速度阶跃运控评估任务") + parser.add_argument("--trajectory", action="store_true", help="发送一个短轨迹跟踪任务") + parser.add_argument("--split-trajectory", action="store_true", help="把短轨迹拆成两段下发,验证分段缓存和最终拼接执行") + parser.add_argument("--fake-telemetry", action="store_true", help="发布一段假的外部真值和底盘遥测,用于无 Isaac 时测试轨迹接口") + parser.add_argument("--distance-m", type=float, default=0.20) + parser.add_argument("--speed-mps", type=float, default=0.10) + parser.add_argument("--skip-brake", action="store_true", help="跳过急停接口测试") + return parser.parse_args() + + +def start_fake_vehicle_feedback( + external_pose_topic: str, + chassis_topic: str, + x_m: float, + y_m: float, + yaw_rad: float, + external_pose_tcp_host: str = "", + external_pose_tcp_port: int = 0, + external_pose_tcp_timeout_sec: float = 0.2, +) -> tuple[threading.Event, threading.Thread]: + import rclpy + from calibration_chassis_interfaces.msg import ChassisTelemetry + from calibration_external_localization_interfaces.msg import ExternalLocalizationTelemetry + + stop_event = threading.Event() + + def run() -> None: + if not rclpy.ok(): + rclpy.init(args=None) + node = rclpy.create_node("vehicle_agent_smoke_fake_vehicle_feedback") + external_publisher = node.create_publisher(ExternalLocalizationTelemetry, external_pose_topic, 10) + chassis_publisher = node.create_publisher(ChassisTelemetry, chassis_topic, 10) + try: + while not stop_event.is_set(): + timestamp_us = now_us() + + pose_msg = ExternalLocalizationTelemetry() + pose_msg.hardware_timestamp_us = timestamp_us + pose_msg.pose_valid = True + pose_msg.workshop_pose.x_m = x_m + pose_msg.workshop_pose.y_m = y_m + pose_msg.workshop_pose.z_m = 0.0 + pose_msg.workshop_pose.yaw_rad = yaw_rad + pose_msg.position_stddev_m = 0.001 + pose_msg.yaw_stddev_rad = 0.001 + pose_msg.tracking_loss_ratio = 0.0 + pose_msg.time_sync_offset_ms = 1.0 + pose_msg.quality_score = 1.0 + pose_msg.observed_target_count = 4 + pose_msg.reference_source_name = "smoke_fake_external_truth" + pose_msg.active_job_id = "vehicle_agent_smoke_fake_pose" + external_publisher.publish(pose_msg) + if external_pose_tcp_host and external_pose_tcp_port > 0: + try: + send_request( + external_pose_tcp_host, + external_pose_tcp_port, + EXTERNAL_POSE_PUSH_REQ, + { + "hardware_timestamp_us": timestamp_us, + "pose_valid": True, + "workshop_pose": { + "x_m": x_m, + "y_m": y_m, + "z_m": 0.0, + "roll_rad": 0.0, + "pitch_rad": 0.0, + "yaw_rad": yaw_rad, + }, + "position_stddev_m": 0.001, + "yaw_stddev_rad": 0.001, + "tracking_loss_ratio": 0.0, + "time_sync_offset_ms": 1.0, + "quality_score": 1.0, + "observed_target_count": 4, + "reference_source_name": "smoke_fake_external_truth", + "active_job_id": "vehicle_agent_smoke_fake_pose", + }, + external_pose_tcp_timeout_sec, + ) + except Exception: + pass + + chassis_msg = ChassisTelemetry() + chassis_msg.hardware_timestamp_us = timestamp_us + chassis_msg.odom_x_m = x_m + chassis_msg.odom_y_m = y_m + chassis_msg.odom_yaw_rad = yaw_rad + chassis_msg.linear_velocity_ms = 0.0 + chassis_msg.angular_velocity_rads = 0.0 + chassis_msg.estop_engaged = False + chassis_msg.driver_error_code = 0 + chassis_msg.active_job_id = "vehicle_agent_smoke_fake_velocity" + chassis_publisher.publish(chassis_msg) + + rclpy.spin_once(node, timeout_sec=0.0) + time.sleep(0.05) + finally: + node.destroy_node() + if rclpy.ok(): + rclpy.shutdown() + + thread = threading.Thread(target=run, daemon=True) + thread.start() + return stop_event, thread + + +def main() -> int: + args = parse_args() + profile = load_profile(args.profile or None) + validate_sim_profile(profile) + + gateway = profile["workshop_pc"]["gateway"] + host = args.host or str(gateway.get("vehicle_host", gateway["chassis_host"])) + vehicle_port = as_int( + gateway.get("vehicle_port", gateway.get("chassis_port", 9000)), + "workshop_pc.gateway.vehicle_port", + ) + vehicle_id = str(profile["vehicle_id"]) + max_speed = as_float(get_nested(profile, "safety.max_speed_mps", 0.3), "safety.max_speed_mps") + speed = min(abs(args.speed_mps), max_speed) + external_pose_topic = str(get_nested(profile, "vehicle_agent.external_pose_topic", "/isaac/external_localization/telemetry")) + external_pose_transport = str(get_nested(profile, "vehicle_agent.external_pose_transport", "both")) + external_pose_tcp_host = str(gateway.get("vehicle_host", gateway.get("control_host", "127.0.0.1"))) + external_pose_tcp_port = as_int(gateway.get("vehicle_port", 9000), "workshop_pc.gateway.vehicle_port") + chassis_topic = str(get_nested(profile, "vehicle_agent.chassis_telemetry_topic", "/chassis/telemetry")) + fake_stop = None + fake_thread = None + + if args.trajectory and args.fake_telemetry: + fake_stop, fake_thread = start_fake_vehicle_feedback( + external_pose_topic, + chassis_topic, + x_m=abs(args.distance_m), + y_m=0.0, + yaw_rad=0.0, + external_pose_tcp_host=external_pose_tcp_host if external_pose_transport in ("wifi6_tcp", "both") else "", + external_pose_tcp_port=external_pose_tcp_port if external_pose_transport in ("wifi6_tcp", "both") else 0, + ) + time.sleep(0.2) + + try: + chassis_req = { + "agent_name": "chassis", + "include_details": True, + "target_resource_ids": [], + } + rsp_type, payload = send_request(host, vehicle_port, CHASSIS_GET_READINESS_REQ, chassis_req, args.timeout_sec) + assert_success("chassis readiness", CHASSIS_GET_READINESS_RSP, rsp_type, payload) + + control_req = { + "agent_name": "control", + "include_details": True, + "target_resource_ids": [], + } + rsp_type, payload = send_request(host, vehicle_port, CONTROL_GET_READINESS_REQ, control_req, args.timeout_sec) + assert_success("control readiness", CONTROL_GET_READINESS_RSP, rsp_type, payload) + + if args.motion: + request_id = f"sim_smoke_straight_{now_us()}" + motion_req = { + "header": { + "session_id": f"sim_smoke_{now_us()}", + "task_id": "sim_smoke_straight", + "vehicle_id": vehicle_id, + "request_id": request_id, + "client_send_timestamp_us": now_us(), + "operator_id": "sim_smoke", + "workshop_host": "simulation", + }, + "test_case_id": "smoke_straight_line", + "task_purpose": 1, + "selected_primitive": 1, + "brake_when_finished": True, + "timeout_sec": max(3.0, args.distance_m / max(speed, 0.01) + 2.0), + "source_iteration_id": "smoke_iter_001", + "straight_line": { + "target_speed_ms": speed, + "target_distance_m": abs(args.distance_m), + "reverse": False, + }, + } + rsp_type, payload = send_request(host, vehicle_port, CHASSIS_MOTION_PRIMITIVE_REQ, motion_req, args.timeout_sec + 10.0) + assert_success("chassis straight motion", CHASSIS_MOTION_PRIMITIVE_RSP, rsp_type, payload) + + if args.control_eval: + request_id = f"sim_smoke_control_{now_us()}" + control_req = { + "header": { + "session_id": f"sim_smoke_{now_us()}", + "task_id": "sim_smoke_velocity_step", + "vehicle_id": vehicle_id, + "request_id": request_id, + "client_send_timestamp_us": now_us(), + "operator_id": "sim_smoke", + "workshop_host": "simulation", + }, + "test_case_id": "smoke_velocity_step", + "task_purpose": 1, + "selected_task": 2, + "source_iteration_id": "smoke_iter_001", + "velocity_step": { + "target_velocity_ms": speed, + "hold_time_sec": 0.5, + "settle_before_step_sec": 0.1, + }, + } + rsp_type, payload = send_request(host, vehicle_port, CONTROL_EVALUATION_REQ, control_req, args.timeout_sec + 10.0) + assert_success("control velocity step", CONTROL_EVALUATION_RSP, rsp_type, payload) + + if args.trajectory: + request_id = f"sim_smoke_trajectory_{now_us()}" + path = [ + {"x_m": 0.0, "y_m": 0.0, "yaw_rad": 0.0, "target_speed_ms": speed}, + {"x_m": abs(args.distance_m), "y_m": 0.0, "yaw_rad": 0.0, "target_speed_ms": speed}, + ] + + def make_trajectory_request(segment_path: list[dict[str, float]], segment_index: int, total_segments: int, is_final: bool) -> dict[str, Any]: + return { + "header": { + "session_id": f"sim_smoke_{now_us()}", + "task_id": "sim_smoke_trajectory", + "vehicle_id": vehicle_id, + "request_id": f"{request_id}_seg{segment_index}" if total_segments > 1 else request_id, + "client_send_timestamp_us": now_us(), + "operator_id": "sim_smoke", + "workshop_host": "simulation", + }, + "test_case_id": "smoke_trajectory_tracking", + "task_purpose": 1, + "selected_task": 1, + "source_iteration_id": "smoke_iter_001", + "trajectory_tracking": { + "path": segment_path, + "stop_at_end": True, + "timeout_sec": max(3.0, args.distance_m / max(speed, 0.01) + 2.0), + "required_external_pose_source_id": "smoke_fake_external_truth" if args.fake_telemetry else "", + "max_external_pose_age_ms": 500.0 if args.fake_telemetry else 0.0, + "min_external_pose_quality_score": 0.1, + "trajectory_id": request_id, + "segment_index": segment_index, + "total_segments": total_segments, + "is_final_segment": is_final, + }, + } + + if args.split_trajectory: + segments = [path[:1], path[1:]] + for segment_index, segment_path in enumerate(segments): + is_final = segment_index == len(segments) - 1 + control_req = make_trajectory_request(segment_path, segment_index, len(segments), is_final) + rsp_type, payload = send_request( + host, + vehicle_port, + CONTROL_EVALUATION_REQ, + control_req, + args.timeout_sec + 10.0, + ) + name = "control trajectory tracking" if is_final else f"control trajectory segment {segment_index} upload" + assert_success(name, CONTROL_EVALUATION_RSP, rsp_type, payload) + else: + control_req = make_trajectory_request(path, 0, 1, True) + rsp_type, payload = send_request(host, vehicle_port, CONTROL_EVALUATION_REQ, control_req, args.timeout_sec + 10.0) + assert_success("control trajectory tracking", CONTROL_EVALUATION_RSP, rsp_type, payload) + + if not args.skip_brake: + rsp_type, payload = send_request(host, vehicle_port, CHASSIS_EMERGENCY_BRAKE_REQ, {}, args.timeout_sec) + assert_success("chassis emergency brake", CHASSIS_EMERGENCY_BRAKE_RSP, rsp_type, payload) + finally: + if fake_stop is not None: + fake_stop.set() + if fake_thread is not None: + fake_thread.join(timeout=2.0) + + print("[PASS] vehicle agent TCP smoke test completed.") + return 0 + + +if __name__ == "__main__": + try: + raise SystemExit(main()) + except Exception as exc: + print(f"[FAIL] {exc}", file=sys.stderr) + raise SystemExit(1) diff --git a/agv_calib_brain/src/simulation/tools/smoke_test_vehicle_sensor_agent.py b/agv_calib_brain/src/simulation/tools/smoke_test_vehicle_sensor_agent.py new file mode 100644 index 0000000..6ebf0c2 --- /dev/null +++ b/agv_calib_brain/src/simulation/tools/smoke_test_vehicle_sensor_agent.py @@ -0,0 +1,239 @@ +#!/usr/bin/env python3 +"""TCP smoke test for the simulated vehicle-side sensor agent.""" + +from __future__ import annotations + +import argparse +import json +import socket +import struct +import sys +import threading +import time +from typing import Any + +from sim_profile import as_int, get_nested, load_profile, validate_sim_profile + + +SENSOR_GET_READINESS_REQ = 21 +SENSOR_GET_READINESS_RSP = 22 +SENSOR_GET_LATEST_FRAME_REQ = 23 +SENSOR_GET_LATEST_FRAME_RSP = 24 +SENSOR_LIST_SENSORS_REQ = 25 +SENSOR_LIST_SENSORS_RSP = 26 + + +def now_us() -> int: + return int(time.time() * 1_000_000) + + +def recv_exactly(conn: socket.socket, size: int) -> bytes: + data = b"" + while len(data) < size: + chunk = conn.recv(size - len(data)) + if not chunk: + raise ConnectionError("连接提前关闭") + data += chunk + return data + + +def send_request(host: str, port: int, msg_type: int, payload: dict[str, Any], timeout_sec: float) -> tuple[int, dict[str, Any]]: + encoded = json.dumps(payload, ensure_ascii=False, separators=(",", ":")).encode("utf-8") + with socket.create_connection((host, port), timeout=timeout_sec) as conn: + conn.settimeout(timeout_sec) + conn.sendall(struct.pack(" None: + if actual_type != expected_type: + raise AssertionError(f"{name}: 响应类型错误,期望 {expected_type},实际 {actual_type}") + if not payload.get("success", False): + raise AssertionError(f"{name}: success=false, payload={payload}") + print(f"[PASS] {name}: {payload.get('message', '')}") + + +def parse_args() -> argparse.Namespace: + parser = argparse.ArgumentParser(description="仿真车端传感器 TCP 接口快速验收") + parser.add_argument("--profile", default="", help="仿真 profile 路径") + parser.add_argument("--host", default="", help="覆盖 profile 中的车端 host") + parser.add_argument("--timeout-sec", type=float, default=5.0) + parser.add_argument("--fake-sensors", action="store_true", help="发布假的图像、点云、LaserScan、IMU 数据") + parser.add_argument("--hold-fake-sensors-sec", type=float, default=0.0, help="验收完成后继续发布假传感器数据的秒数") + parser.add_argument("--max-payload-bytes", type=int, default=256) + return parser.parse_args() + + +def start_fake_sensors(topics: dict[str, str]) -> tuple[threading.Event, threading.Thread]: + import rclpy + from sensor_msgs.msg import Image, Imu, LaserScan, PointCloud2, PointField + + stop_event = threading.Event() + + def stamp_msg(msg: Any, frame_id: str) -> None: + msg.header.frame_id = frame_id + now_ns = time.time_ns() + msg.header.stamp.sec = now_ns // 1_000_000_000 + msg.header.stamp.nanosec = now_ns % 1_000_000_000 + + def run() -> None: + if not rclpy.ok(): + rclpy.init(args=None) + node = rclpy.create_node("vehicle_sensor_agent_smoke_fake_sensors") + front_pub = node.create_publisher(Image, topics["front_camera"], 10) + down_pub = node.create_publisher(Image, topics["down_camera"], 10) + cloud_pub = node.create_publisher(PointCloud2, topics["lidar_3d"], 10) + scan_pub = node.create_publisher(LaserScan, topics["lidar_2d"], 10) + imu_pub = node.create_publisher(Imu, topics["imu"], 10) + + image_data = bytes([0, 32, 64, 96, 128, 160, 192, 255] * 6) + point_data = bytes([0, 0, 0, 0, 0, 0, 128, 63, 0, 0, 0, 64, 0, 0, 64, 64] * 4) + try: + while not stop_event.is_set(): + front = Image() + stamp_msg(front, "front_camera_link") + front.width = 4 + front.height = 4 + front.encoding = "rgb8" + front.is_bigendian = 0 + front.step = 12 + front.data = image_data + front_pub.publish(front) + + down = Image() + stamp_msg(down, "down_camera_link") + down.width = 4 + down.height = 4 + down.encoding = "mono8" + down.is_bigendian = 0 + down.step = 4 + down.data = image_data[:16] + down_pub.publish(down) + + cloud = PointCloud2() + stamp_msg(cloud, "lidar_3d_link") + cloud.height = 1 + cloud.width = 4 + cloud.fields = [ + PointField(name="x", offset=0, datatype=PointField.FLOAT32, count=1), + PointField(name="y", offset=4, datatype=PointField.FLOAT32, count=1), + PointField(name="z", offset=8, datatype=PointField.FLOAT32, count=1), + PointField(name="intensity", offset=12, datatype=PointField.FLOAT32, count=1), + ] + cloud.is_bigendian = False + cloud.point_step = 16 + cloud.row_step = 64 + cloud.is_dense = True + cloud.data = point_data + cloud_pub.publish(cloud) + + scan = LaserScan() + stamp_msg(scan, "lidar_2d_link") + scan.angle_min = -1.57 + scan.angle_max = 1.57 + scan.angle_increment = 0.785 + scan.time_increment = 0.0 + scan.scan_time = 0.1 + scan.range_min = 0.05 + scan.range_max = 20.0 + scan.ranges = [1.0, 1.2, 1.5, 1.2, 1.0] + scan.intensities = [10.0, 20.0, 30.0, 20.0, 10.0] + scan_pub.publish(scan) + + imu = Imu() + stamp_msg(imu, "imu_link") + imu.orientation.w = 1.0 + imu.angular_velocity.z = 0.01 + imu.linear_acceleration.z = 9.81 + imu_pub.publish(imu) + + rclpy.spin_once(node, timeout_sec=0.0) + time.sleep(0.05) + finally: + node.destroy_node() + if rclpy.ok(): + rclpy.shutdown() + + thread = threading.Thread(target=run, daemon=True) + thread.start() + return stop_event, thread + + +def main() -> int: + args = parse_args() + profile = load_profile(args.profile or None) + validate_sim_profile(profile) + + gateway = profile["workshop_pc"]["gateway"] + sensor_agent = profile.get("vehicle_sensor_agent", {}) + host = args.host or str(gateway.get("sensor_host", gateway.get("control_host", "127.0.0.1"))) + port = as_int( + gateway.get("vehicle_port", gateway.get("sensor_port", 9003)), + "workshop_pc.gateway.vehicle_port", + ) + topics = { + "front_camera": str(get_nested(profile, "vehicle_sensor_agent.front_camera_topic", "/sensor/front_camera/image_raw")), + "down_camera": str(get_nested(profile, "vehicle_sensor_agent.down_camera_topic", "/sensor/down_camera/image_raw")), + "lidar_3d": str(get_nested(profile, "vehicle_sensor_agent.lidar_3d_topic", "/sensor/lidar_3d/pointcloud")), + "lidar_2d": str(get_nested(profile, "vehicle_sensor_agent.lidar_2d_topic", "/sensor/lidar_2d/scan")), + "imu": str(get_nested(profile, "vehicle_sensor_agent.imu_topic", "/sensor/imu/data")), + } + sensor_ids = [ + str(sensor_agent.get("front_camera_sensor_id", "demo_front_camera")), + str(sensor_agent.get("down_camera_sensor_id", "demo_down_camera")), + str(sensor_agent.get("lidar_3d_sensor_id", "demo_lidar_3d")), + str(sensor_agent.get("lidar_2d_sensor_id", "demo_lidar_2d")), + str(sensor_agent.get("imu_sensor_id", "demo_imu")), + ] + + fake_stop = None + fake_thread = None + if args.fake_sensors: + fake_stop, fake_thread = start_fake_sensors(topics) + time.sleep(0.5) + + try: + rsp_type, payload = send_request(host, port, SENSOR_LIST_SENSORS_REQ, {}, args.timeout_sec) + assert_success("sensor list", SENSOR_LIST_SENSORS_RSP, rsp_type, payload) + + rsp_type, payload = send_request(host, port, SENSOR_GET_READINESS_REQ, {}, args.timeout_sec) + assert_success("sensor readiness", SENSOR_GET_READINESS_RSP, rsp_type, payload) + + for sensor_id in sensor_ids: + rsp_type, payload = send_request( + host, + port, + SENSOR_GET_LATEST_FRAME_REQ, + { + "sensor_id": sensor_id, + "include_payload": True, + "max_payload_bytes": args.max_payload_bytes, + "fragment_index": 0, + }, + args.timeout_sec, + ) + assert_success(f"latest frame {sensor_id}", SENSOR_GET_LATEST_FRAME_RSP, rsp_type, payload) + frame = payload.get("sensor_frame", {}) + if not frame.get("header", {}).get("sequence_id", 0): + raise AssertionError(f"{sensor_id}: sequence_id missing in payload={payload}") + if args.fake_sensors and args.hold_fake_sensors_sec > 0.0: + time.sleep(args.hold_fake_sensors_sec) + finally: + if fake_stop is not None: + fake_stop.set() + if fake_thread is not None: + fake_thread.join(timeout=2.0) + + print("[PASS] vehicle sensor agent TCP smoke test completed.") + return 0 + + +if __name__ == "__main__": + try: + raise SystemExit(main()) + except Exception as exc: + print(f"[FAIL] {exc}", file=sys.stderr) + raise SystemExit(1) diff --git a/agv_calib_brain/src/simulation/tools/smoke_test_workshop_orchestrator.py b/agv_calib_brain/src/simulation/tools/smoke_test_workshop_orchestrator.py new file mode 100644 index 0000000..9275abd --- /dev/null +++ b/agv_calib_brain/src/simulation/tools/smoke_test_workshop_orchestrator.py @@ -0,0 +1,617 @@ +#!/usr/bin/env python3 +"""车间总控端到端验收脚本。 + +使用前先启动仿真/本地 demo 栈。本脚本负责: +1. 注册一份最小车辆画像; +2. 创建 workshop session; +3. 调用 /workshop_v2/execute_session action; +4. 检查最终报告和阶段结果。 + +默认验收到传感器内参链路;传感器外参需要有效观测帧,等观测数据链路就绪后可通过 +--tasks external,chassis,control,sensor_intrinsic,sensor_extrinsic 显式加入。 +""" + +from __future__ import annotations + +import argparse +import os +import sys +import time +from pathlib import Path +from typing import Iterable + +try: + import rclpy + from rclpy.action import ActionClient +except ImportError as exc: # pragma: no cover - 只在未 source ROS 环境时触发 + print(f"[FAIL] 无法导入 ROS2 Python 模块: {exc}", file=sys.stderr) + print("请先执行: source install/setup.bash", file=sys.stderr) + raise + +from calibration_common_interfaces.msg import KeyValuePair, RequestHeader +from calibration_external_localization_interfaces.msg import ExternalLocalizationTelemetry +from rcl_interfaces.msg import Parameter, ParameterType, ParameterValue +from rcl_interfaces.srv import SetParameters +from calibration_vehicle_profile_interfaces.msg import ( + CalibrationAbilityType, + CalibrationCapability, + CameraMountType, + ChassisType, + ControlAxisType, + ControllerAlgorithmType, + ControllerProfile, + ControllerSelection, + SensorProfile, + SensorType, + VehicleProfile, + WorkflowStageType, +) +from calibration_workshop_orchestration_interfaces.action import ExecuteWorkshopSession +from calibration_workshop_orchestration_interfaces.msg import ( + RequestedCalibrationTask, + StageExecutionPolicy, + WorkshopOperatorInfo, + WorkshopSessionConfig, +) +from calibration_workshop_orchestration_interfaces.srv import ( + CreateWorkshopSession, + GetWorkshopReport, + GetWorkshopSession, +) +from calibration_vehicle_profile_interfaces.srv import RegisterOrUpdateVehicleProfile + + +TASK_STAGE_TYPES = { + "external": WorkflowStageType.EXTERNAL_REFERENCE_READY_CHECK_STAGE, + "chassis": WorkflowStageType.CHASSIS_CALIBRATION_STAGE, + "control": WorkflowStageType.CONTROL_CALIBRATION_STAGE, + "sensor_intrinsic": WorkflowStageType.SENSOR_INTRINSIC_CALIBRATION_STAGE, + "sensor_extrinsic": WorkflowStageType.SENSOR_EXTRINSIC_CALIBRATION_STAGE, + "hand_eye": WorkflowStageType.HAND_EYE_CALIBRATION_STAGE, +} + +DEFAULT_TASKS = "external,chassis,control,sensor_intrinsic" + + +def now_us() -> int: + return int(time.time() * 1_000_000) + + +def make_header(vehicle_id: str, task_id: str, operator_id: str) -> RequestHeader: + header = RequestHeader() + header.session_id = f"orchestrator_smoke_{now_us()}" + header.task_id = task_id + header.vehicle_id = vehicle_id + header.request_id = f"{task_id}_{now_us()}" + header.client_send_timestamp_us = now_us() + header.operator_id = operator_id + header.workshop_host = "smoke_test_workshop_orchestrator" + return header + + +def kv(key: str, value: str) -> KeyValuePair: + item = KeyValuePair() + item.key = key + item.value = value + return item + + +def make_capability(ability: int, message: str) -> CalibrationCapability: + cap = CalibrationCapability() + cap.ability_type.value = ability + cap.supported = True + cap.message = message + return cap + + +def make_controller_profile(axis: int, default_algorithm: int) -> ControllerProfile: + profile = ControllerProfile() + profile.control_axis.value = axis + for algorithm in ( + ControllerAlgorithmType.PID, + ControllerAlgorithmType.MPC, + ControllerAlgorithmType.LQR, + ControllerAlgorithmType.PURE_PURSUIT, + ): + item = ControllerAlgorithmType() + item.value = algorithm + profile.supported_algorithms.append(item) + profile.default_algorithm.value = default_algorithm + return profile + + +def make_controller_selection(axis: int, algorithm: int, is_default: bool) -> ControllerSelection: + selection = ControllerSelection() + selection.control_axis.value = axis + selection.algorithm_type.value = algorithm + selection.enabled = True + selection.need_tuning = True + selection.is_default = is_default + return selection + + +def make_sensor(sensor_id: str, sensor_type: int, mount_type: int, name: str) -> SensorProfile: + sensor = SensorProfile() + sensor.sensor_id = sensor_id + sensor.sensor_type.value = sensor_type + sensor.sensor_name = name + sensor.frame_id = f"{sensor_id}_frame" + sensor.camera_mount_type.value = mount_type + sensor.enabled = True + sensor.needs_intrinsic_calibration = sensor_type in ( + SensorType.FRONT_CAMERA, + SensorType.DOWNWARD_CAMERA, + SensorType.IMU, + ) + sensor.needs_extrinsic_calibration = True + sensor.device_hint = f"/workshop/vehicle_sensor/{sensor_id}" + sensor.selected_for_this_session = sensor_id == "demo_front_camera" + sensor.supports_intrinsic_calibration = sensor.needs_intrinsic_calibration + sensor.supports_extrinsic_calibration = True + return sensor + + +def make_vehicle_profile(vehicle_id: str) -> VehicleProfile: + profile = VehicleProfile() + profile.base_info.vehicle_id = vehicle_id + profile.base_info.vehicle_name = "仿真阿克曼小车" + profile.base_info.model_name = "ackermann_sim" + profile.base_info.serial_number = f"{vehicle_id}_serial" + profile.base_info.manufacturer = "AutoCalib Workshop" + profile.base_info.description = "orchestrator e2e smoke profile" + profile.chassis_type.value = ChassisType.ACKERMANN + profile.base_link_frame = "base_link" + profile.profile_version = "orchestrator_smoke_v1" + + profile.capabilities.extend([ + make_capability(CalibrationAbilityType.EXTERNAL_LOCALIZATION_CALIBRATION, "支持外部真值接入"), + make_capability(CalibrationAbilityType.CHASSIS_CALIBRATION, "支持底盘标定"), + make_capability(CalibrationAbilityType.CONTROL_CALIBRATION, "支持运控参数标定"), + make_capability(CalibrationAbilityType.SENSOR_CALIBRATION, "支持传感器标定"), + ]) + + profile.sensors.extend([ + make_sensor("demo_front_camera", SensorType.FRONT_CAMERA, CameraMountType.FRONT_MOUNTED, "前视相机"), + make_sensor("demo_down_camera", SensorType.DOWNWARD_CAMERA, CameraMountType.DOWNWARD_MOUNTED, "下视相机"), + make_sensor("demo_lidar_3d", SensorType.LIDAR_3D, CameraMountType.CAMERA_MOUNT_TYPE_UNSPECIFIED, "3D 激光雷达"), + make_sensor("demo_lidar_2d", SensorType.LIDAR_2D, CameraMountType.CAMERA_MOUNT_TYPE_UNSPECIFIED, "2D 激光雷达"), + make_sensor("demo_imu", SensorType.IMU, CameraMountType.CAMERA_MOUNT_TYPE_UNSPECIFIED, "IMU"), + ]) + + profile.controllers.extend([ + make_controller_profile(ControlAxisType.LATERAL_CONTROL, ControllerAlgorithmType.MPC), + make_controller_profile(ControlAxisType.LONGITUDINAL_CONTROL, ControllerAlgorithmType.PID), + ]) + profile.controller_selections.extend([ + make_controller_selection(ControlAxisType.LATERAL_CONTROL, ControllerAlgorithmType.MPC, True), + make_controller_selection(ControlAxisType.LONGITUDINAL_CONTROL, ControllerAlgorithmType.PID, True), + ]) + + for stage_value in ( + WorkflowStageType.EXTERNAL_REFERENCE_READY_CHECK_STAGE, + WorkflowStageType.CHASSIS_CALIBRATION_STAGE, + WorkflowStageType.CONTROL_CALIBRATION_STAGE, + WorkflowStageType.SENSOR_INTRINSIC_CALIBRATION_STAGE, + WorkflowStageType.SENSOR_EXTRINSIC_CALIBRATION_STAGE, + ): + stage = WorkflowStageType() + stage.value = stage_value + profile.enabled_workflow_stages.append(stage) + + return profile + + +def parse_tasks(raw: str) -> list[str]: + if raw.strip().lower() == "all": + raw = DEFAULT_TASKS + tasks = [] + for item in raw.split(","): + name = item.strip() + if not name: + continue + if name not in TASK_STAGE_TYPES: + raise ValueError(f"不支持的任务名: {name},可选: {', '.join(TASK_STAGE_TYPES)}") + tasks.append(name) + if not tasks: + raise ValueError("至少需要指定一个任务") + return tasks + + +def task_params(task_name: str, args: argparse.Namespace) -> list[KeyValuePair]: + if task_name == "external": + return [ + kv("external.static_sample_count", "1"), + kv("external.dynamic_sample_count", "1"), + kv("external.require_short_motion_segment", "false"), + kv("external.max_position_stddev_m", "0.05"), + kv("external.max_yaw_stddev_rad", "0.05"), + kv("external.max_tracking_loss_ratio", "0.05"), + kv("external.max_time_sync_offset_ms", "50.0"), + kv("external.timeout_sec", "5.0"), + ] + if task_name == "chassis": + return [ + kv("primitive_type", "straight_line"), + kv("straight_line.target_distance_m", str(args.chassis_distance_m)), + kv("straight_line.target_speed_ms", str(args.chassis_speed_mps)), + kv("straight_line.reverse", "false"), + kv("brake_when_finished", "true"), + kv("timeout_sec", "10.0"), + ] + if task_name == "control": + return [ + kv("control.task_type", "velocity_step"), + kv("velocity_step.target_velocity_ms", str(args.control_velocity_mps)), + kv("velocity_step.hold_time_sec", "0.5"), + kv("velocity_step.settle_before_step_sec", "0.1"), + kv("control.timeout_sec", "10.0"), + ] + if task_name == "sensor_intrinsic": + return [ + kv("sensor.sensor_id", "demo_front_camera"), + kv("sensor.task_subtype", "front_camera_intrinsic"), + kv("camera_intrinsic.required_image_count", "1"), + kv("camera_intrinsic.target_board_id", args.reference_target_id), + kv("camera_intrinsic.timeout_sec", "5.0"), + ] + if task_name == "sensor_extrinsic": + return [ + kv("sensor.sensor_id", "demo_front_camera"), + kv("sensor.task_subtype", "front_camera_extrinsic"), + kv("sensor_extrinsic.base_frame_id", "base_link"), + kv("sensor_extrinsic.required_sample_count", "1"), + kv("sensor_extrinsic.timeout_sec", "5.0"), + ] + if task_name == "hand_eye": + return [ + kv("sensor.sensor_id", "demo_front_camera"), + kv("sensor.task_subtype", "eye_in_hand"), + kv("hand_eye.arm_id", "demo_arm"), + kv("hand_eye.required_pose_count", "1"), + kv("hand_eye.timeout_sec", "5.0"), + ] + return [] + + +def make_requested_task(task_name: str, args: argparse.Namespace) -> RequestedCalibrationTask: + task = RequestedCalibrationTask() + task.stage_type.value = TASK_STAGE_TYPES[task_name] + task.enabled = True + task.require_manual_approval = False + task.execution_policy.value = StageExecutionPolicy.REQUIRED + task.reason = "orchestrator e2e smoke" + task.task_code = task_name + task.target_id = args.reference_target_id if task_name == "external" else "" + task.task_params.extend(task_params(task_name, args)) + return task + + +def make_session_config(tasks: Iterable[str], args: argparse.Namespace) -> WorkshopSessionConfig: + config = WorkshopSessionConfig() + config.auto_commit_parameters = False + config.require_manual_approval_before_commit = False + config.run_validation_after_each_stage = False + config.stop_on_first_failure = True + config.allow_optional_stage_skip = False + config.enable_auto_rollback_on_validation_failure = False + config.allow_rebuild_execution_plan = False + config.localization_source_id = args.reference_source_name + config.workcell_zone_id = args.workcell_zone_id + config.reference_target_id = args.reference_target_id + for task_name in tasks: + config.requested_tasks.append(make_requested_task(task_name, args)) + return config + + +class OrchestratorSmoke: + def __init__(self, args: argparse.Namespace) -> None: + self.args = args + self.node = rclpy.create_node("smoke_test_workshop_orchestrator") + self.feedback_messages: list[str] = [] + self.external_pub = self.node.create_publisher( + ExternalLocalizationTelemetry, + args.external_telemetry_topic, + 10, + ) + self.external_timer = None + if args.publish_fake_external_telemetry: + period = 1.0 / max(args.fake_external_hz, 1.0) + self.external_timer = self.node.create_timer(period, self.publish_external_telemetry) + + def destroy(self) -> None: + self.node.destroy_node() + + def publish_external_telemetry(self) -> None: + msg = ExternalLocalizationTelemetry() + msg.hardware_timestamp_us = now_us() + msg.pose_valid = True + msg.workshop_pose.x_m = 0.0 + msg.workshop_pose.y_m = 0.0 + msg.workshop_pose.z_m = 0.0 + msg.workshop_pose.roll_rad = 0.0 + msg.workshop_pose.pitch_rad = 0.0 + msg.workshop_pose.yaw_rad = 0.0 + msg.position_stddev_m = 0.001 + msg.yaw_stddev_rad = 0.001 + msg.tracking_loss_ratio = 0.0 + msg.time_sync_offset_ms = 1.0 + msg.quality_score = 1.0 + msg.observed_target_count = 4 + msg.reference_source_name = self.args.reference_source_name + msg.active_job_id = "orchestrator_e2e_smoke" + self.external_pub.publish(msg) + + def spin_for(self, seconds: float) -> None: + deadline = time.monotonic() + seconds + while time.monotonic() < deadline: + rclpy.spin_once(self.node, timeout_sec=0.05) + + def wait_for_service(self, client, name: str) -> None: + if not client.wait_for_service(timeout_sec=self.args.service_timeout_sec): + raise RuntimeError(f"服务不可用: {name}") + + def call_service(self, client, request, name: str): + self.wait_for_service(client, name) + future = client.call_async(request) + deadline = time.monotonic() + self.args.service_timeout_sec + while rclpy.ok() and not future.done() and time.monotonic() < deadline: + rclpy.spin_once(self.node, timeout_sec=0.05) + if not future.done(): + raise TimeoutError(f"等待服务响应超时: {name}") + return future.result() + + def maybe_disable_wifi6_precheck(self) -> None: + if not self.args.disable_wifi6_precheck: + return + node_name = self.args.orchestrator_node.strip("/") + client = self.node.create_client(SetParameters, f"/{node_name}/set_parameters") + request = SetParameters.Request() + item = Parameter() + item.name = "wifi6_precheck_enabled" + item.value = ParameterValue() + item.value.type = ParameterType.PARAMETER_BOOL + item.value.bool_value = False + request.parameters.append(item) + response = self.call_service(client, request, f"/{node_name}/set_parameters") + results = response.results + if not results or not results[0].successful: + reason = "" if not results else results[0].reason + raise RuntimeError(f"设置 wifi6_precheck_enabled=false 失败: {reason}") + print("[INFO] 已临时关闭 orchestrator WiFi6 预检。") + + def register_vehicle_profile(self, profile: VehicleProfile) -> None: + client = self.node.create_client( + RegisterOrUpdateVehicleProfile, + "/vehicle_profile_manager/register_or_update_vehicle_profile", + ) + request = RegisterOrUpdateVehicleProfile.Request() + request.request.header = make_header(self.args.vehicle_id, "register_profile", self.args.operator_id) + request.request.profile = profile + response = self.call_service( + client, + request, + "/vehicle_profile_manager/register_or_update_vehicle_profile", + ) + if not response.response.success: + raise RuntimeError(f"注册车辆画像失败: {response.response.message}") + print(f"[PASS] 已注册车辆画像: {self.args.vehicle_id}") + + def create_session(self, profile: VehicleProfile, tasks: list[str]) -> str: + client = self.node.create_client(CreateWorkshopSession, "/workshop_v2/create_session") + request = CreateWorkshopSession.Request() + request.request.header = make_header(self.args.vehicle_id, "create_session", self.args.operator_id) + request.request.vehicle_profile_snapshot = profile + request.request.operator_info = WorkshopOperatorInfo() + request.request.operator_info.operator_id = self.args.operator_id + request.request.operator_info.operator_name = "orchestrator smoke" + request.request.operator_info.workstation_id = self.args.workstation_id + request.request.operator_info.shift_id = "smoke" + request.request.config = make_session_config(tasks, self.args) + request.request.workshop_line_id = self.args.workshop_line_id + request.request.auto_build_execution_plan = True + response = self.call_service(client, request, "/workshop_v2/create_session") + if not response.response.success: + raise RuntimeError(f"创建 session 失败: {response.response.message}") + response_session_id = response.response.session_id + nested_session_id = response.response.session.session_id + session_id = response_session_id or nested_session_id + if not session_id: + raise RuntimeError( + "创建 session 成功但返回了空 session_id: " + f"message={response.response.message!r}, " + f"response_session_id={response_session_id!r}, " + f"nested_session_id={nested_session_id!r}, " + f"stage_count={len(response.response.session.stage_plan)}" + ) + stage_ids = [stage.stage_id for stage in response.response.session.stage_plan] + print(f"[PASS] 已创建 session: {session_id}") + if response_session_id != nested_session_id: + print( + "[WARN] create_session 返回的顶层 session_id 与 session 快照不一致: " + f"response={response_session_id!r}, nested={nested_session_id!r}" + ) + print(f"[INFO] 计划阶段: {', '.join(stage_ids)}") + return session_id + + def verify_session_visible(self, session_id: str) -> None: + client = self.node.create_client(GetWorkshopSession, "/workshop_v2/get_session") + request = GetWorkshopSession.Request() + request.request.header = make_header(self.args.vehicle_id, "get_session", self.args.operator_id) + request.request.session_id = session_id + response = self.call_service(client, request, "/workshop_v2/get_session") + if not response.response.success: + raise RuntimeError( + "创建后立即查询 session 失败,可能存在旧 orchestrator 节点或 service/action 连到不同实例: " + f"session_id={session_id!r}, message={response.response.message!r}" + ) + visible_id = response.response.session.session_id + if visible_id != session_id: + raise RuntimeError( + "get_session 返回的 session_id 与请求不一致: " + f"requested={session_id!r}, visible={visible_id!r}" + ) + print(f"[PASS] session 可查询: {session_id}") + + def feedback_cb(self, feedback_msg) -> None: + event = feedback_msg.feedback.feedback + text = event.message or f"event_type={event.event_type.value}" + if event.stage_id: + text = f"{event.stage_id}: {text}" + self.feedback_messages.append(text) + if self.args.verbose_feedback: + print(f"[FEEDBACK] {text}") + + def execute_session(self, session_id: str): + client = ActionClient(self.node, ExecuteWorkshopSession, "/workshop_v2/execute_session") + if not client.wait_for_server(timeout_sec=self.args.service_timeout_sec): + raise RuntimeError("action server 不可用: /workshop_v2/execute_session") + + goal = ExecuteWorkshopSession.Goal() + goal.goal.header = make_header(self.args.vehicle_id, "execute_session", self.args.operator_id) + goal.goal.header.session_id = session_id + goal.goal.session_id = session_id + send_future = client.send_goal_async(goal, feedback_callback=self.feedback_cb) + deadline = time.monotonic() + self.args.action_timeout_sec + while rclpy.ok() and not send_future.done() and time.monotonic() < deadline: + rclpy.spin_once(self.node, timeout_sec=0.05) + if not send_future.done(): + raise TimeoutError("发送 execute_session goal 超时") + + goal_handle = send_future.result() + if not goal_handle.accepted: + raise RuntimeError(f"orchestrator 拒绝 execute_session goal: session_id={session_id!r}") + print("[PASS] execute_session goal 已接受") + + result_future = goal_handle.get_result_async() + while rclpy.ok() and not result_future.done() and time.monotonic() < deadline: + rclpy.spin_once(self.node, timeout_sec=0.05) + if not result_future.done(): + raise TimeoutError("等待 execute_session 结果超时") + return result_future.result() + + def fetch_report(self, session_id: str): + client = self.node.create_client(GetWorkshopReport, "/workshop_v2/get_report") + request = GetWorkshopReport.Request() + request.request.header = make_header(self.args.vehicle_id, "get_report", self.args.operator_id) + request.request.session_id = session_id + return self.call_service(client, request, "/workshop_v2/get_report") + + def run(self) -> int: + tasks = parse_tasks(self.args.tasks) + Path(self.args.sensor_storage_root).mkdir(parents=True, exist_ok=True) + self.maybe_disable_wifi6_precheck() + if self.args.publish_fake_external_telemetry: + self.spin_for(self.args.external_warmup_sec) + + profile = make_vehicle_profile(self.args.vehicle_id) + self.register_vehicle_profile(profile) + session_id = self.create_session(profile, tasks) + self.verify_session_visible(session_id) + action_result = self.execute_session(session_id) + report_response = action_result.result.result + report = report_response.report + + if not report_response.success: + action_status = getattr(action_result, "status", None) + message = report_response.message or "(orchestrator 未返回失败原因)" + print( + f"[FAIL] execute_session 失败: {message}; " + f"session_id={session_id}; action_status={action_status}", + file=sys.stderr, + ) + self.print_report(report) + return 2 + + if not report.overall_success: + print("[FAIL] report.overall_success=false", file=sys.stderr) + self.print_report(report) + return 3 + + expected_count = len(tasks) + actual_count = len(report.stage_results) + if self.args.strict_stage_count and actual_count != expected_count: + print( + f"[FAIL] 阶段结果数量不符合预期: expected={expected_count}, actual={actual_count}", + file=sys.stderr, + ) + self.print_report(report) + return 4 + + failed = [result for result in report.stage_results if not result.success or not result.auto_acceptance_passed] + if failed: + print("[FAIL] 存在未通过阶段", file=sys.stderr) + self.print_report(report) + return 5 + + service_report = self.fetch_report(session_id) + if not service_report.response.success: + print(f"[FAIL] get_report 失败: {service_report.response.message}", file=sys.stderr) + return 6 + + print("[PASS] workshop orchestrator e2e smoke completed.") + self.print_report(report) + return 0 + + @staticmethod + def print_report(report) -> None: + print(f"[REPORT] session_id={report.session_id} overall_success={report.overall_success}") + print(f"[REPORT] summary={report.summary}") + for result in report.stage_results: + print( + "[STAGE] " + f"id={result.stage_id} " + f"success={result.success} " + f"auto_acceptance={result.auto_acceptance_passed} " + f"state={result.final_job_state.state} " + f"summary={result.summary}" + ) + + +def parse_args() -> argparse.Namespace: + parser = argparse.ArgumentParser(description="workshop_orchestrator_v2 端到端验收脚本") + parser.add_argument("--vehicle-id", default="demo_agv_001") + parser.add_argument("--tasks", default=DEFAULT_TASKS, help=f"逗号分隔任务列表,默认: {DEFAULT_TASKS}") + parser.add_argument("--operator-id", default="smoke_operator") + parser.add_argument("--workstation-id", default="sim_workstation") + parser.add_argument("--workshop-line-id", default="sim_line_001") + parser.add_argument("--workcell-zone-id", default="sim_workcell_zone") + parser.add_argument("--reference-source-name", default="isaac_sim_truth_source") + parser.add_argument("--reference-target-id", default="sim_reference_target") + parser.add_argument("--external-telemetry-topic", default="/isaac/external_localization/telemetry") + parser.add_argument("--publish-fake-external-telemetry", action=argparse.BooleanOptionalAction, default=True) + parser.add_argument("--fake-external-hz", type=float, default=20.0) + parser.add_argument("--external-warmup-sec", type=float, default=0.8) + parser.add_argument("--sensor-storage-root", default="/tmp/agv_sensor_calibration") + parser.add_argument("--chassis-distance-m", type=float, default=0.2) + parser.add_argument("--chassis-speed-mps", type=float, default=0.1) + parser.add_argument("--control-velocity-mps", type=float, default=0.1) + parser.add_argument("--service-timeout-sec", type=float, default=10.0) + parser.add_argument("--action-timeout-sec", type=float, default=240.0) + parser.add_argument("--strict-stage-count", action=argparse.BooleanOptionalAction, default=True) + parser.add_argument("--verbose-feedback", action="store_true") + parser.add_argument( + "--disable-wifi6-precheck", + action="store_true", + help="仅用于本地无 9000 WiFi6 gateway 的 demo;会临时关闭 orchestrator 的 WiFi6 预检参数", + ) + parser.add_argument("--orchestrator-node", default="/workshop_orchestrator_v2") + return parser.parse_args() + + +def main() -> int: + args = parse_args() + rclpy.init(args=None) + runner = OrchestratorSmoke(args) + try: + return runner.run() + except Exception as exc: + print(f"[FAIL] {exc}", file=sys.stderr) + return 1 + finally: + runner.destroy() + if rclpy.ok(): + rclpy.shutdown() + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/agv_calib_brain/src/simulation/tools/vehicle_wifi6_gateway_sim.py b/agv_calib_brain/src/simulation/tools/vehicle_wifi6_gateway_sim.py new file mode 100644 index 0000000..d743240 --- /dev/null +++ b/agv_calib_brain/src/simulation/tools/vehicle_wifi6_gateway_sim.py @@ -0,0 +1,178 @@ +#!/usr/bin/env python3 +"""仿真车端 WiFi6 gateway。 + +车间工控机只连接本脚本暴露的统一端口。脚本根据 frame_type 把请求转发到 +仿真车端内部的底盘、运控、传感器、外部真值后端端口。 +""" + +from __future__ import annotations + +import argparse +import json +import logging +import socket +import struct +import threading +from dataclasses import dataclass +from typing import Any + + +CHASSIS_GET_READINESS_REQ = 1 +CHASSIS_GET_READINESS_RSP = 2 +CHASSIS_MOTION_PRIMITIVE_REQ = 3 +CHASSIS_MOTION_PRIMITIVE_RSP = 4 +CHASSIS_EMERGENCY_BRAKE_REQ = 5 +CHASSIS_EMERGENCY_BRAKE_RSP = 6 + +CONTROL_GET_READINESS_REQ = 11 +CONTROL_GET_READINESS_RSP = 12 +CONTROL_EVALUATION_REQ = 13 +CONTROL_EVALUATION_RSP = 14 + +SENSOR_GET_READINESS_REQ = 21 +SENSOR_GET_READINESS_RSP = 22 +SENSOR_GET_LATEST_FRAME_REQ = 23 +SENSOR_GET_LATEST_FRAME_RSP = 24 +SENSOR_LIST_SENSORS_REQ = 25 +SENSOR_LIST_SENSORS_RSP = 26 + +EXTERNAL_POSE_PUSH_REQ = 31 +EXTERNAL_POSE_PUSH_RSP = 32 + + +@dataclass(frozen=True) +class BackendRoute: + name: str + host: str + port: int + response_type: int + + +def recv_exactly(conn: socket.socket, size: int) -> bytes: + data = b"" + while len(data) < size: + chunk = conn.recv(size - len(data)) + if not chunk: + raise ConnectionError("连接提前关闭") + data += chunk + return data + + +def read_frame(conn: socket.socket) -> tuple[int, bytes]: + header = recv_exactly(conn, 8) + msg_type, payload_len = struct.unpack(" None: + conn.sendall(struct.pack(" bytes: + payload: dict[str, Any] = { + "success": False, + "error_code": {"code": 3}, + "message": message, + } + return json.dumps(payload, ensure_ascii=False, separators=(",", ":")).encode("utf-8") + + +class Wifi6GatewaySim: + def __init__(self, args: argparse.Namespace) -> None: + self.bind_host = args.bind_host + self.vehicle_port = args.vehicle_port + self.timeout_sec = args.timeout_sec + self.routes = self._build_routes(args) + + def _build_routes(self, args: argparse.Namespace) -> dict[int, BackendRoute]: + chassis = (args.chassis_host, args.chassis_port) + control = (args.control_host, args.control_port) + sensor = (args.sensor_host, args.sensor_port) + external_pose = (args.external_pose_host, args.external_pose_port) + return { + CHASSIS_GET_READINESS_REQ: BackendRoute("chassis", *chassis, CHASSIS_GET_READINESS_RSP), + CHASSIS_MOTION_PRIMITIVE_REQ: BackendRoute("chassis", *chassis, CHASSIS_MOTION_PRIMITIVE_RSP), + CHASSIS_EMERGENCY_BRAKE_REQ: BackendRoute("chassis", *chassis, CHASSIS_EMERGENCY_BRAKE_RSP), + CONTROL_GET_READINESS_REQ: BackendRoute("control", *control, CONTROL_GET_READINESS_RSP), + CONTROL_EVALUATION_REQ: BackendRoute("control", *control, CONTROL_EVALUATION_RSP), + SENSOR_GET_READINESS_REQ: BackendRoute("sensor", *sensor, SENSOR_GET_READINESS_RSP), + SENSOR_GET_LATEST_FRAME_REQ: BackendRoute("sensor", *sensor, SENSOR_GET_LATEST_FRAME_RSP), + SENSOR_LIST_SENSORS_REQ: BackendRoute("sensor", *sensor, SENSOR_LIST_SENSORS_RSP), + EXTERNAL_POSE_PUSH_REQ: BackendRoute("external_pose", *external_pose, EXTERNAL_POSE_PUSH_RSP), + } + + def serve_forever(self) -> None: + with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as server: + server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + server.bind((self.bind_host, self.vehicle_port)) + server.listen(32) + logging.info("仿真 WiFi6 gateway 监听 %s:%d", self.bind_host, self.vehicle_port) + while True: + conn, addr = server.accept() + thread = threading.Thread( + target=self._handle_client, + args=(conn, addr), + name=f"wifi6-gateway:{addr[0]}:{addr[1]}", + daemon=True, + ) + thread.start() + + def _handle_client(self, conn: socket.socket, addr: tuple[str, int]) -> None: + with conn: + conn.settimeout(self.timeout_sec) + try: + msg_type, payload = read_frame(conn) + route = self.routes.get(msg_type) + if route is None: + logging.warning("未知 WiFi6 frame_type=%d from %s:%d", msg_type, addr[0], addr[1]) + send_frame(conn, 0, json_error(f"unsupported wifi6 frame_type={msg_type}")) + return + rsp_type, rsp_payload = self._forward(route, msg_type, payload) + send_frame(conn, rsp_type, rsp_payload) + except Exception as exc: + logging.warning("处理 WiFi6 gateway 请求失败: %s", exc) + + def _forward(self, route: BackendRoute, msg_type: int, payload: bytes) -> tuple[int, bytes]: + try: + with socket.create_connection((route.host, route.port), timeout=self.timeout_sec) as backend: + backend.settimeout(self.timeout_sec) + send_frame(backend, msg_type, payload) + rsp_type, rsp_payload = read_frame(backend) + if rsp_type != route.response_type: + return route.response_type, json_error( + f"{route.name} backend response type mismatch: expected {route.response_type}, got {rsp_type}" + ) + return rsp_type, rsp_payload + except Exception as exc: + return route.response_type, json_error( + f"{route.name} backend unavailable at {route.host}:{route.port}: {exc}" + ) + + +def parse_args() -> argparse.Namespace: + parser = argparse.ArgumentParser(description="仿真车端统一 WiFi6 gateway") + parser.add_argument("--bind-host", default="0.0.0.0") + parser.add_argument("--vehicle-port", type=int, default=9000) + parser.add_argument("--timeout-sec", type=float, default=5.0) + parser.add_argument("--chassis-host", default="127.0.0.1") + parser.add_argument("--chassis-port", type=int, default=9001) + parser.add_argument("--control-host", default="127.0.0.1") + parser.add_argument("--control-port", type=int, default=9002) + parser.add_argument("--sensor-host", default="127.0.0.1") + parser.add_argument("--sensor-port", type=int, default=9003) + parser.add_argument("--external-pose-host", default="127.0.0.1") + parser.add_argument("--external-pose-port", type=int, default=9004) + parser.add_argument("--log-level", default="INFO") + return parser.parse_args() + + +def main() -> int: + args = parse_args() + logging.basicConfig(level=getattr(logging, args.log_level.upper(), logging.INFO), format="[%(levelname)s] %(message)s") + Wifi6GatewaySim(args).serve_forever() + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/agv_calib_brain/src/simulation/tools/workshop_sensor_ingest_sim.py b/agv_calib_brain/src/simulation/tools/workshop_sensor_ingest_sim.py new file mode 100644 index 0000000..3f70684 --- /dev/null +++ b/agv_calib_brain/src/simulation/tools/workshop_sensor_ingest_sim.py @@ -0,0 +1,358 @@ +#!/usr/bin/env python3 +"""Workshop-side sensor ingest for the simulated WiFi6 TCP vehicle link.""" + +from __future__ import annotations + +import argparse +import base64 +import json +import socket +import struct +import time +from typing import Any + +import rclpy +from sensor_msgs.msg import Image, Imu, LaserScan, PointCloud2, PointField + + +SENSOR_GET_LATEST_FRAME_REQ = 23 +SENSOR_GET_LATEST_FRAME_RSP = 24 +SENSOR_LIST_SENSORS_REQ = 25 +SENSOR_LIST_SENSORS_RSP = 26 + +PAYLOAD_IMAGE = 1 +PAYLOAD_POINT_CLOUD = 2 +PAYLOAD_LASER_SCAN = 3 +PAYLOAD_IMU = 4 + + +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("connection closed") + chunks.append(chunk) + remaining -= len(chunk) + return b"".join(chunks) + + +def send_request( + host: str, + port: int, + msg_type: int, + payload: dict[str, Any], + timeout_sec: float, +) -> tuple[int, dict[str, Any]]: + encoded = json.dumps(payload, ensure_ascii=False, separators=(",", ":")).encode("utf-8") + with socket.create_connection((host, port), timeout=timeout_sec) as conn: + conn.settimeout(timeout_sec) + conn.sendall(struct.pack(" str: + cleaned = "".join(ch if ch.isalnum() or ch == "_" else "_" for ch in value) + return cleaned.strip("_") or "sensor" + + +def topic_for(prefix: str, sensor_id: str, payload_type: int) -> str: + suffix_by_payload = { + PAYLOAD_IMAGE: "image_raw", + PAYLOAD_POINT_CLOUD: "pointcloud", + PAYLOAD_LASER_SCAN: "scan", + PAYLOAD_IMU: "data", + } + suffix = suffix_by_payload.get(payload_type, "frame") + return f"{prefix.rstrip('/')}/{sanitize_topic_part(sensor_id)}/{suffix}" + + +def stamp_from_us(msg: Any, timestamp_us: int, frame_id: str) -> None: + msg.header.frame_id = frame_id + msg.header.stamp.sec = int(timestamp_us // 1_000_000) + msg.header.stamp.nanosec = int((timestamp_us % 1_000_000) * 1000) + + +def decode_data(payload: dict[str, Any], field_name: str = "data") -> bytes: + encoded = payload.get(field_name, "") + if not encoded: + return b"" + return base64.b64decode(encoded.encode("ascii")) + + +def fixed_covariance(values: Any) -> list[float]: + result = [float(value) for value in list(values or [])[:9]] + while len(result) < 9: + result.append(0.0) + return result + + +class WorkshopSensorIngestSim: + def __init__(self, args: argparse.Namespace): + self.args = args + self.node = rclpy.create_node("workshop_sensor_ingest_sim") + self.publishers: dict[str, Any] = {} + self.sensors: dict[str, dict[str, Any]] = {} + self.last_sensor_refresh_monotonic = 0.0 + self.last_error_log_monotonic = 0.0 + self.published_count = 0 + self.fail_count = 0 + self.timer = self.node.create_timer(1.0 / max(args.poll_hz, 0.1), self.poll_once) + self.node.get_logger().info( + f"workshop sensor ingest: {args.sensor_host}:{args.sensor_port} -> {args.publish_prefix}" + ) + + def log_error(self, message: str) -> None: + self.fail_count += 1 + now = time.monotonic() + if now - self.last_error_log_monotonic >= self.args.error_log_interval_sec: + self.last_error_log_monotonic = now + self.node.get_logger().warning(f"{message}; fail_count={self.fail_count}") + + def refresh_sensors(self) -> None: + now = time.monotonic() + if self.sensors and now - self.last_sensor_refresh_monotonic < self.args.sensor_refresh_interval_sec: + return + self.last_sensor_refresh_monotonic = now + + rsp_type, payload = send_request( + self.args.sensor_host, + self.args.sensor_port, + SENSOR_LIST_SENSORS_REQ, + {}, + self.args.timeout_sec, + ) + if rsp_type != SENSOR_LIST_SENSORS_RSP or not payload.get("success", False): + raise RuntimeError(f"sensor list failed: type={rsp_type}, payload={payload}") + + allowed = set(self.args.sensor_id) + sensors = {} + for sensor in payload.get("sensors", []): + sensor_id = str(sensor.get("sensor_id", "")) + if not sensor_id or (allowed and sensor_id not in allowed): + continue + if not bool(sensor.get("enabled", True)): + continue + sensors[sensor_id] = sensor + self.ensure_publisher(sensor_id, int(sensor.get("payload_type", 0) or 0)) + + self.sensors = sensors + + def ensure_publisher(self, sensor_id: str, payload_type: int) -> None: + if sensor_id in self.publishers: + return + topic = topic_for(self.args.publish_prefix, sensor_id, payload_type) + if payload_type == PAYLOAD_IMAGE: + publisher = self.node.create_publisher(Image, topic, 10) + elif payload_type == PAYLOAD_POINT_CLOUD: + publisher = self.node.create_publisher(PointCloud2, topic, 10) + elif payload_type == PAYLOAD_LASER_SCAN: + publisher = self.node.create_publisher(LaserScan, topic, 10) + elif payload_type == PAYLOAD_IMU: + publisher = self.node.create_publisher(Imu, topic, 10) + else: + self.node.get_logger().warning(f"unsupported sensor payload_type={payload_type} for sensor_id={sensor_id}") + return + self.publishers[sensor_id] = publisher + self.node.get_logger().info(f"publish {sensor_id} frames to {topic}") + + def poll_once(self) -> None: + try: + self.refresh_sensors() + for sensor_id in list(self.sensors): + frame = self.fetch_latest_frame(sensor_id) + self.publish_frame(frame) + except Exception as exc: + self.log_error(str(exc)) + + def fetch_latest_frame(self, sensor_id: str) -> dict[str, Any]: + first = self.request_frame_fragment(sensor_id, 0) + total_fragments = int(first.get("total_fragments", 1) or 1) + if total_fragments <= 1: + return first + + payload_type = int(first.get("payload_type", 0) or 0) + if payload_type not in (PAYLOAD_IMAGE, PAYLOAD_POINT_CLOUD): + return first + + stream_id = str(first.get("stream_id", "")) + raw_parts = [self.extract_raw_payload(first)] + for fragment_index in range(1, total_fragments): + fragment = self.request_frame_fragment(sensor_id, fragment_index) + if str(fragment.get("stream_id", "")) != stream_id: + raise RuntimeError(f"sensor_id={sensor_id} fragment stream changed while reassembling") + raw_parts.append(self.extract_raw_payload(fragment)) + self.inject_raw_payload(first, b"".join(raw_parts)) + first["fragment_index"] = 0 + first["total_fragments"] = 1 + first["is_final_fragment"] = True + return first + + def request_frame_fragment(self, sensor_id: str, fragment_index: int) -> dict[str, Any]: + rsp_type, payload = send_request( + self.args.sensor_host, + self.args.sensor_port, + SENSOR_GET_LATEST_FRAME_REQ, + { + "sensor_id": sensor_id, + "include_payload": True, + "max_payload_bytes": self.args.max_payload_bytes, + "fragment_index": fragment_index, + }, + self.args.timeout_sec, + ) + if rsp_type != SENSOR_GET_LATEST_FRAME_RSP or not payload.get("success", False): + raise RuntimeError(f"latest frame failed for {sensor_id}: type={rsp_type}, payload={payload}") + return payload.get("sensor_frame", {}) + + @staticmethod + def extract_raw_payload(frame: dict[str, Any]) -> bytes: + payload_type = int(frame.get("payload_type", 0) or 0) + if payload_type == PAYLOAD_IMAGE: + return decode_data(frame.get("image", {})) + if payload_type == PAYLOAD_POINT_CLOUD: + return decode_data(frame.get("point_cloud", {})) + return b"" + + @staticmethod + def inject_raw_payload(frame: dict[str, Any], raw: bytes) -> None: + payload_type = int(frame.get("payload_type", 0) or 0) + encoded = base64.b64encode(raw).decode("ascii") + if payload_type == PAYLOAD_IMAGE: + frame.setdefault("image", {})["data"] = encoded + elif payload_type == PAYLOAD_POINT_CLOUD: + frame.setdefault("point_cloud", {})["data"] = encoded + + def publish_frame(self, frame: dict[str, Any]) -> None: + header = frame.get("header", {}) + sensor_id = str(header.get("sensor_id", "")) + payload_type = int(frame.get("payload_type", 0) or 0) + publisher = self.publishers.get(sensor_id) + if publisher is None: + self.ensure_publisher(sensor_id, payload_type) + publisher = self.publishers.get(sensor_id) + if publisher is None: + return + + if payload_type == PAYLOAD_IMAGE: + publisher.publish(self.to_image(frame)) + elif payload_type == PAYLOAD_POINT_CLOUD: + publisher.publish(self.to_point_cloud(frame)) + elif payload_type == PAYLOAD_LASER_SCAN: + publisher.publish(self.to_laser_scan(frame)) + elif payload_type == PAYLOAD_IMU: + publisher.publish(self.to_imu(frame)) + else: + raise RuntimeError(f"unsupported payload_type={payload_type} for sensor_id={sensor_id}") + self.published_count += 1 + + def to_image(self, frame: dict[str, Any]) -> Image: + header = frame.get("header", {}) + payload = frame.get("image", {}) + msg = Image() + stamp_from_us(msg, int(header.get("hardware_timestamp_us", 0) or 0), str(header.get("frame_id", ""))) + msg.width = int(payload.get("width", 0) or 0) + msg.height = int(payload.get("height", 0) or 0) + msg.encoding = str(payload.get("encoding", "")) + msg.is_bigendian = int(1 if payload.get("is_bigendian", False) else 0) + msg.step = int(payload.get("step", 0) or 0) + msg.data = decode_data(payload) + return msg + + def to_point_cloud(self, frame: dict[str, Any]) -> PointCloud2: + header = frame.get("header", {}) + payload = frame.get("point_cloud", {}) + msg = PointCloud2() + stamp_from_us(msg, int(header.get("hardware_timestamp_us", 0) or 0), str(header.get("frame_id", ""))) + msg.height = int(payload.get("height", 0) or 0) + msg.width = int(payload.get("width", 0) or 0) + msg.fields = [ + PointField( + name=str(field.get("name", "")), + offset=int(field.get("offset", 0) or 0), + datatype=int(field.get("datatype", 0) or 0), + count=int(field.get("count", 0) or 0), + ) + for field in payload.get("fields", []) + ] + msg.is_bigendian = bool(payload.get("is_bigendian", False)) + msg.point_step = int(payload.get("point_step", 0) or 0) + msg.row_step = int(payload.get("row_step", 0) or 0) + msg.is_dense = bool(payload.get("is_dense", True)) + msg.data = decode_data(payload) + return msg + + def to_laser_scan(self, frame: dict[str, Any]) -> LaserScan: + header = frame.get("header", {}) + payload = frame.get("laser_scan", {}) + msg = LaserScan() + stamp_from_us(msg, int(header.get("hardware_timestamp_us", 0) or 0), str(header.get("frame_id", ""))) + msg.angle_min = float(payload.get("angle_min_rad", 0.0) or 0.0) + msg.angle_max = float(payload.get("angle_max_rad", 0.0) or 0.0) + msg.angle_increment = float(payload.get("angle_increment_rad", 0.0) or 0.0) + msg.time_increment = float(payload.get("time_increment_sec", 0.0) or 0.0) + msg.scan_time = float(payload.get("scan_time_sec", 0.0) or 0.0) + msg.range_min = float(payload.get("range_min_m", 0.0) or 0.0) + msg.range_max = float(payload.get("range_max_m", 0.0) or 0.0) + msg.ranges = [float(value) for value in payload.get("ranges_m", [])] + msg.intensities = [float(value) for value in payload.get("intensities", [])] + return msg + + def to_imu(self, frame: dict[str, Any]) -> Imu: + header = frame.get("header", {}) + payload = frame.get("imu", {}) + msg = Imu() + stamp_from_us(msg, int(header.get("hardware_timestamp_us", 0) or 0), str(header.get("frame_id", ""))) + msg.orientation.x = float(payload.get("orientation_x", 0.0) or 0.0) + msg.orientation.y = float(payload.get("orientation_y", 0.0) or 0.0) + msg.orientation.z = float(payload.get("orientation_z", 0.0) or 0.0) + msg.orientation.w = float(payload.get("orientation_w", 1.0) or 1.0) + msg.orientation_covariance = fixed_covariance(payload.get("orientation_covariance", [])) + angular_velocity = payload.get("angular_velocity", {}) + msg.angular_velocity.x = float(angular_velocity.get("x", 0.0) or 0.0) + msg.angular_velocity.y = float(angular_velocity.get("y", 0.0) or 0.0) + msg.angular_velocity.z = float(angular_velocity.get("z", 0.0) or 0.0) + msg.angular_velocity_covariance = fixed_covariance(payload.get("angular_velocity_covariance", [])) + linear_acceleration = payload.get("linear_acceleration", {}) + msg.linear_acceleration.x = float(linear_acceleration.get("x", 0.0) or 0.0) + msg.linear_acceleration.y = float(linear_acceleration.get("y", 0.0) or 0.0) + msg.linear_acceleration.z = float(linear_acceleration.get("z", 0.0) or 0.0) + msg.linear_acceleration_covariance = fixed_covariance(payload.get("linear_acceleration_covariance", [])) + return msg + + +def parse_args() -> argparse.Namespace: + parser = argparse.ArgumentParser(description="车间侧传感器 WiFi6/TCP 接入仿真") + parser.add_argument("--sensor-host", default="127.0.0.1") + parser.add_argument("--sensor-port", type=int, default=9000) + parser.add_argument("--publish-prefix", default="/workshop/vehicle_sensor") + parser.add_argument("--poll-hz", type=float, default=15.0) + parser.add_argument("--max-payload-bytes", type=int, default=4 * 1024 * 1024) + parser.add_argument("--timeout-sec", type=float, default=2.0) + parser.add_argument("--sensor-id", action="append", default=[], help="只拉取指定 sensor_id,可重复") + parser.add_argument("--sensor-refresh-interval-sec", type=float, default=5.0) + parser.add_argument("--error-log-interval-sec", type=float, default=2.0) + return parser.parse_args() + + +def main() -> int: + args = parse_args() + rclpy.init(args=None) + ingest = WorkshopSensorIngestSim(args) + try: + rclpy.spin(ingest.node) + except KeyboardInterrupt: + pass + finally: + ingest.node.destroy_node() + if rclpy.ok(): + rclpy.shutdown() + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/agv_calib_brain/src/simulation/vehicle_agent_sim/README.md b/agv_calib_brain/src/simulation/vehicle_agent_sim/README.md new file mode 100644 index 0000000..a055b32 --- /dev/null +++ b/agv_calib_brain/src/simulation/vehicle_agent_sim/README.md @@ -0,0 +1,113 @@ +# 仿真车端代理 + +这个模块模拟真实 Windows 小车上的车端电脑。 + +它的职责是: + +- 监听与真实车端一致的 TCP 端口。 +- 接收 `vehicle_agent_gateway` 发来的底盘/运控请求。 +- 校验 `vehicle_id` 和动作参数。 +- 把被接受的动作转换为私有 Isaac 执行器话题。 +- 执行完成后返回标定任务结果。 +- 对底盘动作和运控评估做任务互斥,避免同时控制车辆。 +- 支持急停请求。 +- 通过 WiFi6/TCP 接收外部真值位姿,并对工控机下发的轨迹做闭环跟踪。 +- 订阅底盘遥测作为速度反馈,不把底盘里程计作为默认全局位姿源。 +- 发布车端电脑与车辆本体之间的内部阿克曼控制命令、车辆状态、健康状态和时间同步状态。 + +默认私有控制话题: + +```text +/vehicle//actuator/cmd_vel +``` + +默认车辆内部话题: + +```text +/vehicle//internal/ackermann_cmd +/vehicle//internal/state +/vehicle//internal/health +/vehicle//internal/time_sync_status +``` + +默认一键仿真的 TCP 端口: + +```text +9000 车端统一 WiFi6/TCP 入口,由 isaac_vehicle_unified_agent_sim.py 对外暴露 +``` + +车间工控机不应该直接发布这个话题。它应该通过: + +```text +workshop_orchestrator -> vehicle_agent_gateway -> vehicle_agent_sim -> Isaac cmd_vel +``` + +在默认仿真部署中,`vehicle_agent_gateway` 实际连接的是 `9000` 统一入口;不同 frame_type 在统一车端 agent 内部分发。 + +启动: + +```bash +python3 src/simulation/tools/launch_sim_stack.py --component vehicle-agent +``` + +直接启动脚本: + +```bash +source install/setup.bash +python3 src/simulation/tools/isaac_vehicle_unified_agent_sim.py \ + --vehicle-id demo_agv_001 \ + --vehicle-port 9000 \ + --cmd-vel-topic /vehicle/demo_agv_001/actuator/cmd_vel \ + --external-pose-topic /isaac/external_localization/telemetry \ + --external-pose-transport wifi6_tcp \ + --chassis-telemetry-topic /chassis/telemetry +``` + +验收: + +```bash +source install/setup.bash +python3 src/simulation/tools/smoke_test_vehicle_agent.py +``` + +小距离运动验收: + +```bash +python3 src/simulation/tools/smoke_test_vehicle_agent.py --motion +``` + +底盘动作和运控评估一起验收: + +```bash +python3 src/simulation/tools/smoke_test_vehicle_agent.py --motion --control-eval +``` + +无 Isaac 时验证轨迹接口: + +```bash +python3 src/simulation/tools/smoke_test_vehicle_agent.py --trajectory --fake-telemetry +``` + +验证分段轨迹下发: + +```bash +python3 src/simulation/tools/smoke_test_vehicle_agent.py --trajectory --fake-telemetry --split-trajectory +``` + +外部真值链路: + +- `external_pose_wifi6_bridge.py` 订阅 Isaac 或外部真值系统发布的 `ExternalLocalizationTelemetry`。 +- 桥接程序把位姿按 31/32 帧类型推送到车端统一 `9000` 端口,再由统一车端 agent 写入车端位姿缓存。 +- 车端只把底盘里程计作为显式降级输入,不作为默认全局位姿源。 + +当前支持的仿真任务: + +- 底盘直线动作 +- 底盘圆弧动作 +- 运控轨迹跟踪,基于 WiFi6/TCP 外部真值位姿做 Pure Pursuit 闭环控制;短轨迹默认整包下发,长轨迹可用 `trajectory_id`、`segment_index`、`total_segments` 分段上传,最后一段触发拼接执行 +- 运控速度阶跃 +- 运控加减速 +- 运控停车精度的简化执行 +- 车辆内部接口仿真:阿克曼命令、底盘状态、健康诊断、时间同步 + +现场部署时,外部真值/定位系统也应按同一语义经 WiFi 6 传到车端;仿真中由 Isaac 的 external truth 发布器提供同形态数据,再由桥接程序转成 TCP 输入。 diff --git a/agv_calib_brain/src/simulation/vehicle_agent_sim/scripts/isaac_vehicle_agent_sim.py b/agv_calib_brain/src/simulation/vehicle_agent_sim/scripts/isaac_vehicle_agent_sim.py new file mode 100644 index 0000000..cd505f2 --- /dev/null +++ b/agv_calib_brain/src/simulation/vehicle_agent_sim/scripts/isaac_vehicle_agent_sim.py @@ -0,0 +1,1300 @@ +#!/usr/bin/env python3 +"""Vehicle-side simulator agent for Isaac validation. + +This process simulates the vehicle computer. It exposes the same TCP frame +protocol used by the real vehicle-side agent and owns the private actuator +command topic that drives the Isaac vehicle. +""" + +import argparse +import json +import math +import socket +import socketserver +import struct +import threading +import time + +import rclpy +from geometry_msgs.msg import Twist + +try: + from calibration_chassis_interfaces.msg import ChassisTelemetry +except ImportError: + ChassisTelemetry = None + +try: + from calibration_external_localization_interfaces.msg import ExternalLocalizationTelemetry +except ImportError: + ExternalLocalizationTelemetry = None + +try: + from vehicle_internal_interfaces.msg import ( + AckermannDriveCommand, + VehicleControlMode, + VehicleHealthStatus, + VehicleInternalState, + VehicleTimeSyncStatus, + ) +except ImportError: + AckermannDriveCommand = None + VehicleControlMode = None + VehicleHealthStatus = None + VehicleInternalState = None + VehicleTimeSyncStatus = None + + +CHASSIS_GET_READINESS_REQ = 1 +CHASSIS_GET_READINESS_RSP = 2 +CHASSIS_MOTION_PRIMITIVE_REQ = 3 +CHASSIS_MOTION_PRIMITIVE_RSP = 4 +CHASSIS_EMERGENCY_BRAKE_REQ = 5 +CHASSIS_EMERGENCY_BRAKE_RSP = 6 +CONTROL_GET_READINESS_REQ = 11 +CONTROL_GET_READINESS_RSP = 12 +CONTROL_EVALUATION_REQ = 13 +CONTROL_EVALUATION_RSP = 14 +EXTERNAL_POSE_PUSH_REQ = 31 +EXTERNAL_POSE_PUSH_RSP = 32 + +ERROR_OK = 1 +ERROR_INVALID_ARGUMENT = 2 +ERROR_INVALID_STATE = 3 +ERROR_VEHICLE_BUSY = 4 +ERROR_UNSUPPORTED_CAPABILITY = 13 + + +def now_us(): + return int(time.time() * 1_000_000) + + +def clamp(value, lower, upper): + return max(lower, min(upper, value)) + + +def normalize_angle(angle): + while angle > math.pi: + angle -= 2.0 * math.pi + while angle < -math.pi: + angle += 2.0 * math.pi + return angle + + +def read_exactly(conn, count): + chunks = [] + remaining = count + while remaining > 0: + chunk = conn.recv(remaining) + if not chunk: + raise ConnectionError("connection closed") + chunks.append(chunk) + remaining -= len(chunk) + return b"".join(chunks) + + +def send_frame(conn, msg_type, payload): + encoded = json.dumps(payload, ensure_ascii=False, separators=(",", ":")).encode("utf-8") + conn.sendall(struct.pack(" 0.0 else self.args.pose_stale_timeout_sec, + "min_quality": min_quality if min_quality > 0.0 else self.args.external_pose_min_quality, + } + + def state_pose_constraint_error(self, state, constraints): + if state is None: + return "external pose unavailable" + + age_sec = time.monotonic() - state["stamp"] + if age_sec > constraints["max_age_sec"]: + return f"external pose stale: age={age_sec:.3f}s, limit={constraints['max_age_sec']:.3f}s" + + required_source = constraints["required_source"] + pose_source = str(state.get("pose_source", "")) + if required_source and pose_source != required_source: + return f"external pose source mismatch: expected {required_source}, got {pose_source}" + + quality_score = float(state.get("quality_score", 0.0)) + if quality_score < constraints["min_quality"]: + return f"external pose quality too low: score={quality_score:.3f}, limit={constraints['min_quality']:.3f}" + + return "" + + def get_chassis_velocity_state(self): + with self.state_lock: + return dict(self.latest_velocity) if self.latest_velocity is not None else None + + def get_pose_status(self): + transport_label = "wifi6_sim_tcp" if self.args.external_pose_transport == "wifi6_tcp" else self.args.external_pose_transport + endpoint = ( + f"{self.args.bind_host}:{self.args.external_pose_port}" + if self.args.external_pose_transport in ("wifi6_tcp", "both") + else self.args.external_pose_topic + ) + with self.state_lock: + if self.latest_pose is None: + return { + "ready": False, + "source": "", + "age_sec": 0.0, + "quality_score": 0.0, + "topic": self.args.external_pose_topic, + "transport": transport_label, + "endpoint": endpoint, + } + age_sec = time.monotonic() - self.latest_pose["stamp"] + return { + "ready": age_sec <= self.args.pose_stale_timeout_sec, + "source": self.latest_pose["source"], + "age_sec": age_sec, + "quality_score": float(self.latest_pose.get("quality_score", 0.0)), + "topic": self.args.external_pose_topic, + "transport": transport_label, + "endpoint": endpoint, + } + + def has_recent_vehicle_state(self): + state = self.get_vehicle_state() + if state is None: + return False + return (time.monotonic() - state["stamp"]) <= self.args.pose_stale_timeout_sec + + def wait_for_vehicle_state(self, constraints=None): + if constraints is None: + constraints = { + "required_source": "", + "max_age_sec": self.args.pose_stale_timeout_sec, + "min_quality": self.args.external_pose_min_quality, + } + + deadline = time.monotonic() + self.args.pose_wait_timeout_sec + last_error = "external pose unavailable" + while time.monotonic() < deadline and not self.stop_requested: + state = self.get_vehicle_state() + last_error = self.state_pose_constraint_error(state, constraints) + if not last_error: + return state + time.sleep(0.02) + raise ValueError( + f"trajectory_tracking requires valid external pose via {self.args.external_pose_transport}: {last_error}" + ) + + def motion_duration_limit(self, request): + request_timeout = float(request.get("timeout_sec", 0.0) or 0.0) + if request_timeout > 0.0: + return min(request_timeout, self.args.max_motion_duration_sec) + return self.args.max_motion_duration_sec + + def readiness_payload(self, domain): + busy = bool(self.active_job_id) + pose_status = self.get_pose_status() + feedback_ready = pose_status["ready"] + common = { + "success": not busy, + "error_code": {"code": ERROR_OK if not busy else ERROR_VEHICLE_BUSY}, + "message": ( + f"Isaac vehicle agent sim: {domain} ready." + if not busy + else f"Isaac vehicle agent sim: busy with {self.active_domain}:{self.active_job_id}." + ), + "agent_ready": not busy, + "estop_released": True, + "vehicle_safe_to_move": not busy, + "checked_timestamp_us": now_us(), + } + if domain == "chassis": + common.update({ + "chassis_driver_online": True, + "motion_control_ready": not busy, + "telemetry_available": True, + }) + else: + common.update({ + "trajectory_executor_ready": not busy, + "vehicle_feedback_ready": feedback_ready, + "control_output_ready": not busy, + "external_pose_topic": self.args.external_pose_topic, + "velocity_feedback_topic": self.args.chassis_telemetry_topic, + "pose_source": pose_status["source"], + "pose_age_sec": pose_status["age_sec"], + "external_pose_feedback_ready": feedback_ready, + "external_pose_source_name": pose_status["source"], + "external_pose_age_ms": pose_status["age_sec"] * 1000.0, + "external_pose_quality_score": pose_status["quality_score"], + "external_pose_transport": pose_status["transport"], + "external_pose_endpoint": pose_status["endpoint"], + }) + return common + + def publish_cmd(self, linear_x, angular_z): + msg = Twist() + msg.linear.x = float(linear_x) + msg.angular.z = float(angular_z) + self.publisher.publish(msg) + self.publish_internal_ackermann_command(linear_x, angular_z) + + def ackermann_steering_angle(self, linear_x, angular_z): + linear_x = float(linear_x) + angular_z = float(angular_z) + if abs(linear_x) <= 1e-4 or abs(angular_z) <= 1e-4: + return 0.0 + angle = math.atan(self.args.wheel_base_m * angular_z / linear_x) + return clamp(angle, -self.args.max_steering_angle_rad, self.args.max_steering_angle_rad) + + def internal_control_mode_value(self): + if VehicleControlMode is None: + return 0 + if self.stop_requested: + return VehicleControlMode.CALIBRATION + if self.active_job_id: + return VehicleControlMode.CALIBRATION + return VehicleControlMode.AUTO + + def publish_internal_ackermann_command(self, linear_x, angular_z): + if self.internal_ackermann_command_publisher is None: + return + + steering_angle = self.ackermann_steering_angle(linear_x, angular_z) + with self.state_lock: + self.latest_internal_command = { + "linear_x": float(linear_x), + "angular_z": float(angular_z), + "steering_angle": float(steering_angle), + "stamp": time.monotonic(), + } + + msg = AckermannDriveCommand() + msg.command_timestamp_us = now_us() + msg.command_id = f"isaac_cmd_{msg.command_timestamp_us}" + msg.source = "isaac_vehicle_agent_sim" + msg.control_mode.value = self.internal_control_mode_value() + msg.target_speed_ms = float(linear_x) + msg.target_accel_ms2 = 0.0 + msg.target_steering_angle_rad = float(steering_angle) + msg.target_steering_rate_rads = 0.0 + msg.brake_command = 1.0 if abs(float(linear_x)) < 1e-5 and abs(float(angular_z)) < 1e-5 else 0.0 + msg.throttle_command = min(1.0, abs(float(linear_x)) / max(self.args.max_speed_mps, 1e-6)) + msg.command_timeout_sec = self.args.internal_command_timeout_sec + msg.stop_when_timeout = True + self.internal_ackermann_command_publisher.publish(msg) + + def publish_internal_state(self): + if self.internal_state_publisher is None: + return + + velocity = self.get_chassis_velocity_state() or {} + with self.state_lock: + command = dict(self.latest_internal_command) + speed = float(velocity.get("linear_velocity", command.get("linear_x", 0.0))) + yaw_rate = float(velocity.get("angular_velocity", command.get("angular_z", 0.0))) + steering_angle = float(command.get("steering_angle", 0.0)) + + msg = VehicleInternalState() + msg.hardware_timestamp_us = now_us() + msg.current_mode.value = self.internal_control_mode_value() + msg.vehicle_enabled = True + msg.estop_engaged = False + msg.calibration_low_speed_mode = bool(self.active_job_id) + msg.fault_active = False + msg.primary_fault_code = "" + msg.primary_fault_message = "" + msg.ackermann_state.hardware_timestamp_us = msg.hardware_timestamp_us + msg.ackermann_state.actual_speed_ms = speed + msg.ackermann_state.actual_accel_ms2 = 0.0 + msg.ackermann_state.actual_steering_angle_rad = steering_angle + msg.ackermann_state.actual_steering_rate_rads = 0.0 + msg.ackermann_state.rear_left_wheel_speed_ms = speed + msg.ackermann_state.rear_right_wheel_speed_ms = speed + msg.ackermann_state.front_left_steering_angle_rad = steering_angle + msg.ackermann_state.front_right_steering_angle_rad = steering_angle + msg.ackermann_state.drive_motor_current_amp = 0.8 + abs(speed) * 2.0 + msg.ackermann_state.steering_motor_current_amp = 0.4 + abs(steering_angle) * 1.5 + msg.ackermann_state.brake_pressure = 1.0 if abs(speed) < 1e-4 and abs(command.get("linear_x", 0.0)) < 1e-4 else 0.0 + msg.ackermann_state.throttle_output = min(1.0, abs(command.get("linear_x", 0.0)) / max(self.args.max_speed_mps, 1e-6)) + msg.battery_voltage_v = 48.0 + msg.battery_current_amp = msg.ackermann_state.drive_motor_current_amp + msg.battery_soc = 0.85 + msg.yaw_rate_rads = yaw_rate + msg.lateral_accel_ms2 = speed * yaw_rate + self.internal_state_publisher.publish(msg) + + def publish_health_status(self): + if self.health_status_publisher is None: + return + + pose_status = self.get_pose_status() + msg = VehicleHealthStatus() + msg.hardware_timestamp_us = now_us() + msg.vehicle_agent_online = True + msg.vehicle_controller_online = True + msg.drive_controller_online = True + msg.steering_controller_online = True + msg.sensor_bus_online = True + msg.vehicle_bus_online = True + msg.external_truth_link_online = bool(pose_status["ready"]) + msg.vehicle_bus_rx_hz = self.args.publish_hz + msg.vehicle_bus_drop_ratio = 0.0 + msg.command_latency_ms = 1.0 + msg.telemetry_latency_ms = pose_status["age_sec"] * 1000.0 if pose_status["ready"] else 0.0 + msg.cpu_load_ratio = 0.0 + msg.memory_used_ratio = 0.0 + msg.disk_used_ratio = 0.0 + msg.temperature_c = 40.0 + msg.healthy = True + msg.diagnostic_code = "OK" + msg.diagnostic_message = "isaac vehicle internal health is nominal" + self.health_status_publisher.publish(msg) + + def publish_time_sync_status(self): + if self.time_sync_status_publisher is None: + return + + pose_status = self.get_pose_status() + msg = VehicleTimeSyncStatus() + msg.hardware_timestamp_us = now_us() + msg.sync_source = "sim_clock" + msg.synchronized = True + msg.system_to_sensor_offset_ms = 0.0 + msg.system_to_external_truth_offset_ms = pose_status["age_sec"] * 1000.0 if pose_status["ready"] else 0.0 + msg.jitter_ms = 1.0 + msg.max_offset_ms = 2.0 + msg.sync_transport_latency_ms = 1.0 + msg.status_message = "simulated time sync status" + self.time_sync_status_publisher.publish(msg) + + def stop(self): + self.stop_requested = True + for _ in range(5): + self.publish_cmd(0.0, 0.0) + time.sleep(0.02) + + def validate_request_vehicle_id(self, request): + header = request.get("header", {}) + request_vehicle_id = header.get("vehicle_id", self.args.vehicle_id) + if request_vehicle_id and request_vehicle_id != self.args.vehicle_id: + raise ValueError(f"vehicle_id mismatch: expected {self.args.vehicle_id}, got {request_vehicle_id}") + return header.get("request_id", f"sim_job_{now_us()}") + + def execute_straight_line(self, request): + straight = request.get("straight_line", {}) + speed = abs(float(straight.get("target_speed_ms", 0.1))) + distance = abs(float(straight.get("target_distance_m", 0.0))) + reverse = bool(straight.get("reverse", False)) + if speed <= 0.0 or distance <= 0.0: + raise ValueError("straight_line requires positive target_speed_ms and target_distance_m") + speed = min(speed, self.args.max_speed_mps) + linear = -speed if reverse else speed + duration = min(distance / speed, self.motion_duration_limit(request)) + self.run_velocity_for(linear, 0.0, duration) + + def execute_arc(self, request): + arc = request.get("arc", {}) + speed = abs(float(arc.get("target_speed_ms", 0.1))) + radius = abs(float(arc.get("radius_m", 1.0))) + sweep = abs(float(arc.get("sweep_angle_deg", 0.0))) + clockwise = bool(arc.get("clockwise", False)) + if speed <= 0.0 or radius <= 0.0 or sweep <= 0.0: + raise ValueError("arc requires positive speed, radius, and sweep angle") + speed = min(speed, self.args.max_speed_mps) + angular = speed / radius + if clockwise: + angular = -angular + duration = min(math.radians(sweep) / abs(angular), self.motion_duration_limit(request)) + self.run_velocity_for(speed, angular, duration) + + def run_velocity_for(self, linear_x, angular_z, duration): + period = 1.0 / self.args.publish_hz + deadline = time.monotonic() + max(0.0, duration) + self.stop_requested = False + samples = 0 + while time.monotonic() < deadline and not self.stop_requested: + self.publish_cmd(linear_x, angular_z) + rclpy.spin_once(self.node, timeout_sec=0.0) + samples += 1 + time.sleep(period) + self.stop() + self.last_motion_summary = { + "duration_sec": max(0.0, duration), + "commanded_linear_x_ms": float(linear_x), + "commanded_angular_z_rads": float(angular_z), + "samples": samples, + "stopped_by_request": bool(self.stop_requested and time.monotonic() < deadline), + } + + def run_ramp_velocity(self, start_linear_x, target_linear_x, angular_z, ramp_duration, hold_duration): + period = 1.0 / self.args.publish_hz + self.stop_requested = False + samples = 0 + ramp_duration = max(0.0, ramp_duration) + hold_duration = max(0.0, hold_duration) + ramp_deadline = time.monotonic() + ramp_duration + start_time = time.monotonic() + while time.monotonic() < ramp_deadline and not self.stop_requested: + alpha = (time.monotonic() - start_time) / max(ramp_duration, 1e-6) + linear_x = start_linear_x + (target_linear_x - start_linear_x) * max(0.0, min(1.0, alpha)) + self.publish_cmd(linear_x, angular_z) + rclpy.spin_once(self.node, timeout_sec=0.0) + samples += 1 + time.sleep(period) + + hold_deadline = time.monotonic() + hold_duration + while time.monotonic() < hold_deadline and not self.stop_requested: + self.publish_cmd(target_linear_x, angular_z) + rclpy.spin_once(self.node, timeout_sec=0.0) + samples += 1 + time.sleep(period) + + stopped_by_request = bool(self.stop_requested) + self.stop() + self.last_motion_summary = { + "duration_sec": ramp_duration + hold_duration, + "commanded_linear_x_ms": float(target_linear_x), + "commanded_angular_z_rads": float(angular_z), + "samples": samples, + "stopped_by_request": stopped_by_request, + } + + def handle_motion_primitive(self, request): + try: + job_id = self.validate_request_vehicle_id(request) + except ValueError as exc: + return self.chassis_job_result( + f"sim_rejected_{now_us()}", + False, + ERROR_INVALID_ARGUMENT, + str(exc), + ) + + with self.lock: + if self.active_job_id: + return self.chassis_job_result( + job_id, + False, + ERROR_VEHICLE_BUSY, + f"vehicle is busy with {self.active_domain}:{self.active_job_id}", + ) + self.active_job_id = job_id + self.active_domain = "chassis" + selected = int(request.get("selected_primitive", 0)) + try: + if selected == 1: + self.execute_straight_line(request) + elif selected == 2: + self.execute_arc(request) + else: + return self.chassis_job_result( + job_id, + False, + ERROR_UNSUPPORTED_CAPABILITY, + f"unsupported simulated primitive: {selected}", + ) + except Exception as exc: + self.stop() + return self.chassis_job_result(job_id, False, ERROR_INVALID_ARGUMENT, str(exc)) + finally: + self.active_job_id = "" + self.active_domain = "" + + return self.chassis_job_result(job_id, True, ERROR_OK, "Isaac simulated motion primitive completed.") + + def chassis_job_result(self, job_id, success, error_code, message): + summary = dict(self.last_motion_summary) + return { + "success": success, + "error_code": {"code": error_code}, + "message": message, + "job_id": job_id, + "data_quality_passed": success, + "suitable_for_commit": success, + "recommended_parameter_version": "isaac_vehicle_agent_sim_v1", + "validation_summary": { + "max_lateral_error_m": 0.0, + "max_yaw_error_rad": 0.0, + "rms_lateral_error_m": 0.0, + "rms_yaw_error_rad": 0.0, + "repeatability_error_m": 0.0, + "curvature_error": 0.0, + "module_consistency_error": 0.0, + "auto_acceptance_passed": success, + }, + "artifacts": [ + { + "file_name": "sim_chassis_motion_summary.json", + "file_uri": "memory://vehicle_agent_sim/chassis_motion_summary", + "size_bytes": 0, + "description": json.dumps(summary, ensure_ascii=False, separators=(",", ":")), + } + ] if summary else [], + } + + def parse_trajectory_path(self, raw_path): + if len(raw_path) < 2: + raise ValueError("trajectory_tracking requires at least two path points") + + path = [] + for index, point in enumerate(raw_path): + try: + x_m = float(point.get("x_m")) + y_m = float(point.get("y_m")) + except (TypeError, ValueError) as exc: + raise ValueError(f"trajectory point {index} requires numeric x_m and y_m") from exc + + target_speed = abs(float(point.get("target_speed_ms", 0.0) or 0.0)) + path.append({ + "x": x_m, + "y": y_m, + "yaw": float(point.get("yaw_rad", 0.0) or 0.0), + "speed": target_speed, + }) + + cumulative = [0.0] + for index in range(len(path) - 1): + segment = math.hypot(path[index + 1]["x"] - path[index]["x"], path[index + 1]["y"] - path[index]["y"]) + if segment <= 1e-6: + raise ValueError(f"trajectory segment {index} is too short") + cumulative.append(cumulative[-1] + segment) + if cumulative[-1] <= 1e-6: + raise ValueError("trajectory length is too short") + return path, cumulative + + def assemble_trajectory_path(self, task, raw_path, request): + header = request.get("header", {}) + trajectory_id = str(task.get("trajectory_id") or header.get("request_id") or f"trajectory_{now_us()}") + segment_index = int(task.get("segment_index", 0) or 0) + total_segments = int(task.get("total_segments", 0) or 0) + is_final_segment = bool(task.get("is_final_segment", False)) + segmented = total_segments > 1 or segment_index > 0 + + if segment_index < 0: + raise ValueError("trajectory segment_index must be non-negative") + if not raw_path: + raise ValueError("trajectory segment path must be non-empty") + + meta = { + "trajectory_id": trajectory_id, + "segment_index": segment_index, + "total_segments": total_segments if total_segments > 0 else 1, + "is_final_segment": True, + "segment_cached": False, + } + + if not segmented: + return raw_path, meta + + if total_segments <= 1: + raise ValueError("segmented trajectory requires total_segments > 1") + if segment_index >= total_segments: + raise ValueError("trajectory segment_index must be less than total_segments") + + final_segment = is_final_segment or (segment_index + 1 == total_segments) + meta.update({ + "total_segments": total_segments, + "is_final_segment": final_segment, + "segment_cached": not final_segment, + }) + + cache_entry = self.trajectory_segment_cache.setdefault( + trajectory_id, + { + "total_segments": total_segments, + "segments": {}, + "created_at": time.monotonic(), + }, + ) + if cache_entry["total_segments"] != total_segments: + raise ValueError("trajectory total_segments changed for the same trajectory_id") + cache_entry["segments"][segment_index] = list(raw_path) + + if not final_segment: + self.last_control_summary = { + "task": "trajectory_tracking_segment_upload", + "trajectory_id": trajectory_id, + "segment_index": segment_index, + "total_segments": total_segments, + "segment_points": len(raw_path), + "cached_segments": len(cache_entry["segments"]), + "data_quality_passed": True, + } + return None, meta + + missing_segments = [index for index in range(total_segments) if index not in cache_entry["segments"]] + if missing_segments: + raise ValueError(f"trajectory missing segments before execution: {missing_segments}") + + assembled_path = [] + for index in range(total_segments): + segment = list(cache_entry["segments"][index]) + if index > 0 and assembled_path and segment: + previous = assembled_path[-1] + first = segment[0] + if math.hypot( + float(previous.get("x_m", 0.0)) - float(first.get("x_m", 0.0)), + float(previous.get("y_m", 0.0)) - float(first.get("y_m", 0.0)), + ) <= 1e-6: + segment = segment[1:] + assembled_path.extend(segment) + + del self.trajectory_segment_cache[trajectory_id] + meta["segment_cached"] = False + return assembled_path, meta + + def interpolate_path(self, path, cumulative, distance_along_path): + target_s = clamp(distance_along_path, 0.0, cumulative[-1]) + for index in range(len(path) - 1): + if target_s <= cumulative[index + 1]: + segment_len = cumulative[index + 1] - cumulative[index] + ratio = 0.0 if segment_len <= 1e-6 else (target_s - cumulative[index]) / segment_len + start = path[index] + end = path[index + 1] + yaw = math.atan2(end["y"] - start["y"], end["x"] - start["x"]) + speed = start["speed"] + (end["speed"] - start["speed"]) * ratio + return { + "x": start["x"] + (end["x"] - start["x"]) * ratio, + "y": start["y"] + (end["y"] - start["y"]) * ratio, + "yaw": yaw, + "speed": speed, + "segment_index": index, + "distance_along_path": target_s, + } + final = path[-1] + previous = path[-2] + return { + "x": final["x"], + "y": final["y"], + "yaw": math.atan2(final["y"] - previous["y"], final["x"] - previous["x"]), + "speed": final["speed"], + "segment_index": len(path) - 2, + "distance_along_path": cumulative[-1], + } + + def nearest_path_reference(self, path, cumulative, state): + best = None + px = state["x"] + py = state["y"] + for index in range(len(path) - 1): + start = path[index] + end = path[index + 1] + dx = end["x"] - start["x"] + dy = end["y"] - start["y"] + seg_len_sq = dx * dx + dy * dy + if seg_len_sq <= 1e-12: + continue + + ratio = clamp(((px - start["x"]) * dx + (py - start["y"]) * dy) / seg_len_sq, 0.0, 1.0) + proj_x = start["x"] + dx * ratio + proj_y = start["y"] + dy * ratio + error_x = px - proj_x + error_y = py - proj_y + dist_sq = error_x * error_x + error_y * error_y + seg_len = math.sqrt(seg_len_sq) + lateral_error = (dx * (py - start["y"]) - dy * (px - start["x"])) / seg_len + yaw = math.atan2(dy, dx) + speed = start["speed"] + (end["speed"] - start["speed"]) * ratio + distance_along = cumulative[index] + seg_len * ratio + candidate = { + "distance_sq": dist_sq, + "lateral_error": lateral_error, + "heading_error": normalize_angle(state["yaw"] - yaw), + "speed": speed, + "yaw": yaw, + "segment_index": index, + "distance_along_path": distance_along, + } + if best is None or dist_sq < best["distance_sq"]: + best = candidate + + if best is None: + raise ValueError("trajectory contains no valid segment") + return best + + @staticmethod + def rms(values): + if not values: + return 0.0 + return math.sqrt(sum(value * value for value in values) / len(values)) + + def execute_trajectory_tracking(self, request): + task = request.get("trajectory_tracking", {}) + raw_path = task.get("path", []) + if not raw_path: + raise ValueError("trajectory_tracking requires non-empty path") + + assembled_path, trajectory_meta = self.assemble_trajectory_path(task, raw_path, request) + if assembled_path is None: + return + + path, cumulative = self.parse_trajectory_path(assembled_path) + pose_constraints = self.trajectory_pose_constraints(task) + stop_at_end = bool(task.get("stop_at_end", True)) + timeout_sec = self.motion_duration_limit(task) + period = 1.0 / self.args.publish_hz + deadline = time.monotonic() + timeout_sec + started_at = time.monotonic() + self.stop_requested = False + self.last_control_summary = {} + + lateral_errors = [] + heading_errors = [] + speed_errors = [] + saturation_samples = 0 + samples = 0 + max_jerk = 0.0 + previous_command_speed = None + previous_accel = 0.0 + previous_sample_time = None + reached_goal = False + final_position_error = float("inf") + final_reference = None + stopped_by_request = False + pose_source = "" + + try: + self.wait_for_vehicle_state(pose_constraints) + while time.monotonic() < deadline and not self.stop_requested: + state = self.get_vehicle_state() + pose_error = self.state_pose_constraint_error(state, pose_constraints) + if pose_error: + raise ValueError(f"trajectory tracking invalid external pose: {pose_error}") + pose_source = state.get("pose_source", "") + + nearest = self.nearest_path_reference(path, cumulative, state) + distance_to_final = math.hypot(state["x"] - path[-1]["x"], state["y"] - path[-1]["y"]) + final_position_error = distance_to_final + if distance_to_final <= self.args.trajectory_goal_tolerance_m: + reached_goal = True + break + + lookahead_s = min( + cumulative[-1], + nearest["distance_along_path"] + self.args.trajectory_lookahead_m, + ) + target = self.interpolate_path(path, cumulative, lookahead_s) + target_dx = target["x"] - state["x"] + target_dy = target["y"] - state["y"] + target_distance = max(math.hypot(target_dx, target_dy), 1e-3) + target_heading = math.atan2(target_dy, target_dx) + alpha = normalize_angle(target_heading - state["yaw"]) + curvature = 2.0 * math.sin(alpha) / target_distance + + command_speed = target["speed"] if target["speed"] > 0.0 else self.args.default_tracking_speed_mps + command_speed = clamp(command_speed, self.args.min_tracking_speed_mps, self.args.max_speed_mps) + if distance_to_final < self.args.trajectory_slowdown_radius_m: + command_speed = min( + command_speed, + max(self.args.min_tracking_speed_mps, distance_to_final * self.args.trajectory_slowdown_gain), + ) + + command_angular = clamp( + command_speed * curvature, + -self.args.max_angular_speed_rps, + self.args.max_angular_speed_rps, + ) + saturated = abs(command_angular) >= (self.args.max_angular_speed_rps * 0.98) + if saturated: + saturation_samples += 1 + + now = time.monotonic() + if previous_command_speed is not None and previous_sample_time is not None: + dt = max(now - previous_sample_time, 1e-6) + accel = (command_speed - previous_command_speed) / dt + max_jerk = max(max_jerk, abs((accel - previous_accel) / dt)) + previous_accel = accel + previous_command_speed = command_speed + previous_sample_time = now + + lateral_errors.append(abs(nearest["lateral_error"])) + heading_errors.append(abs(nearest["heading_error"])) + speed_errors.append(abs(state["linear_velocity"] - command_speed)) + final_reference = nearest + samples += 1 + + self.publish_cmd(command_speed, command_angular) + time.sleep(period) + + stopped_by_request = bool(self.stop_requested) + if stopped_by_request: + raise ValueError("trajectory tracking was interrupted by stop request") + if not reached_goal: + raise ValueError("trajectory tracking timed out before reaching goal") + finally: + duration_sec = time.monotonic() - started_at + rms_lateral = self.rms(lateral_errors) + rms_heading = self.rms(heading_errors) + rms_speed = self.rms(speed_errors) + data_quality_passed = ( + reached_goal + and rms_lateral <= self.args.trajectory_max_rms_lateral_error_m + and rms_heading <= self.args.trajectory_max_rms_heading_error_rad + ) + self.last_control_summary = { + "task": "trajectory_tracking", + "trajectory_id": trajectory_meta["trajectory_id"], + "segment_index": trajectory_meta["segment_index"], + "total_segments": trajectory_meta["total_segments"], + "is_final_segment": trajectory_meta["is_final_segment"], + "path_points": len(path), + "path_length_m": cumulative[-1], + "duration_sec": duration_sec, + "samples": samples, + "reached_goal": reached_goal, + "stopped_by_request": stopped_by_request, + "final_position_error_m": 0.0 if not math.isfinite(final_position_error) else final_position_error, + "rms_lateral_error_m": rms_lateral, + "max_lateral_error_m": max(lateral_errors) if lateral_errors else 0.0, + "rms_heading_error_rad": rms_heading, + "max_heading_error_rad": max(heading_errors) if heading_errors else 0.0, + "rms_speed_error_ms": rms_speed, + "max_jerk": max_jerk, + "saturation_ratio": float(saturation_samples / samples) if samples else 0.0, + "last_path_distance_m": final_reference["distance_along_path"] if final_reference else 0.0, + "pose_source": pose_source, + "external_pose_topic": self.args.external_pose_topic, + "velocity_feedback_topic": self.args.chassis_telemetry_topic, + "required_external_pose_source_id": pose_constraints["required_source"], + "max_external_pose_age_ms": pose_constraints["max_age_sec"] * 1000.0, + "min_external_pose_quality_score": pose_constraints["min_quality"], + "data_quality_passed": data_quality_passed, + } + if stop_at_end or not reached_goal: + self.stop() + + def execute_velocity_step(self, request): + task = request.get("velocity_step", {}) + settle = max(0.0, float(task.get("settle_before_step_sec", 0.0))) + hold = max(0.0, float(task.get("hold_time_sec", 2.0))) + speed = min(abs(float(task.get("target_velocity_ms", 0.1))), self.args.max_speed_mps) + if settle > 0.0: + self.run_velocity_for(0.0, 0.0, min(settle, self.args.max_motion_duration_sec)) + self.run_velocity_for(speed, 0.0, min(hold, self.args.max_motion_duration_sec)) + + def execute_accel_decel(self, request): + task = request.get("accel_decel", {}) + start_speed = min(abs(float(task.get("start_velocity_ms", 0.0))), self.args.max_speed_mps) + target_speed = min(abs(float(task.get("target_velocity_ms", 0.1))), self.args.max_speed_mps) + accel = abs(float(task.get("target_accel_ms2", 0.1))) + hold = max(0.0, float(task.get("hold_time_sec", 1.0))) + if accel <= 0.0: + raise ValueError("accel_decel requires positive target_accel_ms2") + ramp_duration = abs(target_speed - start_speed) / accel + ramp_duration = min(ramp_duration, self.args.max_motion_duration_sec) + self.run_ramp_velocity(start_speed, target_speed, 0.0, ramp_duration, hold) + + def execute_stop_accuracy(self, request): + task = request.get("stop_accuracy", {}) + timeout = float(task.get("timeout_sec", 3.0) or 3.0) + speed = min(0.10, self.args.max_speed_mps) + self.run_velocity_for(speed, 0.0, min(timeout * 0.5, self.args.max_motion_duration_sec)) + + def handle_control_evaluation(self, request): + try: + job_id = self.validate_request_vehicle_id(request) + except ValueError as exc: + return self.control_job_result( + f"sim_control_rejected_{now_us()}", + False, + ERROR_INVALID_ARGUMENT, + str(exc), + ) + + with self.lock: + if self.active_job_id: + return self.control_job_result( + job_id, + False, + ERROR_VEHICLE_BUSY, + f"vehicle is busy with {self.active_domain}:{self.active_job_id}", + ) + self.active_job_id = job_id + self.active_domain = "control" + self.last_control_summary = {} + selected = int(request.get("selected_task", 0)) + try: + if selected == 1: + self.execute_trajectory_tracking(request) + elif selected == 2: + self.execute_velocity_step(request) + elif selected == 3: + self.execute_accel_decel(request) + elif selected == 4: + self.execute_stop_accuracy(request) + else: + return self.control_job_result( + job_id, + False, + ERROR_UNSUPPORTED_CAPABILITY, + f"unsupported simulated control task: {selected}", + ) + except Exception as exc: + self.stop() + return self.control_job_result(job_id, False, ERROR_INVALID_ARGUMENT, str(exc)) + finally: + self.active_job_id = "" + self.active_domain = "" + + return self.control_job_result(job_id, True, ERROR_OK, "Isaac simulated control evaluation completed.") + + def control_job_result(self, job_id, success, error_code, message): + summary = dict(self.last_control_summary) + data_quality_passed = bool(summary.get("data_quality_passed", success)) + suitable_for_commit = bool(success and data_quality_passed) + saturation_ratio = float(summary.get("saturation_ratio", 0.0 if success else 1.0)) + return { + "success": success, + "error_code": {"code": error_code}, + "message": message, + "job_id": job_id, + "data_quality_passed": data_quality_passed, + "suitable_for_commit": suitable_for_commit, + "recommended_parameter_version": "isaac_control_sim_v1", + "validation_summary": { + "rms_lateral_error_m": float(summary.get("rms_lateral_error_m", 0.0)), + "rms_heading_error_rad": float(summary.get("rms_heading_error_rad", 0.0)), + "rms_speed_error_ms": float(summary.get("rms_speed_error_ms", 0.0)), + "overshoot_ratio": 0.0, + "settle_time_sec": float(summary.get("duration_sec", self.last_motion_summary.get("duration_sec", 0.0))), + "stop_position_error_m": float(summary.get("final_position_error_m", 0.0)), + "max_jerk": float(summary.get("max_jerk", 0.0)), + "saturation_ratio": saturation_ratio, + "auto_acceptance_passed": data_quality_passed, + }, + "artifacts": [ + { + "file_name": "sim_control_task_summary.json", + "file_uri": "memory://vehicle_agent_sim/control_task_summary", + "size_bytes": 0, + "description": json.dumps(summary, ensure_ascii=False, separators=(",", ":")), + } + ] if summary else [], + } + + +class AgentTcpHandler(socketserver.BaseRequestHandler): + def handle(self): + agent = self.server.agent + try: + msg_type, payload = read_frame(self.request) + if msg_type == CHASSIS_GET_READINESS_REQ: + send_frame(self.request, CHASSIS_GET_READINESS_RSP, agent.readiness_payload("chassis")) + elif msg_type == CHASSIS_MOTION_PRIMITIVE_REQ: + send_frame(self.request, CHASSIS_MOTION_PRIMITIVE_RSP, agent.handle_motion_primitive(payload)) + elif msg_type == CHASSIS_EMERGENCY_BRAKE_REQ: + agent.stop() + send_frame( + self.request, + CHASSIS_EMERGENCY_BRAKE_RSP, + {"success": True, "error_code": {"code": ERROR_OK}, "message": "Isaac simulated brake executed."}, + ) + elif msg_type == CONTROL_GET_READINESS_REQ: + send_frame(self.request, CONTROL_GET_READINESS_RSP, agent.readiness_payload("control")) + elif msg_type == CONTROL_EVALUATION_REQ: + send_frame(self.request, CONTROL_EVALUATION_RSP, agent.handle_control_evaluation(payload)) + elif msg_type == EXTERNAL_POSE_PUSH_REQ: + send_frame(self.request, EXTERNAL_POSE_PUSH_RSP, agent.handle_external_pose_push(payload)) + else: + print(f"[WARN] unsupported vehicle_agent_sim msg_type={msg_type}") + except Exception as exc: + print(f"[WARN] vehicle_agent_sim request failed: {exc}") + + +class ThreadingTcpServer(socketserver.ThreadingMixIn, socketserver.TCPServer): + allow_reuse_address = True + daemon_threads = True + + +def create_server(agent, host, port): + server = ThreadingTcpServer((host, port), AgentTcpHandler) + server.agent = agent + return server + + +def serve(server, name): + host, port = server.server_address + print(f"[*] {name} vehicle agent sim listening on {host}:{port}") + server.serve_forever() + + +def parse_args(): + parser = argparse.ArgumentParser(description="Isaac vehicle-side agent simulator") + parser.add_argument("--vehicle-id", default="demo_agv_001") + parser.add_argument("--cmd-vel-topic", default="/vehicle/demo_agv_001/actuator/cmd_vel") + parser.add_argument("--external-pose-topic", default="/isaac/external_localization/telemetry") + parser.add_argument( + "--external-pose-transport", + choices=["ros_topic", "wifi6_tcp", "both"], + default="both", + help="外部真值位姿输入方式;现场/闭环仿真建议使用 wifi6_tcp", + ) + parser.add_argument("--external-pose-port", type=int, default=9004) + parser.add_argument("--external-pose-min-quality", type=float, default=0.1) + parser.add_argument("--chassis-telemetry-topic", default="/chassis/telemetry") + parser.add_argument("--allow-chassis-pose-fallback", action="store_true") + parser.add_argument("--bind-host", default="0.0.0.0") + parser.add_argument("--chassis-port", type=int, default=9001) + parser.add_argument("--control-port", type=int, default=9002) + parser.add_argument("--publish-hz", type=float, default=20.0) + parser.add_argument("--max-speed-mps", type=float, default=0.3) + parser.add_argument("--wheel-base-m", type=float, default=0.80) + parser.add_argument("--max-steering-angle-rad", type=float, default=0.60) + parser.add_argument("--internal-command-timeout-sec", type=float, default=0.5) + parser.add_argument("--internal-ackermann-command-topic", default="/vehicle/demo_agv_001/internal/ackermann_cmd") + parser.add_argument("--internal-state-topic", default="/vehicle/demo_agv_001/internal/state") + parser.add_argument("--internal-health-topic", default="/vehicle/demo_agv_001/internal/health") + parser.add_argument("--internal-time-sync-topic", default="/vehicle/demo_agv_001/internal/time_sync_status") + parser.add_argument("--max-angular-speed-rps", type=float, default=1.2) + parser.add_argument("--max-motion-duration-sec", type=float, default=60.0) + parser.add_argument("--pose-wait-timeout-sec", type=float, default=2.0) + parser.add_argument("--pose-stale-timeout-sec", type=float, default=0.5) + parser.add_argument("--trajectory-lookahead-m", type=float, default=0.45) + parser.add_argument("--trajectory-goal-tolerance-m", type=float, default=0.08) + parser.add_argument("--trajectory-slowdown-radius-m", type=float, default=0.5) + parser.add_argument("--trajectory-slowdown-gain", type=float, default=0.8) + parser.add_argument("--default-tracking-speed-mps", type=float, default=0.12) + parser.add_argument("--min-tracking-speed-mps", type=float, default=0.03) + parser.add_argument("--trajectory-max-rms-lateral-error-m", type=float, default=0.20) + parser.add_argument("--trajectory-max-rms-heading-error-rad", type=float, default=0.60) + return parser.parse_args() + + +def main(): + args = parse_args() + rclpy.init(args=None) + agent = IsaacVehicleAgentSim(args) + servers = [] + servers_started = False + try: + servers.append((create_server(agent, args.bind_host, args.chassis_port), "chassis")) + servers.append((create_server(agent, args.bind_host, args.control_port), "control")) + if args.external_pose_transport in ("wifi6_tcp", "both"): + servers.append((create_server(agent, args.bind_host, args.external_pose_port), "external-pose")) + for server, name in servers: + threading.Thread(target=serve, args=(server, name), daemon=True).start() + servers_started = True + print(f"[*] publishing private actuator commands to {args.cmd_vel_topic}") + while rclpy.ok(): + rclpy.spin_once(agent.node, timeout_sec=0.1) + except OSError as exc: + print(f"[ERROR] vehicle_agent_sim network startup failed: {exc}") + return 1 + except KeyboardInterrupt: + print("[*] vehicle_agent_sim shutdown requested.") + finally: + if servers_started: + for server, _name in servers: + server.shutdown() + for server, _name in servers: + server.server_close() + try: + if rclpy.ok(): + agent.stop() + except Exception as exc: + print(f"[WARN] vehicle_agent_sim stop during shutdown failed: {exc}") + agent.node.destroy_node() + if rclpy.ok(): + rclpy.shutdown() + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/agv_calib_brain/src/simulation/vehicle_sensor_agent_sim/README.md b/agv_calib_brain/src/simulation/vehicle_sensor_agent_sim/README.md new file mode 100644 index 0000000..35d5910 --- /dev/null +++ b/agv_calib_brain/src/simulation/vehicle_sensor_agent_sim/README.md @@ -0,0 +1,79 @@ +# 仿真车端传感器代理 + +这个模块模拟真实 Windows 小车上的传感器采集服务。 + +职责: + +- 订阅 Isaac 发布的车载传感器 ROS2 topic。 +- 将前视相机、下视相机、3D 激光雷达、2D 激光雷达、IMU 转成与 `VehicleSensorFrame` 对齐的 JSON payload。 +- 默认由统一车端 agent 对外提供;本模块也可作为独立传感器后端调试使用。 +- 对图像和点云支持按 `fragment_index / total_fragments` 分片读取。 +- 发布车端内部 `SensorLinkStatus`,用于描述传感器在线、帧率、丢帧和延迟。 + +默认一键仿真的 TCP 端口: + +```text +9000 车端统一 WiFi6/TCP 入口 +``` + +默认订阅: + +```text +/sensor/front_camera/image_raw +/sensor/down_camera/image_raw +/sensor/lidar_3d/pointcloud +/sensor/lidar_2d/scan +/sensor/imu/data +``` + +默认内部状态话题: + +```text +/vehicle//internal/sensor_link_status +``` + +启动: + +```bash +source install/setup.bash +python3 src/simulation/tools/launch_sim_stack.py --component sensor-agent +``` + +无 Isaac 验收: + +```bash +source install/setup.bash +python3 src/simulation/tools/smoke_test_vehicle_sensor_agent.py --fake-sensors +``` + +帧类型: + +```text +21 SENSOR_GET_READINESS_REQ +22 SENSOR_GET_READINESS_RSP +23 SENSOR_GET_LATEST_FRAME_REQ +24 SENSOR_GET_LATEST_FRAME_RSP +25 SENSOR_LIST_SENSORS_REQ +26 SENSOR_LIST_SENSORS_RSP +``` + +正式现场部署时,真实 Windows 车端应订阅本车传感器驱动数据,再按 `sensor_calibration.proto` 中的 `VehicleSensorFrame` 通过 WiFi6 提供给车间电脑。 + +车间侧接入: + +```bash +source install/setup.bash +python3 src/simulation/tools/launch_sim_stack.py --component sensor-ingest +``` + +默认会通过车端统一 `9000` 端口读取最新帧,再重新发布到: + +```text +/workshop/vehicle_sensor/demo_front_camera/image_raw +/workshop/vehicle_sensor/demo_down_camera/image_raw +/workshop/vehicle_sensor/demo_lidar_3d/pointcloud +/workshop/vehicle_sensor/demo_lidar_2d/scan +/workshop/vehicle_sensor/demo_imu/data +``` + +后续传感器标定算法应订阅这些车间侧 topic,而不是直接订阅 Isaac 原始 `/sensor/...` topic。 diff --git a/agv_calib_brain/src/simulation/vehicle_sensor_agent_sim/scripts/isaac_vehicle_sensor_agent_sim.py b/agv_calib_brain/src/simulation/vehicle_sensor_agent_sim/scripts/isaac_vehicle_sensor_agent_sim.py new file mode 100644 index 0000000..357f104 --- /dev/null +++ b/agv_calib_brain/src/simulation/vehicle_sensor_agent_sim/scripts/isaac_vehicle_sensor_agent_sim.py @@ -0,0 +1,554 @@ +#!/usr/bin/env python3 +"""Vehicle-side sensor agent simulator for Isaac validation. + +The process represents the sensor-facing part of the vehicle computer. It +subscribes to Isaac ROS sensor topics and exposes the latest frames through the +same simple TCP frame envelope used by the chassis/control simulator. +""" + +from __future__ import annotations + +import argparse +import base64 +import hashlib +import json +import math +import socket +import socketserver +import struct +import threading +import time +from dataclasses import dataclass, field +from typing import Any + +import rclpy +from sensor_msgs.msg import Image, Imu, LaserScan, PointCloud2 + +try: + from vehicle_internal_interfaces.msg import SensorLinkStatus +except ImportError: + SensorLinkStatus = None + + +SENSOR_GET_READINESS_REQ = 21 +SENSOR_GET_READINESS_RSP = 22 +SENSOR_GET_LATEST_FRAME_REQ = 23 +SENSOR_GET_LATEST_FRAME_RSP = 24 +SENSOR_LIST_SENSORS_REQ = 25 +SENSOR_LIST_SENSORS_RSP = 26 + +ERROR_OK = 1 +ERROR_INVALID_ARGUMENT = 2 +ERROR_INVALID_STATE = 3 + +SENSOR_TYPE_DOWNWARD_CAMERA = 1 +SENSOR_TYPE_FRONT_CAMERA = 2 +SENSOR_TYPE_LIDAR_3D = 4 +SENSOR_TYPE_LIDAR_2D = 5 +SENSOR_TYPE_IMU = 6 + +PAYLOAD_IMAGE = 1 +PAYLOAD_POINT_CLOUD = 2 +PAYLOAD_LASER_SCAN = 3 +PAYLOAD_IMU = 4 + + +def now_us() -> int: + return int(time.time() * 1_000_000) + + +def ros_stamp_to_us(stamp: Any) -> int: + sec = int(getattr(stamp, "sec", 0)) + nanosec = int(getattr(stamp, "nanosec", 0)) + if sec == 0 and nanosec == 0: + return now_us() + return sec * 1_000_000 + nanosec // 1000 + + +def read_exactly(conn: socket.socket, count: int) -> bytes: + chunks: list[bytes] = [] + remaining = count + while remaining > 0: + chunk = conn.recv(remaining) + if not chunk: + raise ConnectionError("connection closed") + 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(" tuple[int, dict[str, Any]]: + header = read_exactly(conn, 8) + msg_type, payload_len = struct.unpack(" dict[str, SensorSpec]: + return { + "front_camera": SensorSpec(args.front_camera_sensor_id, SENSOR_TYPE_FRONT_CAMERA, PAYLOAD_IMAGE, args.front_camera_topic, "front_camera_link"), + "down_camera": SensorSpec(args.down_camera_sensor_id, SENSOR_TYPE_DOWNWARD_CAMERA, PAYLOAD_IMAGE, args.down_camera_topic, "down_camera_link"), + "lidar_3d": SensorSpec(args.lidar_3d_sensor_id, SENSOR_TYPE_LIDAR_3D, PAYLOAD_POINT_CLOUD, args.lidar_3d_topic, "lidar_3d_link"), + "lidar_2d": SensorSpec(args.lidar_2d_sensor_id, SENSOR_TYPE_LIDAR_2D, PAYLOAD_LASER_SCAN, args.lidar_2d_topic, "lidar_2d_link"), + "imu": SensorSpec(args.imu_sensor_id, SENSOR_TYPE_IMU, PAYLOAD_IMU, args.imu_topic, "imu_link"), + } + + def add_subscription(self, key: str, msg_type: Any, callback: Any) -> None: + sensor = self.sensors[key] + if not self.sensor_enabled(sensor): + return + self.subscriptions.append(self.node.create_subscription(msg_type, sensor.topic, lambda msg, key=key: callback(key, msg), 20)) + + def sensor_enabled(self, sensor: SensorSpec) -> bool: + return not self.args.enabled_sensor_ids or sensor.sensor_id in self.args.enabled_sensor_ids + + def next_sequence(self, sensor_id: str) -> int: + with self.lock: + next_value = self.sequence_by_sensor.get(sensor_id, 0) + 1 + self.sequence_by_sensor[sensor_id] = next_value + return next_value + + def store_frame(self, frame: LatestFrame) -> None: + with self.lock: + self.latest_frames[frame.sensor.sensor_id] = frame + + def on_image(self, key: str, msg: Image) -> None: + sensor = self.sensors[key] + raw = bytes(msg.data) + payload = { + "width": int(msg.width), + "height": int(msg.height), + "encoding": str(msg.encoding), + "is_bigendian": bool(msg.is_bigendian), + "step": int(msg.step), + "compression": "none", + } + frame = LatestFrame( + sensor=sensor, + hardware_timestamp_us=ros_stamp_to_us(msg.header.stamp), + sequence_id=self.next_sequence(sensor.sensor_id), + received_monotonic=time.monotonic(), + payload={"image": payload}, + raw_bytes=raw, + raw_field_path=("image", "data"), + uncompressed_size_bytes=len(raw), + digest=f"sha256:{hashlib.sha256(raw).hexdigest()}", + ) + self.store_frame(frame) + + def on_point_cloud(self, key: str, msg: PointCloud2) -> None: + sensor = self.sensors[key] + raw = bytes(msg.data) + fields = [ + { + "name": str(field.name), + "offset": int(field.offset), + "datatype": int(field.datatype), + "count": int(field.count), + } + for field in msg.fields + ] + payload = { + "height": int(msg.height), + "width": int(msg.width), + "fields": fields, + "is_bigendian": bool(msg.is_bigendian), + "point_step": int(msg.point_step), + "row_step": int(msg.row_step), + "is_dense": bool(msg.is_dense), + "compression": "none", + } + frame = LatestFrame( + sensor=sensor, + hardware_timestamp_us=ros_stamp_to_us(msg.header.stamp), + sequence_id=self.next_sequence(sensor.sensor_id), + received_monotonic=time.monotonic(), + payload={"point_cloud": payload}, + raw_bytes=raw, + raw_field_path=("point_cloud", "data"), + uncompressed_size_bytes=len(raw), + digest=f"sha256:{hashlib.sha256(raw).hexdigest()}", + ) + self.store_frame(frame) + + def on_laser_scan(self, key: str, msg: LaserScan) -> None: + sensor = self.sensors[key] + payload = { + "angle_min_rad": float(msg.angle_min), + "angle_max_rad": float(msg.angle_max), + "angle_increment_rad": float(msg.angle_increment), + "time_increment_sec": float(msg.time_increment), + "scan_time_sec": float(msg.scan_time), + "range_min_m": float(msg.range_min), + "range_max_m": float(msg.range_max), + "ranges_m": [float(value) for value in msg.ranges], + "intensities": [float(value) for value in msg.intensities], + } + frame = LatestFrame( + sensor=sensor, + hardware_timestamp_us=ros_stamp_to_us(msg.header.stamp), + sequence_id=self.next_sequence(sensor.sensor_id), + received_monotonic=time.monotonic(), + payload={"laser_scan": payload}, + ) + self.store_frame(frame) + + def on_imu(self, key: str, msg: Imu) -> None: + sensor = self.sensors[key] + payload = { + "orientation_x": float(msg.orientation.x), + "orientation_y": float(msg.orientation.y), + "orientation_z": float(msg.orientation.z), + "orientation_w": float(msg.orientation.w), + "orientation_covariance": [float(value) for value in msg.orientation_covariance], + "angular_velocity": { + "x": float(msg.angular_velocity.x), + "y": float(msg.angular_velocity.y), + "z": float(msg.angular_velocity.z), + }, + "angular_velocity_covariance": [float(value) for value in msg.angular_velocity_covariance], + "linear_acceleration": { + "x": float(msg.linear_acceleration.x), + "y": float(msg.linear_acceleration.y), + "z": float(msg.linear_acceleration.z), + }, + "linear_acceleration_covariance": [float(value) for value in msg.linear_acceleration_covariance], + } + frame = LatestFrame( + sensor=sensor, + hardware_timestamp_us=ros_stamp_to_us(msg.header.stamp), + sequence_id=self.next_sequence(sensor.sensor_id), + received_monotonic=time.monotonic(), + payload={"imu": payload}, + ) + self.store_frame(frame) + + def list_sensors_payload(self) -> dict[str, Any]: + sensors = [] + for sensor in self.sensors.values(): + sensors.append({ + "sensor_id": sensor.sensor_id, + "sensor_type": sensor.sensor_type, + "payload_type": sensor.payload_type, + "topic": sensor.topic, + "frame_id": sensor.frame_id, + "enabled": self.sensor_enabled(sensor), + }) + return { + "success": True, + "error_code": {"code": ERROR_OK}, + "message": "Isaac vehicle sensor agent sim: sensors listed.", + "sensors": sensors, + } + + def readiness_payload(self) -> dict[str, Any]: + now = time.monotonic() + ready_sensor_ids = [] + per_sensor = [] + with self.lock: + latest_frames = dict(self.latest_frames) + + active_sensors = [sensor for sensor in self.sensors.values() if self.sensor_enabled(sensor)] + for sensor in active_sensors: + frame = latest_frames.get(sensor.sensor_id) + age_sec = (now - frame.received_monotonic) if frame is not None else math.inf + ready = frame is not None and age_sec <= self.args.frame_stale_timeout_sec + if ready: + ready_sensor_ids.append(sensor.sensor_id) + per_sensor.append({ + "sensor_id": sensor.sensor_id, + "sensor_type": sensor.sensor_type, + "topic": sensor.topic, + "ready": ready, + "age_ms": None if not math.isfinite(age_sec) else age_sec * 1000.0, + "sequence_id": 0 if frame is None else frame.sequence_id, + }) + + required_sensor_ids = [sensor.sensor_id for sensor in active_sensors] + all_ready = all(sensor_id in ready_sensor_ids for sensor_id in required_sensor_ids) + return { + "success": True, + "error_code": {"code": ERROR_OK}, + "message": "Isaac vehicle sensor agent sim: sensor readiness checked.", + "agent_ready": True, + "capture_pipeline_ready": all_ready, + "storage_ready": True, + "telemetry_ready": True, + "vehicle_safe_to_move": True, + "arm_ready": True, + "ready_sensor_ids": ready_sensor_ids, + "issues": [] if all_ready else [ + { + "resource_id": sensor["sensor_id"], + "message": f"sensor frame unavailable or stale on {sensor['topic']}", + "severity": 2, + } + for sensor in per_sensor + if not sensor["ready"] + ], + "checked_timestamp_us": now_us(), + "sensor_data_stream_ready": bool(ready_sensor_ids), + "streaming_sensor_ids": ready_sensor_ids, + "sensor_data_transport": "wifi6_sim_tcp", + "per_sensor": per_sensor, + } + + def publish_sensor_link_status(self) -> None: + if self.sensor_link_status_publisher is None: + return + + now = time.monotonic() + with self.lock: + latest_frames = dict(self.latest_frames) + for sensor in self.sensors.values(): + if not self.sensor_enabled(sensor): + continue + frame = latest_frames.get(sensor.sensor_id) + age_sec = math.inf if frame is None else now - frame.received_monotonic + online = frame is not None and age_sec <= self.args.frame_stale_timeout_sec + + msg = SensorLinkStatus() + msg.hardware_timestamp_us = now_us() + msg.sensor_id = sensor.sensor_id + msg.sensor_type.value = sensor.sensor_type + msg.frame_id = sensor.frame_id + msg.source_channel = sensor.topic + msg.online = online + msg.frame_rate_hz = 0.0 if frame is None else min(1.0 / max(age_sec, 1e-3), 1000.0) + msg.latest_frame_age_ms = 0.0 if not math.isfinite(age_sec) else age_sec * 1000.0 + msg.frame_count = 0 if frame is None else frame.sequence_id + msg.dropped_frame_ratio = 0.0 + msg.latest_transport_latency_ms = msg.latest_frame_age_ms + msg.status_message = "online" if online else "sensor frame unavailable or stale" + self.sensor_link_status_publisher.publish(msg) + + def frame_payload(self, request: dict[str, Any]) -> dict[str, Any]: + sensor_id = str(request.get("sensor_id", "")) + if not sensor_id: + raise ValueError("sensor_id is required") + include_payload = bool(request.get("include_payload", True)) + fragment_index = int(request.get("fragment_index", 0) or 0) + if fragment_index < 0: + raise ValueError("fragment_index must be non-negative") + max_payload_bytes = int(request.get("max_payload_bytes", 0) or 0) + if max_payload_bytes <= 0: + max_payload_bytes = self.args.max_frame_payload_bytes + + with self.lock: + frame = self.latest_frames.get(sensor_id) + if frame is None: + return { + "success": False, + "error_code": {"code": ERROR_INVALID_STATE}, + "message": f"no frame has been received for sensor_id={sensor_id}", + } + + sensor_frame = self.encode_vehicle_sensor_frame(frame, include_payload, max_payload_bytes, fragment_index) + return { + "success": True, + "error_code": {"code": ERROR_OK}, + "message": "Isaac vehicle sensor agent sim: latest frame returned.", + "sensor_frame": sensor_frame, + } + + def encode_vehicle_sensor_frame( + self, + frame: LatestFrame, + include_payload: bool, + max_payload_bytes: int, + fragment_index: int, + ) -> dict[str, Any]: + payload = json.loads(json.dumps(frame.payload, separators=(",", ":"))) + total_fragments = 1 + fragment = b"" + if include_payload and frame.raw_bytes: + max_payload_bytes = max(1, max_payload_bytes) + total_fragments = max(1, math.ceil(len(frame.raw_bytes) / max_payload_bytes)) + if fragment_index >= total_fragments: + raise ValueError(f"fragment_index {fragment_index} exceeds total_fragments {total_fragments}") + start = fragment_index * max_payload_bytes + end = min(len(frame.raw_bytes), start + max_payload_bytes) + fragment = frame.raw_bytes[start:end] + if frame.raw_field_path is not None: + object_name, field_name = frame.raw_field_path + payload[object_name][field_name] = base64.b64encode(fragment).decode("ascii") + elif frame.raw_field_path is not None: + object_name, field_name = frame.raw_field_path + payload[object_name][field_name] = "" + + sensor_frame = { + "header": { + "sensor_id": frame.sensor.sensor_id, + "sensor_type": frame.sensor.sensor_type, + "frame_id": frame.sensor.frame_id, + "hardware_timestamp_us": frame.hardware_timestamp_us, + "sequence_id": frame.sequence_id, + "active_job_id": frame.active_job_id, + "capture_intent": frame.capture_intent, + }, + "payload_type": frame.sensor.payload_type, + "stream_id": f"{frame.sensor.sensor_id}:{frame.sequence_id}", + "fragment_index": fragment_index, + "total_fragments": total_fragments, + "is_final_fragment": fragment_index + 1 >= total_fragments, + "uncompressed_size_bytes": frame.uncompressed_size_bytes, + "payload_digest": frame.digest, + } + sensor_frame.update(payload) + sensor_frame["fragment_payload_bytes"] = len(fragment) + sensor_frame["received_age_ms"] = (time.monotonic() - frame.received_monotonic) * 1000.0 + return sensor_frame + + def handle_request(self, msg_type: int, payload: dict[str, Any]) -> tuple[int, dict[str, Any]]: + if msg_type == SENSOR_GET_READINESS_REQ: + return SENSOR_GET_READINESS_RSP, self.readiness_payload() + if msg_type == SENSOR_GET_LATEST_FRAME_REQ: + return SENSOR_GET_LATEST_FRAME_RSP, self.frame_payload(payload) + if msg_type == SENSOR_LIST_SENSORS_REQ: + return SENSOR_LIST_SENSORS_RSP, self.list_sensors_payload() + return SENSOR_GET_READINESS_RSP, { + "success": False, + "error_code": {"code": ERROR_INVALID_ARGUMENT}, + "message": f"unsupported sensor msg_type={msg_type}", + } + + +class AgentTcpHandler(socketserver.BaseRequestHandler): + def handle(self) -> None: + agent: IsaacVehicleSensorAgentSim = self.server.agent + try: + msg_type, payload = read_frame(self.request) + response_type, response_payload = agent.handle_request(msg_type, payload) + except Exception as exc: + response_type = SENSOR_GET_READINESS_RSP + response_payload = { + "success": False, + "error_code": {"code": ERROR_INVALID_ARGUMENT}, + "message": str(exc), + } + send_frame(self.request, response_type, response_payload) + + +class ThreadingTcpServer(socketserver.ThreadingMixIn, socketserver.TCPServer): + allow_reuse_address = True + daemon_threads = True + + +def create_server(agent: IsaacVehicleSensorAgentSim, host: str, port: int) -> ThreadingTcpServer: + server = ThreadingTcpServer((host, port), AgentTcpHandler) + server.agent = agent + return server + + +def serve(server: ThreadingTcpServer) -> None: + host, port = server.server_address + print(f"[*] sensor vehicle agent sim listening on {host}:{port}") + server.serve_forever() + + +def parse_args() -> argparse.Namespace: + parser = argparse.ArgumentParser(description="Isaac vehicle-side sensor agent simulator") + parser.add_argument("--bind-host", default="0.0.0.0") + parser.add_argument("--sensor-port", type=int, default=9003) + parser.add_argument("--max-frame-payload-bytes", type=int, default=262144) + parser.add_argument("--frame-stale-timeout-sec", type=float, default=1.0) + parser.add_argument("--enabled-sensor-ids", nargs="*", default=[], help="只启用指定 sensor_id;默认启用全部") + parser.add_argument("--sensor-link-status-topic", default="/vehicle/demo_agv_001/internal/sensor_link_status") + + parser.add_argument("--front-camera-sensor-id", default="demo_front_camera") + parser.add_argument("--down-camera-sensor-id", default="demo_down_camera") + parser.add_argument("--lidar-3d-sensor-id", default="demo_lidar_3d") + parser.add_argument("--lidar-2d-sensor-id", default="demo_lidar_2d") + parser.add_argument("--imu-sensor-id", default="demo_imu") + + parser.add_argument("--front-camera-topic", default="/sensor/front_camera/image_raw") + parser.add_argument("--down-camera-topic", default="/sensor/down_camera/image_raw") + parser.add_argument("--lidar-3d-topic", default="/sensor/lidar_3d/pointcloud") + parser.add_argument("--lidar-2d-topic", default="/sensor/lidar_2d/scan") + parser.add_argument("--imu-topic", default="/sensor/imu/data") + return parser.parse_args() + + +def main() -> int: + args = parse_args() + rclpy.init(args=None) + agent = IsaacVehicleSensorAgentSim(args) + server = None + try: + server = create_server(agent, args.bind_host, args.sensor_port) + threading.Thread(target=serve, args=(server,), daemon=True).start() + print("[*] subscribed vehicle sensor topics:") + for sensor in agent.sensors.values(): + print(f" - {sensor.sensor_id}: {sensor.topic}") + while rclpy.ok(): + rclpy.spin_once(agent.node, timeout_sec=0.1) + except OSError as exc: + print(f"[ERROR] vehicle_sensor_agent_sim network startup failed: {exc}") + return 1 + except KeyboardInterrupt: + print("[*] vehicle_sensor_agent_sim shutdown requested.") + finally: + if server is not None: + server.shutdown() + server.server_close() + agent.node.destroy_node() + if rclpy.ok(): + rclpy.shutdown() + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/agv_calib_brain/src/site_deployment/README.md b/agv_calib_brain/src/site_deployment/README.md new file mode 100644 index 0000000..f65a0af --- /dev/null +++ b/agv_calib_brain/src/site_deployment/README.md @@ -0,0 +1,13 @@ +# 现场部署代码 + +这个目录用于存放真实现场设备相关代码。 + +现场部署代码应该复用仿真阶段已经验证过的外部接口语义,但需要把仿真适配器替换成真实适配器。 + +当前边界: + +- `vehicle_agent_real`:真实车端电脑适配器。 +- 现场专用 launch/config 文件应放在 `src/deployment/profiles`。 +- 真实安全检查必须放在这里,或者放在真实车端控制器里,不能放进 Isaac 仿真代码。 + +这个目录不要引入 Isaac API。 diff --git a/agv_calib_brain/src/site_deployment/vehicle_agent_real/README.md b/agv_calib_brain/src/site_deployment/vehicle_agent_real/README.md new file mode 100644 index 0000000..00891a1 --- /dev/null +++ b/agv_calib_brain/src/site_deployment/vehicle_agent_real/README.md @@ -0,0 +1,14 @@ +# 真实车端代理 + +这个目录用于存放真实 AGV 小车上的车端部署适配代码。 + +它应该暴露与 `vehicle_agent_sim` 相同的 TCP 协议,但底层执行不再调用 Isaac 控制,而是对接真实车辆 SDK、PLC 接口、CAN 网关或厂商控制器接口。 + +现场使用前必须满足: + +- 拒绝 `vehicle_id` 不匹配的命令。 +- 同一时间只允许一个活动任务。 +- gateway 断连或命令超时时必须停车。 +- 支持紧急制动。 +- 上报 readiness 状态和遥测健康状态。 +- 对每一条被接受的命令和最终结果记录时间戳日志。 diff --git a/agv_calib_brain/src/agv_calib_core/vehicle_agent_windows/CMakeLists.txt b/agv_calib_brain/src/site_deployment/vehicle_agent_real/vehicle_agent_windows/CMakeLists.txt similarity index 100% rename from agv_calib_brain/src/agv_calib_core/vehicle_agent_windows/CMakeLists.txt rename to agv_calib_brain/src/site_deployment/vehicle_agent_real/vehicle_agent_windows/CMakeLists.txt diff --git a/agv_calib_brain/src/agv_calib_core/vehicle_agent_windows/docs/protocol.md b/agv_calib_brain/src/site_deployment/vehicle_agent_real/vehicle_agent_windows/docs/protocol.md similarity index 96% rename from agv_calib_brain/src/agv_calib_core/vehicle_agent_windows/docs/protocol.md rename to agv_calib_brain/src/site_deployment/vehicle_agent_real/vehicle_agent_windows/docs/protocol.md index 0debc66..b8b7275 100644 --- a/agv_calib_brain/src/agv_calib_core/vehicle_agent_windows/docs/protocol.md +++ b/agv_calib_brain/src/site_deployment/vehicle_agent_real/vehicle_agent_windows/docs/protocol.md @@ -368,7 +368,14 @@ Ubuntu 发送,让车端按当前控制参数执行一次评估任务。`select {"x_m": 10.0, "y_m": 0.0, "yaw_rad": 0.0, "target_speed_ms": 0.0} ], "stop_at_end": true, - "timeout_sec": 60.0 + "timeout_sec": 60.0, + "required_external_pose_source_id": "workshop_external_truth", + "max_external_pose_age_ms": 100.0, + "min_external_pose_quality_score": 0.8, + "trajectory_id": "traj_001", + "segment_index": 0, + "total_segments": 1, + "is_final_segment": true } } ``` @@ -392,7 +399,14 @@ Ubuntu 发送,让车端按当前控制参数执行一次评估任务。`select {"x_m": 0.0, "y_m": 0.0, "yaw_rad": 0.0, "target_speed_ms": 0.5} ], "stop_at_end": true, - "timeout_sec": 60.0 + "timeout_sec": 60.0, + "required_external_pose_source_id": "workshop_external_truth", + "max_external_pose_age_ms": 100.0, + "min_external_pose_quality_score": 0.8, + "trajectory_id": "traj_001", + "segment_index": 0, + "total_segments": 1, + "is_final_segment": true } ``` @@ -538,7 +552,7 @@ Ubuntu 侧提供了 Python stub 服务器用于 Windows 侧联调前的自测: ```bash # 在 Ubuntu 上运行,模拟 Windows 车端(返回空 payload 的成功响应) -python3 src/win_ubuntu_bridge/vehicle_agent_gateway/scripts/windows_stub_server.py +python3 src/communication/win_ubuntu_bridge/vehicle_agent_gateway/scripts/windows_stub_server.py ``` --- diff --git a/agv_calib_brain/src/agv_calib_core/vehicle_agent_windows/frame_codec.py b/agv_calib_brain/src/site_deployment/vehicle_agent_real/vehicle_agent_windows/frame_codec.py similarity index 95% rename from agv_calib_brain/src/agv_calib_core/vehicle_agent_windows/frame_codec.py rename to agv_calib_brain/src/site_deployment/vehicle_agent_real/vehicle_agent_windows/frame_codec.py index d2c3e4a..0de0274 100644 --- a/agv_calib_brain/src/agv_calib_core/vehicle_agent_windows/frame_codec.py +++ b/agv_calib_brain/src/site_deployment/vehicle_agent_real/vehicle_agent_windows/frame_codec.py @@ -192,6 +192,11 @@ def make_control_readiness_response( estop_released: bool, vehicle_safe_to_move: bool, checked_timestamp_us: int, + external_pose_feedback_ready: bool = False, + external_pose_source_name: str = "", + external_pose_age_ms: float = 0.0, + external_pose_quality_score: float = 0.0, + external_pose_transport: str = "", error_code: ErrorCode = ErrorCode.OK, ) -> Dict[str, Any]: """构造运控就绪响应 payload(对应 ControlReadinessResponse.msg)。""" @@ -206,6 +211,11 @@ def make_control_readiness_response( "estop_released": estop_released, "vehicle_safe_to_move": vehicle_safe_to_move, "checked_timestamp_us": checked_timestamp_us, + "external_pose_feedback_ready": external_pose_feedback_ready, + "external_pose_source_name": external_pose_source_name, + "external_pose_age_ms": external_pose_age_ms, + "external_pose_quality_score": external_pose_quality_score, + "external_pose_transport": external_pose_transport, } diff --git a/agv_calib_brain/src/agv_calib_core/vehicle_agent_windows/include/chassis_domain_server.hpp b/agv_calib_brain/src/site_deployment/vehicle_agent_real/vehicle_agent_windows/include/chassis_domain_server.hpp similarity index 100% rename from agv_calib_brain/src/agv_calib_core/vehicle_agent_windows/include/chassis_domain_server.hpp rename to agv_calib_brain/src/site_deployment/vehicle_agent_real/vehicle_agent_windows/include/chassis_domain_server.hpp diff --git a/agv_calib_brain/src/agv_calib_core/vehicle_agent_windows/include/chassis_handler.hpp b/agv_calib_brain/src/site_deployment/vehicle_agent_real/vehicle_agent_windows/include/chassis_handler.hpp similarity index 100% rename from agv_calib_brain/src/agv_calib_core/vehicle_agent_windows/include/chassis_handler.hpp rename to agv_calib_brain/src/site_deployment/vehicle_agent_real/vehicle_agent_windows/include/chassis_handler.hpp diff --git a/agv_calib_brain/src/agv_calib_core/vehicle_agent_windows/include/control_domain_server.hpp b/agv_calib_brain/src/site_deployment/vehicle_agent_real/vehicle_agent_windows/include/control_domain_server.hpp similarity index 100% rename from agv_calib_brain/src/agv_calib_core/vehicle_agent_windows/include/control_domain_server.hpp rename to agv_calib_brain/src/site_deployment/vehicle_agent_real/vehicle_agent_windows/include/control_domain_server.hpp diff --git a/agv_calib_brain/src/agv_calib_core/vehicle_agent_windows/include/control_handler.hpp b/agv_calib_brain/src/site_deployment/vehicle_agent_real/vehicle_agent_windows/include/control_handler.hpp similarity index 100% rename from agv_calib_brain/src/agv_calib_core/vehicle_agent_windows/include/control_handler.hpp rename to agv_calib_brain/src/site_deployment/vehicle_agent_real/vehicle_agent_windows/include/control_handler.hpp diff --git a/agv_calib_brain/src/agv_calib_core/vehicle_agent_windows/include/data_types.hpp b/agv_calib_brain/src/site_deployment/vehicle_agent_real/vehicle_agent_windows/include/data_types.hpp similarity index 94% rename from agv_calib_brain/src/agv_calib_core/vehicle_agent_windows/include/data_types.hpp rename to agv_calib_brain/src/site_deployment/vehicle_agent_real/vehicle_agent_windows/include/data_types.hpp index 6fb7c27..22d973a 100644 --- a/agv_calib_brain/src/agv_calib_core/vehicle_agent_windows/include/data_types.hpp +++ b/agv_calib_brain/src/site_deployment/vehicle_agent_real/vehicle_agent_windows/include/data_types.hpp @@ -445,12 +445,26 @@ struct TrajectoryTrackingTask { std::vector path; bool stop_at_end{false}; double timeout_sec{0.0}; + std::string required_external_pose_source_id; + double max_external_pose_age_ms{0.0}; + double min_external_pose_quality_score{0.0}; + std::string trajectory_id; + uint32_t segment_index{0}; + uint32_t total_segments{0}; + bool is_final_segment{false}; }; inline void from_json(const nlohmann::json& j, TrajectoryTrackingTask& t) { if (j.contains("path") && j["path"].is_array()) t.path = j["path"].get>(); - t.stop_at_end = j.value("stop_at_end", false); - t.timeout_sec = j.value("timeout_sec", 0.0); + t.stop_at_end = j.value("stop_at_end", false); + t.timeout_sec = j.value("timeout_sec", 0.0); + t.required_external_pose_source_id = j.value("required_external_pose_source_id", std::string{}); + t.max_external_pose_age_ms = j.value("max_external_pose_age_ms", 0.0); + t.min_external_pose_quality_score = j.value("min_external_pose_quality_score", 0.0); + t.trajectory_id = j.value("trajectory_id", std::string{}); + t.segment_index = j.value("segment_index", uint32_t{0}); + t.total_segments = j.value("total_segments", uint32_t{0}); + t.is_final_segment = j.value("is_final_segment", false); } struct VelocityStepTask { @@ -555,6 +569,11 @@ struct ControlReadinessResponse { bool estop_released{false}; bool vehicle_safe_to_move{false}; int64_t checked_timestamp_us{0}; + bool external_pose_feedback_ready{false}; + std::string external_pose_source_name; + double external_pose_age_ms{0.0}; + double external_pose_quality_score{0.0}; + std::string external_pose_transport; }; inline void to_json(nlohmann::json& j, const ControlReadinessResponse& r) @@ -570,6 +589,11 @@ inline void to_json(nlohmann::json& j, const ControlReadinessResponse& r) {"estop_released", r.estop_released}, {"vehicle_safe_to_move", r.vehicle_safe_to_move}, {"checked_timestamp_us", r.checked_timestamp_us}, + {"external_pose_feedback_ready", r.external_pose_feedback_ready}, + {"external_pose_source_name", r.external_pose_source_name}, + {"external_pose_age_ms", r.external_pose_age_ms}, + {"external_pose_quality_score", r.external_pose_quality_score}, + {"external_pose_transport", r.external_pose_transport}, }; } diff --git a/agv_calib_brain/src/agv_calib_core/vehicle_agent_windows/include/frame_codec.hpp b/agv_calib_brain/src/site_deployment/vehicle_agent_real/vehicle_agent_windows/include/frame_codec.hpp similarity index 100% rename from agv_calib_brain/src/agv_calib_core/vehicle_agent_windows/include/frame_codec.hpp rename to agv_calib_brain/src/site_deployment/vehicle_agent_real/vehicle_agent_windows/include/frame_codec.hpp diff --git a/agv_calib_brain/src/agv_calib_core/vehicle_agent_windows/src/chassis_domain_server.cpp b/agv_calib_brain/src/site_deployment/vehicle_agent_real/vehicle_agent_windows/src/chassis_domain_server.cpp similarity index 100% rename from agv_calib_brain/src/agv_calib_core/vehicle_agent_windows/src/chassis_domain_server.cpp rename to agv_calib_brain/src/site_deployment/vehicle_agent_real/vehicle_agent_windows/src/chassis_domain_server.cpp diff --git a/agv_calib_brain/src/agv_calib_core/vehicle_agent_windows/src/control_domain_server.cpp b/agv_calib_brain/src/site_deployment/vehicle_agent_real/vehicle_agent_windows/src/control_domain_server.cpp similarity index 100% rename from agv_calib_brain/src/agv_calib_core/vehicle_agent_windows/src/control_domain_server.cpp rename to agv_calib_brain/src/site_deployment/vehicle_agent_real/vehicle_agent_windows/src/control_domain_server.cpp diff --git a/agv_calib_brain/src/agv_calib_core/vehicle_agent_windows/src/main.cpp b/agv_calib_brain/src/site_deployment/vehicle_agent_real/vehicle_agent_windows/src/main.cpp similarity index 100% rename from agv_calib_brain/src/agv_calib_core/vehicle_agent_windows/src/main.cpp rename to agv_calib_brain/src/site_deployment/vehicle_agent_real/vehicle_agent_windows/src/main.cpp diff --git a/agv_calib_brain/src/win_ubuntu_bridge.zip b/agv_calib_brain/src/win_ubuntu_bridge.zip deleted file mode 100644 index ec9bd33..0000000 Binary files a/agv_calib_brain/src/win_ubuntu_bridge.zip and /dev/null differ diff --git a/agv_calib_brain/src/win_ubuntu_bridge/calibration_control_interfaces/msg/TrajectoryTrackingTask.msg b/agv_calib_brain/src/win_ubuntu_bridge/calibration_control_interfaces/msg/TrajectoryTrackingTask.msg deleted file mode 100644 index 5de04e6..0000000 --- a/agv_calib_brain/src/win_ubuntu_bridge/calibration_control_interfaces/msg/TrajectoryTrackingTask.msg +++ /dev/null @@ -1,11 +0,0 @@ -# ========================================================= -# 轨迹跟踪评估任务 -# 作用:让车端按当前控制参数跑一条测试轨迹 -# ========================================================= - -# 轨迹点序列 -calibration_control_interfaces/TrajectoryPoint[] path -# 结束后是否停车 -bool stop_at_end -# 超时时间 -float64 timeout_sec diff --git a/agv_calib_brain/src/win_ubuntu_bridge/vehicle_agent_gateway/launch/vehicle_agent_gateway.launch.py b/agv_calib_brain/src/win_ubuntu_bridge/vehicle_agent_gateway/launch/vehicle_agent_gateway.launch.py deleted file mode 100644 index abe3952..0000000 --- a/agv_calib_brain/src/win_ubuntu_bridge/vehicle_agent_gateway/launch/vehicle_agent_gateway.launch.py +++ /dev/null @@ -1,23 +0,0 @@ -from launch import LaunchDescription -from launch_ros.actions import Node - - -def generate_launch_description(): - return LaunchDescription([ - Node( - package="vehicle_agent_gateway", - executable="vehicle_agent_gateway_node", - name="vehicle_agent_gateway", - output="screen", - parameters=[{ - # Windows 车端 IP,实际部署时通过命令行覆盖: - # ros2 launch vehicle_agent_gateway vehicle_agent_gateway.launch.py chassis_host:=192.168.x.x - "chassis_host": "192.168.1.100", - "chassis_port": 9001, - "chassis_timeout_ms": 5000, - "control_host": "192.168.1.100", - "control_port": 9002, - "control_timeout_ms": 5000, - }], - ), - ]) diff --git a/agv_calib_brain/stop_isaac_real_sim_stack.sh b/agv_calib_brain/stop_isaac_real_sim_stack.sh new file mode 100755 index 0000000..50838fb --- /dev/null +++ b/agv_calib_brain/stop_isaac_real_sim_stack.sh @@ -0,0 +1,129 @@ +#!/usr/bin/env bash +# 清理 Isaac 真实仿真闭环测试遗留进程。 +# +# 默认只列出候选进程,不会停止任何东西。 +# 使用 --kill 后才会发送 SIGINT/SIGTERM。 + +set -Eeuo pipefail + +DO_KILL=0 + +usage() { + cat <<'EOF' +用法: + ./stop_isaac_real_sim_stack.sh [选项] + +选项: + --list 只列出候选进程,默认行为。 + --kill 停止候选进程,先 SIGINT,未退出再 SIGTERM。 + -h, --help 显示帮助。 + +说明: + 该脚本只匹配 AutoCalib-Workshop 仿真闭环相关进程,例如: + launch_sim_stack.py、minimal_workshop_demo.launch.py、 + workshop_orchestrator_v2_node、vehicle_agent_gateway_node 等。 +EOF +} + +while [[ $# -gt 0 ]]; do + case "$1" in + --list) + DO_KILL=0 + shift + ;; + --kill) + DO_KILL=1 + shift + ;; + -h|--help) + usage + exit 0 + ;; + *) + echo "[ERROR] 未知参数: $1" >&2 + exit 1 + ;; + esac +done + +PATTERNS=( + "src/simulation/tools/launch_sim_stack.py --component isaac" + "src/simulation/tools/launch_sim_stack.py --component vehicle-agent" + "src/simulation/tools/launch_sim_stack.py --component sensor-agent" + "src/simulation/tools/launch_sim_stack.py --component wifi6-gateway" + "src/simulation/tools/launch_sim_stack.py --component external-pose-bridge" + "src/simulation/tools/launch_sim_stack.py --component sensor-ingest" + "minimal_workshop_demo.launch.py" + "build_calibration_room.py" + "isaac_vehicle_unified_agent_sim.py" + "isaac_vehicle_agent_sim.py" + "isaac_vehicle_sensor_agent_sim.py" + "vehicle_wifi6_gateway_sim.py" + "external_pose_wifi6_bridge_sim.py" + "workshop_sensor_ingest_sim.py" + "workshop_orchestrator_v2_node" + "vehicle_profile_manager_node" + "external_localization_service_node" + "sensor_calibration_service_node" + "chassis_calibration_service_node" + "control_calibration_service_node" + "vehicle_agent_gateway_node" +) + +declare -A PID_TO_CMD=() + +for pattern in "${PATTERNS[@]}"; do + while IFS= read -r line; do + [[ -n "${line}" ]] || continue + pid="${line%% *}" + cmd="${line#* }" + [[ "${pid}" != "$$" ]] || continue + PID_TO_CMD["${pid}"]="${cmd}" + done < <(pgrep -af -- "${pattern}" 2>/dev/null || true) +done + +if [[ "${#PID_TO_CMD[@]}" -eq 0 ]]; then + echo "[INFO] 未发现 AutoCalib 仿真闭环相关遗留进程。" + exit 0 +fi + +echo "[INFO] 发现候选进程:" +for pid in "${!PID_TO_CMD[@]}"; do + echo " pid=${pid} ${PID_TO_CMD[${pid}]}" +done | sort -n -k1.7 + +if [[ "${DO_KILL}" -eq 0 ]]; then + echo "[INFO] 当前是只读检查。确认无误后可执行: ./stop_isaac_real_sim_stack.sh --kill" + exit 0 +fi + +echo "[INFO] 发送 SIGINT..." +for pid in "${!PID_TO_CMD[@]}"; do + kill -INT "${pid}" >/dev/null 2>&1 || true +done + +sleep 3 + +echo "[INFO] 检查未退出进程并发送 SIGTERM..." +for pid in "${!PID_TO_CMD[@]}"; do + if kill -0 "${pid}" >/dev/null 2>&1; then + echo " SIGTERM pid=${pid}" + kill -TERM "${pid}" >/dev/null 2>&1 || true + fi +done + +sleep 1 + +LEFT=0 +for pid in "${!PID_TO_CMD[@]}"; do + if kill -0 "${pid}" >/dev/null 2>&1; then + echo "[WARN] 进程仍未退出: pid=${pid} ${PID_TO_CMD[${pid}]}" >&2 + LEFT=1 + fi +done + +if [[ "${LEFT}" -eq 0 ]]; then + echo "[INFO] 清理完成。" +else + echo "[WARN] 仍有进程未退出,请手动确认后再处理。" >&2 +fi diff --git a/models/ack_m.usd b/models/ack_m.usd new file mode 100644 index 0000000..ed42247 --- /dev/null +++ b/models/ack_m.usd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:91a5488e0fd8934276deef6353f895076af83d6c471474e6a0266446de8c9e3f +size 75606638