已开启
接入AirSim仿真 #4
接入AirSim仿真 #4
已开启
Noah创建于 4月3日
23 个文件变更+1032-64
@@ -0,0 +1,4 @@
1+[submodule "AirSim"]
2+ path = AirSim
3+ url = https://github.com/Air-Suck/AirSim.git
4+ branch = main
AAirSim+1-0
@@ -0,0 +1 @@
1+Subproject commit d90821bd2c75b6e5240f405d277dd692dab953b1
@@ -0,0 +1,145 @@
1+# OpenHarmony 无人机 AirSim 仿真后端使用说明
2+ 
3+本文档说明如何在 **宿主机 ROS 2 环境** 中将 **Microsoft AirSim** 作为可选仿真后端,与 **Navigation2 / RViz****OpenHarmony 端侧** 协同运行。AirSim 以子模块形式集成于本仓库的 `AirSim/` 目录,并配合自定义 **桥接节点** 对齐原有 Gazebo 后端的 Topic / TF 约定。
4+ 
5+> **前置阅读**:[快速开始](./QUICKSTART.md)(Docker、端侧 HDC、`simhost` / `simoh` 启动流程)。
6+ 
7+## 功能概览
8+ 
9+| 能力 | 说明 |
10+|------|------|
11+| 后端切换 | 通过 `demos/config/sim_backend.yaml``gazebo``airsim` 之间切换,无需改 launch 入口。 |
12+| AirSim ROS 2 Wrapper | 使用上游 `airsim_ros_pkgs` 中的 `airsim_node`,订阅/发布 AirSim 标准话题。 |
13+| Nav2 兼容桥接 | `demos` 包内 `airsim_bridge` 将 AirSim 的里程计、激光相关数据与控制接口转换为 Nav2 / `diffdrive_car` 期望的 `/odom``/scan``/cmd_vel` 与静态 TF。 |
14+| Python RPC(可选) | 桥接节点可选连接 AirSim `MultirotorClient` 做高度保持等控制;未安装 `airsim` Python 包时回退为 VelCmd 方式。 |
15+ 
16+## 仓库内相关路径
17+ 
18+```
19+software/host/
20+├── AirSim/ # git submodule,内含 ros2/airsim_ros_pkgs 等
21+├── demos/
22+│ ├── config/sim_backend.yaml # 仿真后端与 AirSim 参数
23+│ ├── launch/
24+│ │ ├── _airsim_sim.launch.py # AirSim 节点 + 桥接节点
25+│ │ ├── simhost.nav2.launch.py # 宿主机:根据配置选择 Gazebo 或 AirSim
26+│ │ └── sim.nav2.launch.py # 完整 Nav2 流程(含 sim 后端)
27+│ └── demos/airsim_bridge_node.py # 桥接实现
28+├── ros-demo.sh # 启动预置 Nav2 Demo 镜像的容器
29+└── ros-desktop.sh # GUI / 显示相关 Docker 参数
30+```
31+ 
32+初始化子模块(首次克隆或 PR 检出后):
33+ 
34+```bash
35+git submodule update --init --recursive
36+```
37+ 
38+## 配置仿真后端
39+ 
40+编辑 **安装后** 会被安装到 share 目录的配置源文件(开发时改工作区中的这一份即可,与包内路径一致):
41+ 
42+**文件**`demos/config/sim_backend.yaml`
43+ 
44+```yaml
45+# sim_backend: "gazebo" 或 "airsim"
46+sim_backend: "airsim"
47+ 
48+airsim:
49+ host_ip: "172.30.224.1" # 运行 AirSim 的机器 IP(从 Docker 容器内必须可访问)
50+ publish_clock: true
51+ vehicle_name: "drone_1"
52+ target_altitude: 3.0
53+ lidar_sensor_name: "LidarSensor1"
54+ update_airsim_img_response_every_n_sec: 0.05
55+ update_airsim_control_every_n_sec: 0.01
56+ update_lidar_every_n_sec: 0.01
57+```
58+ 
59+- **`host_ip`**:AirSim 的 RPC 所在地址。宿主机 Docker 场景下常为 **Windows 主机在 WSL2/虚拟网桥下的地址**,或 Linux 上跑 AirSim 时用 `localhost` / 宿主机局域网 IP。若 `airsim_node` 无法连接,请用容器内 `ping` / `nc` 核对到该 IP 的连通性。
60+- **`vehicle_name` / `lidar_sensor_name`**:需与 AirSim 环境中无人机/Car 及传感器命名一致(默认多旋翼常见为 `drone_1` 等,以你的 `settings.json` 为准)。
61+ 
62+桥接节点额外参数(可在 launch 或参数文件中扩展)包括 **`airsim_rpc_ip`****`airsim_rpc_port`**(默认 `41451`,与 AirSim Python/C++ 客户端默认端口一致,若你修改过 settings 请同步)。
63+ 
64+## 宿主机运行流程(与 QUICKSTART 对齐)
65+ 
66+### 1. 启动 AirSim
67+ 
68+**安装并运行 AirSim 的机器**(常见为 Windows + Unreal)上启动仿真场景,并确保:
69+ 
70+- 已启用对外 **RPC**(默认端口与 AirSim 文档一致,一般为 **41451**)。
71+- 防火墙允许 **Docker 宿主机 / WSL** 所在网段访问该端口。
72+ 
73+具体编译、场景与 `settings.json` 请参考子模块 [AirSim/README.md](./AirSim/README.md) 与 [官方文档](https://microsoft.github.io/AirSim/)。
74+ 
75+### 2. 启动 ROS 2 容器并配置 `host_ip`
76+ 
77+```bash
78+./ros-demo.sh
79+# 进入容器后
80+cd /root/oh_robot_sim # 若与你本地挂载路径不同,以实际 workspace 为准
81+source ~/.bashrc_ros
82+source install/setup.bash
83+export ROS_DOMAIN_ID=0
84+export RMW_IMPLEMENTATION=rmw_cyclonedds_cpp
85+```
86+ 
87+确认 `demos/config/sim_backend.yaml``sim_backend: airsim``airsim.host_ip` 指向 **从容器内** 能访问的 AirSim 机器地址。
88+ 
89+### 3. 启动宿主机仿真 + Nav2
90+ 
91+```bash
92+ros2 launch demos simhost.nav2.launch.py
93+```
94+ 
95+Launch 会加载 `_airsim_sim.launch.py`:先起 **`airsim_node`**,约 **5 秒** 后启动 **`airsim_bridge`**(延时用于等待 AirSim 侧就绪)。
96+ 
97+### 4. (可选)OpenHarmony 端侧
98+ 
99+在 OH 设备/模拟器上按 [QUICKSTART](./QUICKSTART.md) 完成 `hdc``ros2ohos.env``ors` 环境后:
100+ 
101+```bash
102+ros2 launch demos simoh.nav2.launch.py
103+```
104+ 
105+保证 **`ROS_DOMAIN_ID`** 与宿主机一致,且网络可达,以便 DDS 发现。
106+ 
107+## 桥接节点行为摘要
108+ 
109+实现文件:`demos/demos/airsim_bridge_node.py`(入口名 `airsim_bridge`)。
110+ 
111+面向 Nav2 与 Gazebo `diff_drive` 插件的约定,桥接主要完成:
112+ 
113+1. **`/cmd_vel`** → AirSim `VelCmd`(世界系)及可选 RPC 速度/高度控制。
114+2. **AirSim 本地 NED 里程计****`/odom`**(ENU 与 frame 修正)及 **`odom` → `base_footprint`** 动态 TF。
115+3. **AirSim 激光点云****`/scan`**`LaserScan` + 坐标与高度滤波参数可配)。
116+4. **静态 TF**`base_footprint``base_link``laser_link`
117+ 
118+依赖的 AirSim 侧话题前缀形如:`/airsim_node/<vehicle_name>/...`,与 `airsim_ros_pkgs` 默认一致。
119+ 
120+**Python 依赖(可选)**:若在容器内使用 RPC 定高,可安装 `airsim` Python 包,或将 `AirSim/PythonClient` 加入 `PYTHONPATH`。未安装时节点会记录告警并回退到纯 ROS 控制路径。
121+ 
122+## 常见问题
123+ 
124+| 现象 | 排查建议 |
125+|------|----------|
126+| `airsim_node` 连接超时 | 检查 `host_ip`、防火墙、AirSim 是否已加载场景;在容器内测试到 RPC 端口的连通性。 |
127+| `/odom``/scan` 无数据 | 确认 `vehicle_name``lidar_sensor_name` 与 AirSim 中一致;查看 `airsim_node` 是否已发布对应话题。 |
128+| 桥接节点报 RPC 失败 | 安装 Python `airsim` 或核对 `airsim_rpc_ip`/`airsim_rpc_port`;或关闭 RPC 路径仅用 VelCmd。 |
129+| Nav2 行为与地图不一致 | AirSim 场景与 2D 栅格地图(`assets/maps/...`)需语义上匹配;必要时更换 map 或仍使用 Gazebo 后端。 |
130+| 仅宿主机调试、无 OH | 可只运行 `simhost.nav2.launch.py` + RViz,无需启动 `simoh`。 |
131+ 
132+## 参与贡献与 PR 说明
133+ 
134+- 后端切换设计应 **保持 Gazebo 路径不受影响**:默认值仍为 `gazebo`,仅在配置为 `airsim` 时引入 AirSim 与桥接。
135+- 新增参数请同步 **`sim_backend.yaml` 示例** 与本文档。
136+- 子模块 `AirSim` 指向社区维护的 fork(见根目录 `.gitmodules`),升级子模块版本请在 PR 中说明变更原因与测试结果。
137+ 
138+## 参考链接
139+ 
140+- 主项目说明:[README.md](./README.md)
141+- 快速开始:[QUICKSTART.md](./QUICKSTART.md)
142+- AirSim 子模块说明:[AirSim/README.md](./AirSim/README.md)
143+- AirSim ROS 2 包说明:[AirSim/ros2/src/airsim_ros_pkgs/README.md](./AirSim/ros2/src/airsim_ros_pkgs/README.md)
144+ 
145+PS: 本文档为AI生成,仅供参考。
@@ -19,6 +19,8 @@ Robot Sim 是首个面向 OpenHarmony (EDU) 生态的具身智能机器人模拟
19 19 
20- [不使用 OH 的开发环境?](./NO-OH-DEV.md);20- [不使用 OH 的开发环境?](./NO-OH-DEV.md);
21 21 
22+- [AirSim 仿真后端与 Nav2 桥接](./README-AIRSIM.md);
23+ 
22 24 
23## ✨ 核心特性25## ✨ 核心特性
24 26 
@@ -56,7 +58,7 @@ Robot Sim 是首个面向 OpenHarmony (EDU) 生态的具身智能机器人模拟
56| ------ | ------------------------------------------- | -------------------- |58| ------ | ------------------------------------------- | -------------------- |
57| 交互层 | ArkTS 前端、端侧伺服遥控、Agent+MCP | 多模态指令输入、可视化交互、端侧协同控制 |59| 交互层 | ArkTS 前端、端侧伺服遥控、Agent+MCP | 多模态指令输入、可视化交互、端侧协同控制 |
58| 控制层 | ROS2 Controllers、Robot State Nodes | 关节轨迹规划、状态采集发布、控制信号生成 |60| 控制层 | ROS2 Controllers、Robot State Nodes | 关节轨迹规划、状态采集发布、控制信号生成 |
59-| 仿真层 | Gazebo/MuJoCo 仿真节点 | 物理仿真、机器人模型驱动、图像渲染 |61+| 仿真层 | Gazebo/MuJoCo 仿真节点;可选 [AirSim](./README-AIRSIM.md) 后端 | 物理仿真、机器人模型驱动、图像渲染;无人机 AirSim 与 ROS 2 Wrapper 桥接 |
60| 数据与模型层 | LeRobot Data Collector、VLA Train\&Inference | 多源数据采集、模型训练、推理部署 |62| 数据与模型层 | LeRobot Data Collector、VLA Train\&Inference | 多源数据采集、模型训练、推理部署 |
61 63 
62## 🛠️ 技术栈64## 🛠️ 技术栈
Binary files do not support preview
@@ -0,0 +1,7 @@
1+image: blocks_map.pgm
2+mode: trinary
3+resolution: 0.05
4+origin: [-1.63, -66.5, 0]
5+negate: 0
6+occupied_thresh: 0.65
7+free_thresh: 0.25
@@ -2,6 +2,7 @@ from ament_index_python.packages import get_package_share_directory
2 2 
3import glob3import glob
4import os4import os
5+import yaml
5 6 
6# keep track with config.h7# keep track with config.h
7EMERGENCY_STOP_MARK_SVC = "/emergency_stop"8EMERGENCY_STOP_MARK_SVC = "/emergency_stop"
@@ -86,3 +87,20 @@ def is_robot_support_mujoco(robot_name: str) -> bool:
86 return os.path.exists(87 return os.path.exists(
87 get_robot_mujoco_resource_share_dir(robot_name))88 get_robot_mujoco_resource_share_dir(robot_name))
88 89 
90+ 
91+_SIM_BACKEND_CONFIG_FILENAME = 'sim_backend.yaml'
92+ 
93+def get_sim_backend_config() -> dict:
94+ """Read simulation backend configuration from the demos package.
95+ Returns a dict with at least {'sim_backend': 'gazebo'} as default.
96+ """
97+ try:
98+ demos_share = get_package_share_directory('demos')
99+ except Exception:
100+ return {'sim_backend': 'gazebo'}
101+ config_path = os.path.join(demos_share, 'config', _SIM_BACKEND_CONFIG_FILENAME)
102+ if not os.path.isfile(config_path):
103+ return {'sim_backend': 'gazebo'}
104+ with open(config_path) as f:
105+ return yaml.safe_load(f) or {'sim_backend': 'gazebo'}
106+ 
@@ -1,5 +1,6 @@
1#pragma once1#pragma once
2 2 
3+#include "control_svc/detail/sysroot_math_before_grpc.hpp"
3#include <grpcpp/grpcpp.h>4#include <grpcpp/grpcpp.h>
4 5 
5#include <rclcpp/rclcpp.hpp>6#include <rclcpp/rclcpp.hpp>
@@ -0,0 +1,51 @@
1+#pragma once
2+ 
3+// Workaround for musl-based sysroots (e.g. OHOS) + libc++ + gRPC.
4+// On glibc (normal Linux) this file is intentionally empty.
5+ 
6+#include <features.h>
7+ 
8+#if !defined(__GLIBC__) && defined(__cplusplus)
9+ 
10+typedef decltype(nullptr) nullptr_t;
11+ 
12+#include <__type_traits/promote.h>
13+#include <math.h>
14+ 
15+#ifdef fpclassify
16+#undef fpclassify
17+#endif
18+#ifdef signbit
19+#undef signbit
20+#endif
21+#ifdef isinf
22+#undef isinf
23+#endif
24+#ifdef isnan
25+#undef isnan
26+#endif
27+#ifdef isfinite
28+#undef isfinite
29+#endif
30+ 
31+inline int (fpclassify)(float __x) { return __fpclassifyf(__x); }
32+inline int (fpclassify)(double __x) { return __fpclassify(__x); }
33+inline int (fpclassify)(long double __x) { return __fpclassifyl(__x); }
34+ 
35+inline int (signbit)(float __x) { return __builtin_signbit(__x); }
36+inline int (signbit)(double __x) { return __builtin_signbit(__x); }
37+inline int (signbit)(long double __x) { return __builtin_signbit(__x); }
38+ 
39+inline int (isinf)(float __x) { return __builtin_isinf(__x); }
40+inline int (isinf)(double __x) { return __builtin_isinf(__x); }
41+inline int (isinf)(long double __x) { return __builtin_isinf(__x); }
42+ 
43+inline int (isnan)(float __x) { return __builtin_isnan(__x); }
44+inline int (isnan)(double __x) { return __builtin_isnan(__x); }
45+inline int (isnan)(long double __x) { return __builtin_isnan(__x); }
46+ 
47+inline int (isfinite)(float __x) { return __builtin_isfinite(__x); }
48+inline int (isfinite)(double __x) { return __builtin_isfinite(__x); }
49+inline int (isfinite)(long double __x) { return __builtin_isfinite(__x); }
50+ 
51+#endif
@@ -1,5 +1,6 @@
1#pragma once1#pragma once
2 2 
3+#include "control_svc/detail/sysroot_math_before_grpc.hpp"
3#include <grpcpp/grpcpp.h>4#include <grpcpp/grpcpp.h>
4#include <rclcpp/rclcpp.hpp>5#include <rclcpp/rclcpp.hpp>
5#include <rclcpp_action/rclcpp_action.hpp>6#include <rclcpp_action/rclcpp_action.hpp>
@@ -1,5 +1,6 @@
1#pragma once1#pragma once
2 2 
3+#include "control_svc/detail/sysroot_math_before_grpc.hpp"
3#include <grpcpp/grpcpp.h>4#include <grpcpp/grpcpp.h>
4#include "control_stubs/sensing.grpc.pb.h"5#include "control_stubs/sensing.grpc.pb.h"
5#include "control_stubs/common.pb.h"6#include "control_stubs/common.pb.h"
@@ -284,7 +284,7 @@ grpc::Status CoreServiceRos2Impl::GetEndEffectorState(
284 const std::string &eelink = move_group_->getEndEffectorLink();284 const std::string &eelink = move_group_->getEndEffectorLink();
285 if (eelink.empty()) {285 if (eelink.empty()) {
286 std::string errmsg = "there is no end effector link defined for move group '" + move_group_->getName() + "'";286 std::string errmsg = "there is no end effector link defined for move group '" + move_group_->getName() + "'";
287- RCLCPP_WARN(node_->get_logger(), errmsg.c_str());287+ RCLCPP_WARN(node_->get_logger(), "%s", errmsg.c_str());
288 288
289 return grpc::Status(grpc::StatusCode::INVALID_ARGUMENT, errmsg);289 return grpc::Status(grpc::StatusCode::INVALID_ARGUMENT, errmsg);
290 } else try {290 } else try {
@@ -310,12 +310,12 @@ grpc::Status CoreServiceRos2Impl::GetEndEffectorState(
310 orientation->set_w(quat.w());310 orientation->set_w(quat.w());
311 } catch (const std::exception &ex) {311 } catch (const std::exception &ex) {
312 std::string err = std::string("failed to get end effector state: ") + ex.what();312 std::string err = std::string("failed to get end effector state: ") + ex.what();
313- RCLCPP_ERROR(node_->get_logger(), err.c_str());313+ RCLCPP_ERROR(node_->get_logger(), "%s", err.c_str());
314 return grpc::Status(grpc::StatusCode::INTERNAL, err);314 return grpc::Status(grpc::StatusCode::INTERNAL, err);
315 }315 }
316 } else {316 } else {
317 std::string err = "failed to current robot state for move group '" + move_group_->getName() + "': maybe it doesn't exist?";317 std::string err = "failed to current robot state for move group '" + move_group_->getName() + "': maybe it doesn't exist?";
318- RCLCPP_ERROR(node_->get_logger(), err.c_str());318+ RCLCPP_ERROR(node_->get_logger(), "%s", err.c_str());
319 return grpc::Status(grpc::StatusCode::UNKNOWN, err);319 return grpc::Status(grpc::StatusCode::UNKNOWN, err);
320 }320 }
321 321 
@@ -1,4 +1,5 @@
1 1 
2+#include "control_svc/detail/sysroot_math_before_grpc.hpp"
2#include <grpcpp/grpcpp.h>3#include <grpcpp/grpcpp.h>
3#include "control_stubs/sensing.grpc.pb.h"4#include "control_stubs/sensing.grpc.pb.h"
4 5 
@@ -0,0 +1,188 @@
1+Panels:
2+ - Class: rviz_common/Displays
3+ Help Height: 0
4+ Name: Displays
5+ Property Tree Widget:
6+ Expanded:
7+ - /Global Options1
8+ - /TF1/Frames1
9+ Splitter Ratio: 0.5
10+ Tree Height: 600
11+ - Class: rviz_common/Selection
12+ Name: Selection
13+ - Class: rviz_common/Tool Properties
14+ Expanded: ~
15+ Name: Tool Properties
16+ Splitter Ratio: 0.5886790156364441
17+ - Class: rviz_common/Views
18+ Expanded:
19+ - /Current View1
20+ Name: Views
21+ Splitter Ratio: 0.5
22+ - Class: rviz_common/Time
23+ Experimental: false
24+ Name: Time
25+ SyncMode: 0
26+ SyncSource: LaserScan
27+Visualization Manager:
28+ Class: ""
29+ Displays:
30+ - Alpha: 0.5
31+ Cell Size: 1
32+ Class: rviz_default_plugins/Grid
33+ Color: 160; 160; 164
34+ Enabled: true
35+ Line Style:
36+ Line Width: 0.029999999329447746
37+ Value: Lines
38+ Name: Grid
39+ Normal Cell Count: 0
40+ Offset:
41+ X: 0
42+ Y: 0
43+ Z: 0
44+ Plane: XY
45+ Plane Cell Count: 20
46+ Reference Frame: <Fixed Frame>
47+ Value: true
48+ - Class: rviz_default_plugins/TF
49+ Enabled: true
50+ Frame Timeout: 15
51+ Frames:
52+ All Enabled: true
53+ Marker Scale: 1
54+ Name: TF
55+ Show Arrows: true
56+ Show Axes: true
57+ Show Names: false
58+ Tree:
59+ {}
60+ Update Interval: 0
61+ Value: true
62+ - Alpha: 1
63+ Class: rviz_default_plugins/Map
64+ Color Scheme: map
65+ Draw Behind: true
66+ Enabled: true
67+ Name: Map
68+ Topic:
69+ Depth: 1
70+ Durability Policy: Transient Local
71+ History Policy: Keep Last
72+ Reliability Policy: Reliable
73+ Value: /map
74+ Use Timestamp: false
75+ Value: true
76+ - Alpha: 1
77+ Autocompute Intensity Bounds: true
78+ Autocompute Value Bounds:
79+ Max Value: 10
80+ Min Value: -10
81+ Value: true
82+ Axis: Z
83+ Channel Name: intensity
84+ Class: rviz_default_plugins/LaserScan
85+ Color: 255; 255; 255
86+ Color Transformer: Intensity
87+ Decay Time: 0
88+ Enabled: true
89+ Invert Rainbow: false
90+ Max Color: 255; 255; 255
91+ Max Intensity: 0
92+ Min Color: 0; 0; 0
93+ Min Intensity: 0
94+ Name: LaserScan
95+ Position Transformer: XYZ
96+ Selectable: true
97+ Size (Pixels): 3
98+ Size (m): 0.029999999329447746
99+ Style: Points
100+ Topic:
101+ Depth: 5
102+ Durability Policy: Volatile
103+ Filter size: 10
104+ History Policy: Keep Last
105+ Reliability Policy: Best Effort
106+ Value: /scan
107+ Use Fixed Frame: true
108+ Use rainbow: true
109+ Value: true
110+ Enabled: true
111+ Global Options:
112+ Background Color: 48; 48; 48
113+ Fixed Frame: map
114+ Frame Rate: 30
115+ Name: root
116+ Tools:
117+ - Class: rviz_default_plugins/Interact
118+ Hide Inactive Objects: true
119+ - Class: rviz_default_plugins/MoveCamera
120+ - Class: rviz_default_plugins/Select
121+ - Class: rviz_default_plugins/FocusCamera
122+ - Class: rviz_default_plugins/Measure
123+ Line color: 128; 128; 0
124+ - Class: rviz_default_plugins/SetInitialPose
125+ Covariance x: 0.25
126+ Covariance y: 0.25
127+ Covariance yaw: 0.06853891909122467
128+ Topic:
129+ Depth: 5
130+ Durability Policy: Volatile
131+ History Policy: Keep Last
132+ Reliability Policy: Reliable
133+ Value: /initialpose
134+ - Class: rviz_default_plugins/SetGoal
135+ Topic:
136+ Depth: 5
137+ Durability Policy: Volatile
138+ History Policy: Keep Last
139+ Reliability Policy: Reliable
140+ Value: /goal_pose
141+ - Class: rviz_default_plugins/PublishPoint
142+ Single click: true
143+ Topic:
144+ Depth: 5
145+ Durability Policy: Volatile
146+ History Policy: Keep Last
147+ Reliability Policy: Reliable
148+ Value: /clicked_point
149+ Transformation:
150+ Current:
151+ Class: rviz_default_plugins/TF
152+ Value: true
153+ Views:
154+ Current:
155+ Angle: -1.5708
156+ Class: rviz_default_plugins/TopDownOrtho
157+ Enable Stereo Rendering:
158+ Stereo Eye Separation: 0.05999999865889549
159+ Stereo Focal Distance: 1
160+ Swap Stereo Eyes: false
161+ Value: false
162+ Invert Z Axis: false
163+ Name: Current View
164+ Near Clip Distance: 0.009999999776482582
165+ Scale: 150
166+ Target Frame: <Fixed Frame>
167+ Value: TopDownOrtho (rviz_default_plugins)
168+ X: 0
169+ Y: 0
170+ Saved: ~
171+Window Geometry:
172+ Displays:
173+ collapsed: false
174+ Height: 900
175+ Hide Left Dock: false
176+ Hide Right Dock: false
177+ QMainWindow State: 000000ff00000000fd00000004000000000000016a0000034afc0200000002fb000000100044006900730070006c006100790073010000003d0000029b000000c900fffffffb0000000a0056006900650077007301000002de000000a90000008100ffffff000000010000010f0000034afc0200000000
178+ Selection:
179+ collapsed: false
180+ Time:
181+ collapsed: false
182+ Tool Properties:
183+ collapsed: false
184+ Views:
185+ collapsed: false
186+ Width: 1500
187+ X: 200
188+ Y: 100
@@ -0,0 +1,13 @@
1+# Simulation backend configuration.
2+# sim_backend: "gazebo" or "airsim"
3+sim_backend: "airsim"
4+ 
5+# AirSim-specific settings (only used when sim_backend is "airsim").
6+airsim:
7+ host_ip: "172.30.224.1"
8+ publish_clock: true
9+ vehicle_name: "drone_1"
10+ target_altitude: 3.0
11+ update_airsim_img_response_every_n_sec: 0.05
12+ update_airsim_control_every_n_sec: 0.01
13+ update_lidar_every_n_sec: 0.01
Binary files do not support preview
@@ -0,0 +1,443 @@
1+"""
2+AirSim ↔ Nav2/RViz bridge node.
3+ 
4+Makes AirSim ROS2 wrapper appear identical to the Gazebo backend by providing
5+the same topics, message types, and TF frames that Nav2 and RViz expect.
6+ 
7+Gazebo provided:
8+ PUB /odom (nav_msgs/Odometry) ← diff_drive plugin
9+ PUB /scan (sensor_msgs/LaserScan) ← ray sensor plugin
10+ PUB /tf odom → base_footprint ← diff_drive plugin
11+ PUB /tf_static base_footprint → base_link → laser_link ← URDF / robot_state_publisher
12+ PUB /clock (rosgraph_msgs/Clock) ← Gazebo core
13+ SUB /cmd_vel (geometry_msgs/Twist) ← diff_drive plugin
14+ 
15+AirSim provides:
16+ PUB ~/drone_1/odom_local_ned (nav_msgs/Odometry) — NED frame
17+ PUB ~/drone_1/lidar/<name> (sensor_msgs/PointCloud2) — NED frame
18+ PUB ~/clock (rosgraph_msgs/Clock) — remapped in launch
19+ PUB TF: world_ned → drone_1
20+ SUB ~/drone_1/vel_cmd_world_frame (airsim_interfaces/VelCmd)
21+ 
22+This bridge handles:
23+ 1. /cmd_vel → VelCmd and/or AirSim RPC (moveByVelocityZ = fixed NED height while moving)
24+ 2. AirSim odom → /odom (NED→ENU + frame ID fix)
25+ 3. AirSim odom → /tf odom→base_footprint (NED→ENU + frame ID fix)
26+ 4. AirSim PointCloud2 → /scan (PointCloud2→LaserScan + NED→ENU)
27+ 5. Static TF: base_footprint → base_link → laser_link
28+"""
29+ 
30+import math
31+import struct
32+ 
33+import numpy as np
34+ 
35+try:
36+ import airsim
37+ from airsim.types import DrivetrainType, YawMode
38+except ImportError:
39+ airsim = None
40+ DrivetrainType = None
41+ YawMode = None
42+ 
43+import rclpy
44+from rclpy.node import Node
45+from rclpy.qos import QoSProfile, ReliabilityPolicy
46+ 
47+from geometry_msgs.msg import Twist, TransformStamped
48+from nav_msgs.msg import Odometry
49+from sensor_msgs.msg import LaserScan, PointCloud2
50+from airsim_interfaces.msg import VelCmd
51+from airsim_interfaces.srv import Takeoff, Land
52+ 
53+from tf2_ros import TransformBroadcaster, StaticTransformBroadcaster
54+ 
55+ 
56+class AirsimBridgeNode(Node):
57+ 
58+ def __init__(self):
59+ super().__init__('airsim_cmd_vel_bridge')
60+ 
61+ # ── parameters ──────────────────────────────────────────────
62+ self.declare_parameter('vehicle_name', 'drone_1')
63+ self.declare_parameter('target_altitude', 3.0)
64+ self.declare_parameter('altitude_kp', 2.0)
65+ self.declare_parameter('max_vz', 2.0)
66+ self.declare_parameter('latch_altitude_after_takeoff', False)
67+ self.declare_parameter('use_airsim_velocity_z_hold', True)
68+ self.declare_parameter('move_to_z_velocity', 2.0)
69+ self.declare_parameter('vel_cmd_duration_sec', 0.05)
70+ self.declare_parameter('airsim_rpc_ip', '172.30.224.1')
71+ self.declare_parameter('airsim_rpc_port', 41451)
72+ self.declare_parameter('cmd_vel_timeout', 0.5)
73+ self.declare_parameter('lidar_sensor_name', 'LidarSensor1')
74+ self.declare_parameter('scan_range_min', 0.12)
75+ self.declare_parameter('scan_range_max', 50.0)
76+ self.declare_parameter('scan_height_min', -1.0)
77+ self.declare_parameter('scan_height_max', 1.0)
78+ self.declare_parameter('scan_samples', 360)
79+ 
80+ self.vehicle = self.get_parameter('vehicle_name').value
81+ self.target_alt = self.get_parameter('target_altitude').value
82+ self.alt_kp = self.get_parameter('altitude_kp').value
83+ self.max_vz = self.get_parameter('max_vz').value
84+ self.cmd_vel_timeout = self.get_parameter('cmd_vel_timeout').value
85+ self.lidar_name = self.get_parameter('lidar_sensor_name').value
86+ self.scan_range_min = self.get_parameter('scan_range_min').value
87+ self.scan_range_max = self.get_parameter('scan_range_max').value
88+ self.scan_height_min = self.get_parameter('scan_height_min').value
89+ self.scan_height_max = self.get_parameter('scan_height_max').value
90+ self.scan_samples = self.get_parameter('scan_samples').value
91+ 
92+ self._latch_alt = self.get_parameter(
93+ 'latch_altitude_after_takeoff').value
94+ self._use_vel_z = self.get_parameter(
95+ 'use_airsim_velocity_z_hold').value
96+ self._move_to_z_v = float(
97+ self.get_parameter('move_to_z_velocity').value)
98+ self._vel_dur = float(self.get_parameter('vel_cmd_duration_sec').value)
99+ rpc_ip = self.get_parameter('airsim_rpc_ip').value
100+ rpc_port = int(self.get_parameter('airsim_rpc_port').value)
101+ 
102+ self._alt_setpoint = float(self.target_alt)
103+ self._alt_latched = False
104+ self._airsim_client = None
105+ if self._use_vel_z:
106+ if airsim is None:
107+ self.get_logger().warn(
108+ 'airsim Python 未安装,改用 VelCmd+P 定高。'
109+ '可 pip install airsim 或把 AirSim/PythonClient 加入 PYTHONPATH。')
110+ self._use_vel_z = False
111+ else:
112+ try:
113+ self._airsim_client = airsim.MultirotorClient(
114+ ip=rpc_ip, port=rpc_port)
115+ self._airsim_client.confirmConnection()
116+ except Exception as e:
117+ self.get_logger().error(
118+ f'连接 AirSim RPC 失败 ({e}),改用 VelCmd+P 定高。')
119+ self._airsim_client = None
120+ self._use_vel_z = False
121+ 
122+ prefix = f'/airsim_node/{self.vehicle}'
123+ 
124+ # ── publishers ──────────────────────────────────────────────
125+ self.vel_pub = self.create_publisher(
126+ VelCmd, f'{prefix}/vel_cmd_world_frame', 10)
127+ self.odom_pub = self.create_publisher(
128+ Odometry, '/odom', 10)
129+ self.scan_pub = self.create_publisher(
130+ LaserScan, '/scan', 10)
131+ 
132+ # ── subscribers ─────────────────────────────────────────────
133+ self.cmd_vel_sub = self.create_subscription(
134+ Twist, '/cmd_vel', self._cmd_vel_cb,
135+ QoSProfile(depth=10, reliability=ReliabilityPolicy.RELIABLE))
136+ 
137+ self.odom_sub = self.create_subscription(
138+ Odometry, f'{prefix}/odom_local_ned', self._odom_cb,
139+ QoSProfile(depth=10, reliability=ReliabilityPolicy.BEST_EFFORT))
140+ 
141+ self.lidar_sub = self.create_subscription(
142+ PointCloud2, f'{prefix}/lidar/{self.lidar_name}', self._lidar_cb,
143+ QoSProfile(depth=5, reliability=ReliabilityPolicy.BEST_EFFORT))
144+ 
145+ # ── TF broadcasters ────────────────────────────────────────
146+ self.tf_broadcaster = TransformBroadcaster(self)
147+ self.static_tf_broadcaster = StaticTransformBroadcaster(self)
148+ self._publish_static_tfs()
149+ 
150+ # ── service clients ─────────────────────────────────────────
151+ self.takeoff_cli = self.create_client(Takeoff, f'{prefix}/takeoff')
152+ self.land_cli = self.create_client(Land, f'{prefix}/land')
153+ 
154+ # ── state ───────────────────────────────────────────────────
155+ self.current_alt = 0.0
156+ self.current_yaw = 0.0
157+ self.last_cmd_vel = Twist()
158+ self.last_cmd_stamp = self.get_clock().now()
159+ self.flying = False
160+ 
161+ self._do_takeoff()
162+ 
163+ self.timer = self.create_timer(0.05, self._control_loop)
164+ self.get_logger().info(
165+ f'Bridge started: vehicle={self.vehicle}, '
166+ f'target_alt={self.target_alt}m, lidar={self.lidar_name}')
167+ 
168+ # ================================================================
169+ # Static TFs — replaces robot_state_publisher's URDF-derived TFs
170+ # ================================================================
171+ 
172+ def _publish_static_tfs(self):
173+ stamp = self.get_clock().now().to_msg()
174+ static_tfs = []
175+ 
176+ # base_footprint → base_link (identity, same as typical URDF)
177+ t1 = TransformStamped()
178+ t1.header.stamp = stamp
179+ t1.header.frame_id = 'base_footprint'
180+ t1.child_frame_id = 'base_link'
181+ t1.transform.rotation.w = 1.0
182+ static_tfs.append(t1)
183+ 
184+ # base_link → laser_link (identity — LiDAR co-located with base)
185+ t2 = TransformStamped()
186+ t2.header.stamp = stamp
187+ t2.header.frame_id = 'base_link'
188+ t2.child_frame_id = 'laser_link'
189+ t2.transform.rotation.w = 1.0
190+ static_tfs.append(t2)
191+ 
192+ self.static_tf_broadcaster.sendTransform(static_tfs)
193+ 
194+ # ================================================================
195+ # Odom callback — AirSim → /odom + TF(odom → base_footprint)
196+ # ================================================================
197+ 
198+ def _odom_cb(self, msg: Odometry):
199+ # AirSim NED position → ROS (x=forward, y=left, z=up)
200+ ned_x = msg.pose.pose.position.x
201+ ned_y = msg.pose.pose.position.y
202+ ned_z = msg.pose.pose.position.z
203+ 
204+ ros_x = ned_x
205+ ros_y = -ned_y
206+ ros_z = -ned_z
207+ 
208+ # NED quaternion → ROS quaternion: (w, x, y, z) → (w, x, -y, -z)
209+ q = msg.pose.pose.orientation
210+ ros_qw = q.w
211+ ros_qx = q.x
212+ ros_qy = -q.y
213+ ros_qz = -q.z
214+ 
215+ # extract yaw for control loop (NED yaw → ROS yaw = negated)
216+ siny = 2.0 * (q.w * q.z + q.x * q.y)
217+ cosy = 1.0 - 2.0 * (q.y * q.y + q.z * q.z)
218+ self.current_yaw = -math.atan2(siny, cosy)
219+ self.current_alt = ros_z
220+ 
221+ if self.flying and self._latch_alt and not self._alt_latched:
222+ self._alt_setpoint = float(ros_z)
223+ self._alt_latched = True
224+ self.get_logger().info(
225+ f'定高设定为当前高度: {self._alt_setpoint:.2f} m')
226+ 
227+ stamp = self.get_clock().now().to_msg()
228+ 
229+ # publish /odom with standard frame IDs
230+ odom = Odometry()
231+ odom.header.stamp = stamp
232+ odom.header.frame_id = 'odom'
233+ odom.child_frame_id = 'base_footprint'
234+ odom.pose.pose.position.x = ros_x
235+ odom.pose.pose.position.y = ros_y
236+ odom.pose.pose.position.z = ros_z
237+ odom.pose.pose.orientation.w = ros_qw
238+ odom.pose.pose.orientation.x = ros_qx
239+ odom.pose.pose.orientation.y = ros_qy
240+ odom.pose.pose.orientation.z = ros_qz
241+ 
242+ # velocity conversion: NED → ROS
243+ odom.twist.twist.linear.x = msg.twist.twist.linear.x
244+ odom.twist.twist.linear.y = -msg.twist.twist.linear.y
245+ odom.twist.twist.linear.z = -msg.twist.twist.linear.z
246+ odom.twist.twist.angular.x = msg.twist.twist.angular.x
247+ odom.twist.twist.angular.y = -msg.twist.twist.angular.y
248+ odom.twist.twist.angular.z = -msg.twist.twist.angular.z
249+ 
250+ self.odom_pub.publish(odom)
251+ 
252+ # broadcast TF: odom → base_footprint
253+ tf_msg = TransformStamped()
254+ tf_msg.header.stamp = stamp
255+ tf_msg.header.frame_id = 'odom'
256+ tf_msg.child_frame_id = 'base_footprint'
257+ tf_msg.transform.translation.x = ros_x
258+ tf_msg.transform.translation.y = ros_y
259+ tf_msg.transform.translation.z = ros_z
260+ tf_msg.transform.rotation.w = ros_qw
261+ tf_msg.transform.rotation.x = ros_qx
262+ tf_msg.transform.rotation.y = ros_qy
263+ tf_msg.transform.rotation.z = ros_qz
264+ self.tf_broadcaster.sendTransform(tf_msg)
265+ 
266+ # ================================================================
267+ # LiDAR callback — AirSim PointCloud2 → /scan (LaserScan)
268+ # ================================================================
269+ 
270+ def _lidar_cb(self, msg: PointCloud2):
271+ if msg.width == 0:
272+ return
273+ 
274+ # parse PointCloud2 (3×float32 per point, NED frame)
275+ n_points = msg.width * msg.height
276+ fmt = f'<{n_points * 3}f'
277+ if len(msg.data) < struct.calcsize(fmt):
278+ return
279+ raw = struct.unpack(fmt, bytes(msg.data[:struct.calcsize(fmt)]))
280+ pts = np.array(raw, dtype=np.float32).reshape(-1, 3)
281+ 
282+ # NED → ROS sensor frame: y=-y, z=-z
283+ pts[:, 1] = -pts[:, 1]
284+ pts[:, 2] = -pts[:, 2]
285+ 
286+ # height filter (z in ROS sensor frame, z=up, 0=sensor level)
287+ mask = (pts[:, 2] >= self.scan_height_min) & (pts[:, 2] <= self.scan_height_max)
288+ pts = pts[mask]
289+ 
290+ if len(pts) == 0:
291+ return
292+ 
293+ # project to 2D scan
294+ angles = np.arctan2(pts[:, 1], pts[:, 0])
295+ ranges = np.hypot(pts[:, 0], pts[:, 1])
296+ 
297+ # build LaserScan
298+ scan = LaserScan()
299+ scan.header.stamp = self.get_clock().now().to_msg()
300+ scan.header.frame_id = 'laser_link'
301+ scan.angle_min = -math.pi
302+ scan.angle_max = math.pi
303+ scan.angle_increment = 2.0 * math.pi / self.scan_samples
304+ scan.range_min = self.scan_range_min
305+ scan.range_max = self.scan_range_max
306+ scan.time_increment = 0.0
307+ scan.scan_time = 0.1
308+ 
309+ # bin points into angular buckets, keep closest range per bucket
310+ scan.ranges = [float('inf')] * self.scan_samples
311+ for ang, rng in zip(angles, ranges):
312+ if rng < self.scan_range_min or rng > self.scan_range_max:
313+ continue
314+ idx = int((ang - scan.angle_min) / scan.angle_increment)
315+ idx = max(0, min(self.scan_samples - 1, idx))
316+ if rng < scan.ranges[idx]:
317+ scan.ranges[idx] = rng
318+ 
319+ self.scan_pub.publish(scan)
320+ 
321+ # ================================================================
322+ # cmd_vel callback — Nav2 → AirSim
323+ # ================================================================
324+ 
325+ def _cmd_vel_cb(self, msg: Twist):
326+ self.last_cmd_vel = msg
327+ self.last_cmd_stamp = self.get_clock().now()
328+ 
329+ # ================================================================
330+ # Takeoff / Land
331+ # ================================================================
332+ 
333+ def _do_takeoff(self):
334+ if not self.takeoff_cli.wait_for_service(timeout_sec=10.0):
335+ self.get_logger().error('Takeoff service not available')
336+ return
337+ req = Takeoff.Request()
338+ req.wait_on_last_task = True
339+ future = self.takeoff_cli.call_async(req)
340+ future.add_done_callback(self._takeoff_done)
341+ self.get_logger().info('Takeoff requested ...')
342+ 
343+ def _takeoff_done(self, future):
344+ try:
345+ future.result()
346+ if (
347+ self._use_vel_z and self._airsim_client is not None
348+ and not self._latch_alt):
349+ ned_z = -self._alt_setpoint
350+ self._airsim_client.moveToZAsync(
351+ ned_z,
352+ self._move_to_z_v,
353+ vehicle_name=self.vehicle,
354+ ).join()
355+ self.flying = True
356+ self.get_logger().info('Takeoff complete, bridge active')
357+ except Exception as e:
358+ self.get_logger().error(f'Takeoff failed: {e}')
359+ 
360+ def _do_land(self):
361+ if not self.land_cli.wait_for_service(timeout_sec=5.0):
362+ self.get_logger().warn('Land service not available')
363+ return
364+ req = Land.Request()
365+ req.wait_on_last_task = True
366+ self.land_cli.call_async(req)
367+ self.get_logger().info('Landing requested')
368+ 
369+ # ================================================================
370+ # Control loop — cmd_vel → AirSim VelCmd (with altitude hold)
371+ # ================================================================
372+ 
373+ def _control_loop(self):
374+ if not self.flying:
375+ return
376+ if self._latch_alt and not self._alt_latched:
377+ return
378+ 
379+ now = self.get_clock().now()
380+ dt = (now - self.last_cmd_stamp).nanoseconds * 1e-9
381+ if dt > self.cmd_vel_timeout:
382+ vx_world = 0.0
383+ vy_world = 0.0
384+ vyaw = 0.0
385+ else:
386+ vx_body = self.last_cmd_vel.linear.x
387+ vy_body = self.last_cmd_vel.linear.y
388+ vyaw = self.last_cmd_vel.angular.z
389+ # body-frame (base_link) → world-frame XY using current yaw
390+ # base_link convention: x forward, y left, z up
391+ c = math.cos(self.current_yaw)
392+ s = math.sin(self.current_yaw)
393+ # vel mixing: world-frame XY → body-frame XY using current yaw
394+ vx_world = c * vx_body - s * vy_body
395+ vy_world = s * vx_body + c * vy_body
396+ 
397+ if self._use_vel_z and self._airsim_client is not None:
398+ yaw_mode = YawMode(is_rate=True, yaw_or_rate=math.degrees(-vyaw))
399+ ned_z = -self._alt_setpoint
400+ self._airsim_client.moveByVelocityZAsync(
401+ vx_world,
402+ -vy_world,
403+ ned_z,
404+ self._vel_dur,
405+ DrivetrainType.MaxDegreeOfFreedom,
406+ yaw_mode,
407+ self.vehicle,
408+ )
409+ return
410+ 
411+ alt_err = self._alt_setpoint - self.current_alt
412+ vz = self.alt_kp * alt_err
413+ vz = max(-self.max_vz, min(self.max_vz, vz))
414+ 
415+ msg = VelCmd()
416+ msg.twist.linear.x = vx_world
417+ msg.twist.linear.y = -vy_world
418+ msg.twist.linear.z = -vz
419+ msg.twist.angular.x = 0.0
420+ msg.twist.angular.y = 0.0
421+ msg.twist.angular.z = -vyaw
422+ self.vel_pub.publish(msg)
423+ 
424+ # ================================================================
425+ # Shutdown
426+ # ================================================================
427+ 
428+ def destroy_node(self):
429+ self.flying = False
430+ self._do_land()
431+ super().destroy_node()
432+ 
433+ 
434+def main(args=None):
435+ rclpy.init(args=args)
436+ node = AirsimBridgeNode()
437+ try:
438+ rclpy.spin(node)
439+ except KeyboardInterrupt:
440+ pass
441+ finally:
442+ node.destroy_node()
443+ rclpy.shutdown()
@@ -0,0 +1,61 @@
1+#!/usr/bin/env python3
2+# launch: AirSim simulation backend
3+# Counterpart to gzsim.classic.launch.py — launches the AirSim ROS2 wrapper
4+# instead of Gazebo Classic, plus a bridge node for Nav2 integration.
5+ 
6+import launch
7+from launch.actions import TimerAction
8+from launch_ros.actions import Node
9+ 
10+from robot_sim_common import config
11+ 
12+ 
13+def generate_launch_description():
14+ sim_config = config.get_sim_backend_config()
15+ airsim_cfg = sim_config.get('airsim', {})
16+ 
17+ host_ip = str(airsim_cfg.get('host_ip', 'localhost'))
18+ publish_clock = bool(airsim_cfg.get('publish_clock', True))
19+ vehicle_name = str(airsim_cfg.get('vehicle_name', 'drone_1'))
20+ target_altitude = float(airsim_cfg.get('target_altitude', 3.0))
21+ lidar_sensor_name = str(airsim_cfg.get('lidar_sensor_name', 'LidarSensor1'))
22+ 
23+ airsim_node = Node(
24+ package='airsim_ros_pkgs',
25+ executable='airsim_node',
26+ name='airsim_node',
27+ output='screen',
28+ remappings=[
29+ ('/airsim_node/clock', '/clock'),
30+ ],
31+ parameters=[{
32+ 'is_vulkan': False,
33+ 'update_airsim_img_response_every_n_sec':
34+ float(airsim_cfg.get('update_airsim_img_response_every_n_sec', 0.05)),
35+ 'update_airsim_control_every_n_sec':
36+ float(airsim_cfg.get('update_airsim_control_every_n_sec', 0.01)),
37+ 'update_lidar_every_n_sec':
38+ float(airsim_cfg.get('update_lidar_every_n_sec', 0.01)),
39+ 'publish_clock': publish_clock,
40+ 'host_ip': host_ip,
41+ }]
42+ )
43+ 
44+ bridge_node = Node(
45+ package='demos',
46+ executable='airsim_bridge',
47+ name='airsim_cmd_vel_bridge',
48+ output='screen',
49+ parameters=[{
50+ 'vehicle_name': vehicle_name,
51+ 'target_altitude': target_altitude,
52+ 'lidar_sensor_name': lidar_sensor_name,
53+ }]
54+ )
55+ 
56+ bridge_delayed = TimerAction(period=5.0, actions=[bridge_node])
57+ 
58+ return launch.LaunchDescription([
59+ airsim_node,
60+ bridge_delayed,
61+ ])
Rdemos/launch/gzsim.nav2.launch.pydemos/launch/sim.nav2.launch.py+33-25文件内容审核中,请稍后刷新重试
@@ -1,11 +1,13 @@
1#!/usr/bin/env python31#!/usr/bin/env python3
2-# launch: gazebo classic with robot desc2+# launch: simulation backend (gazebo or airsim) with robot desc
3 3 
4import launch4import launch
5-from launch.actions import DeclareLaunchArgument, OpaqueFunction5+from launch.actions import DeclareLaunchArgument, IncludeLaunchDescription, OpaqueFunction
6from launch.launch_description_sources import PythonLaunchDescriptionSource6from launch.launch_description_sources import PythonLaunchDescriptionSource
7from launch.substitutions import LaunchConfiguration7from launch.substitutions import LaunchConfiguration
8 8 
9+from ament_index_python.packages import get_package_share_directory
10+ 
9import os11import os
10 12 
11from robot_sim_common import config13from robot_sim_common import config
@@ -13,45 +15,57 @@ from robot_sim_common import config
13# current package15# current package
14PKG_NAME = "demos"16PKG_NAME = "demos"
15 17 
18+# default robot
16DEFAULT_ROBOT_NAME = "diffdrive_car"19DEFAULT_ROBOT_NAME = "diffdrive_car"
20+# default simulation backend
17DEFAULT_ROBOT_SIM_LAUNCH_SCRIPT = "gzsim.classic.launch.py"21DEFAULT_ROBOT_SIM_LAUNCH_SCRIPT = "gzsim.classic.launch.py"
22+# default navigation2 configurations
18DEFAULT_ROBOT_NAV2_CONFIG = "nav2_params.classic.yaml"23DEFAULT_ROBOT_NAV2_CONFIG = "nav2_params.classic.yaml"
19- 24+# default world
20DEFAULT_ASSET_GZ_WORLD = "standard_room.classic.world"25DEFAULT_ASSET_GZ_WORLD = "standard_room.classic.world"
21DEFAULT_ASSET_SLAM2D_MAP = "standard_map.yaml"26DEFAULT_ASSET_SLAM2D_MAP = "standard_map.yaml"
22 27 
23 28 
24def launch_setup(context: launch.LaunchContext, *args, **kwargs):29def launch_setup(context: launch.LaunchContext, *args, **kwargs):
25- # get the actual string of substitutions30+ sim_config = config.get_sim_backend_config()
26- robot_name = context.perform_substitution(LaunchConfiguration('robot_name'))31+ sim_backend = sim_config.get('sim_backend', 'gazebo')
27- robot_sim_launch_script = context.perform_substitution(LaunchConfiguration('robot_sim_launch_script'))
28- world = context.perform_substitution(LaunchConfiguration('world'))
29-
30- if not config.is_robot_navigable(robot_name):
31- raise RuntimeError(
32- f"robot '{robot_name}' is not navigable:"
33- f" please create package '{config.get_robot_nav2_pkgname(robot_name)}'"
34- f" and configure your robot with '{config.get_robot_nav2_params_file_pattern()}'")
35 32 
36- robot_desc_share_dir = config.get_robot_description_share_dir(robot_name)33+ if sim_backend == 'airsim':
34+ action_sim_launch = IncludeLaunchDescription(
35+ PythonLaunchDescriptionSource(
36+ os.path.join(get_package_share_directory(PKG_NAME),
37+ 'launch', '_airsim_sim.launch.py')
38+ )
39+ )
40+ else:
41+ robot_name = context.perform_substitution(LaunchConfiguration('robot_name'))
42+ robot_sim_launch_script = context.perform_substitution(
43+ LaunchConfiguration('robot_sim_launch_script'))
44+ world = context.perform_substitution(LaunchConfiguration('world'))
37 45 
38- action_sim_launch = launch.actions.IncludeLaunchDescription(46+ # check if nav2 config file
39- PythonLaunchDescriptionSource(47+ if not config.is_robot_navigable(robot_name):
40- os.path.join(robot_desc_share_dir, 'launch', robot_sim_launch_script)48+ raise RuntimeError(
41- ),49+ f"robot '{robot_name}' is not navigable:"
42- launch_arguments={50+ f" please create package '{config.get_robot_nav2_pkgname(robot_name)}'"
43- 'world': os.path.join(config.ASSET_GZ_WORLDS_DIR, world)51+ f" and configure your robot with '{config.get_robot_nav2_params_file_pattern()}'")
44- }.items()52+ 
45- )53+ robot_desc_share_dir = config.get_robot_description_share_dir(robot_name)
46- 54+ 
47- return [55+ action_sim_launch = IncludeLaunchDescription(
48- # Start the sequence56+ PythonLaunchDescriptionSource(
49- action_sim_launch,57+ os.path.join(robot_desc_share_dir, 'launch', robot_sim_launch_script)
50- ]58+ ),
59+ launch_arguments={
60+ 'world': os.path.join(config.ASSET_GZ_WORLDS_DIR, world)
61+ }.items()
62+ )
63+ 
64+ return [action_sim_launch]
51 65 
52 66 
53def generate_launch_description() -> launch.LaunchDescription:67def generate_launch_description() -> launch.LaunchDescription:
54- 68+ 
55 return launch.LaunchDescription([69 return launch.LaunchDescription([
56 DeclareLaunchArgument(70 DeclareLaunchArgument(
57 'robot_name',71 'robot_name',
@@ -82,5 +82,4 @@ def generate_launch_description() -> launch.LaunchDescription:
82 default_value=DEFAULT_ASSET_SLAM2D_MAP,82 default_value=DEFAULT_ASSET_SLAM2D_MAP,
83 description="The custom SLAM 2D map for navigation2 & simulation env (like gazebo)"),83 description="The custom SLAM 2D map for navigation2 & simulation env (like gazebo)"),
84 OpaqueFunction(function=launch_setup)84 OpaqueFunction(function=launch_setup)
85- ])85+ ])
86- 
@@ -35,6 +35,7 @@ setup(
35 tests_require=['pytest'],35 tests_require=['pytest'],
36 entry_points={36 entry_points={
37 'console_scripts': [37 'console_scripts': [
38+ 'airsim_bridge = demos.airsim_bridge_node:main',
38 ],39 ],
39 },40 },
40)41)
Mros-desktop.sh+14-5文件内容审核中,请稍后刷新重试