├── .gitignore ├── Hardware └── 3D_Printing │ ├── Beam.STL │ ├── CameraMount.STL │ └── Elevating.STL ├── LICENSE ├── README.md ├── README_zh-CN.md ├── config.yaml ├── requirements.txt ├── resources ├── WeChat │ ├── WeChat03.png │ ├── WechatIMG2.jpg │ └── WechatIMG3.jpg ├── eight_skills.gif ├── joystick_mapping.png └── robomatrix-logo3.png ├── robomaster_driver ├── robomaster_msgs │ ├── CMakeLists.txt │ ├── msg │ │ ├── ChassisAttitute.msg │ │ ├── ChassisEsc.msg │ │ ├── ChassisIMU.msg │ │ ├── ChassisPosition.msg │ │ ├── ChassisSingleEsc.msg │ │ ├── GripperStatus.msg │ │ ├── RoboticArmPosition.msg │ │ ├── ServoSingleStatus.msg │ │ └── ServoStatus.msg │ └── package.xml └── robomaster_ros │ ├── launch │ └── collect_data.launch.py │ ├── package.xml │ ├── resource │ └── robomaster_ros │ ├── robomaster_ros │ ├── __init__.py │ ├── data_collection.py │ └── teleoperation.py │ ├── setup.cfg │ └── setup.py ├── robomatrix_client ├── package.xml ├── resource │ └── robomatrix_client ├── robomatrix_client │ ├── __init__.py │ ├── agent_propmts.py │ ├── agent_skills.py │ ├── audio2text.py │ └── robomatrix_client.py ├── setup.cfg └── setup.py ├── robomatrix_interface ├── CMakeLists.txt ├── package.xml └── srv │ ├── Audio.srv │ └── Vla.srv ├── robomatrix_server ├── package.xml ├── resource │ └── robomatrix_server ├── robomatrix_server │ ├── __init__.py │ ├── resource │ │ ├── __init__.py │ │ ├── funcs.py │ │ ├── inference_multistage.py │ │ └── utils.py │ └── vla_service.py ├── setup.cfg └── setup.py └── teleoperation ├── joystick_driver ├── joystick_driver │ ├── __init__.py │ └── xbox_controller.py ├── package.xml ├── resource │ └── joystick_driver ├── setup.cfg └── setup.py └── joystick_msgs ├── CMakeLists.txt ├── msg ├── Axis.msg ├── Button.msg ├── Hat.msg └── XBox360Controller.msg └── package.xml /.gitignore: -------------------------------------------------------------------------------- 1 | README_zh-CN_old.md 2 | README_old.md 3 | .vscode 4 | .DS_Store 5 | *.pt 6 | *.mp4 7 | *.wav 8 | __pycache__/ 9 | Grounding-DINO-1.5-API/ 10 | 11 | # ROS2 12 | build/ 13 | install/ 14 | log/ 15 | -------------------------------------------------------------------------------- /Hardware/3D_Printing/Beam.STL: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WayneMao/RoboMatrix/71b077ee5904078f1cbf9008e3fb1256643ba832/Hardware/3D_Printing/Beam.STL -------------------------------------------------------------------------------- /Hardware/3D_Printing/CameraMount.STL: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WayneMao/RoboMatrix/71b077ee5904078f1cbf9008e3fb1256643ba832/Hardware/3D_Printing/CameraMount.STL -------------------------------------------------------------------------------- /Hardware/3D_Printing/Elevating.STL: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WayneMao/RoboMatrix/71b077ee5904078f1cbf9008e3fb1256643ba832/Hardware/3D_Printing/Elevating.STL -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 ADG@PJLab 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 | logo 3 |
4 | 5 |
6 | 7 | English | [简体中文](README_zh-CN.md) 8 | 9 |
10 | 11 | # RoboMatrix: A Skill-centric Hierarchical Framework for Scalable Robot Task Planning and Execution in Open-World 12 | 13 | ## 📝[Paper](https://arxiv.org/abs/2412.00171) | 🌍[Project Page](https://robo-matrix.github.io/) | 🛢️[Data](https://huggingface.co/datasets/WayneMao/RoboMatrix) 14 | 15 | 16 |
17 | demo 18 |
19 | 20 | 21 | 22 | ## 📰 Release 23 | 24 | - [2025/01/08] 🔥 We release [data collection](https://github.com/WayneMao/RoboMatrix?tab=readme-ov-file#data-collection) method. 25 | - [2024/12/04] 🔥 We release the RoboMatrix supervised fine-tuning (SFT) dataset containing 1,500 high-quality human-annotated demonstration videos. 26 | 27 | ## Demos 28 | ### Dynamic Adversarial Interaction 29 | 31 | 32 | https://private-user-images.githubusercontent.com/35285052/392642975-b78e28aa-45c2-4bb0-9e70-b6a08c678f85.mp4 33 | 34 | 35 | ## Hardware Preparation 36 | We use robots from DJI’s **RoboMaster** series as the hardware platform, including the Engineering Robot (EP) and 37 | the Warrior Robot (S1). These two forms of robots share some common components, including the mobile chassis, monocular RGB camera, audio module, and controller. Additionally, each robot is equipped with a unique set of components to perform specific tasks, such as the target shooting capability of the S1 robot and the target grasping capability of the EP robot. 38 | 39 | We modified the EP robot by mounting the camera above the robot to prevent the camera’s viewpoint from changing with the movement of the robotic arm. See [3D_Printing](Hardware/3D_Printing) for the parts of the designed camera mount. 40 | 41 | We use **BEITONG ASURA 2PRO+ GAMEPAD NEARLINK VERSION** as the controller for robot teleoperation. 42 | 43 | ## Environment Configuration 44 | We developed RoboMatrix using the ROS2 framework on Ubuntu 20.04. You can follow the [official installation guidance](https://docs.ros.org/en/foxy/Installation/Ubuntu-Install-Debians.html) to complete the installation of the Foxy distro of ROS2 and the necessary tools. In addition, we passed the test on Ubuntu 22.04 ([ROS2 Humble](https://docs.ros.org/en/humble/Installation/Ubuntu-Install-Debians.html)), which may provide some reference for you if you want to install RoboMatrix in a later version of Ubuntu. 45 | 46 | We provide a general installation procedure for ROS2, this might give you some help. **If you already have it installed on your system, please skip this step.** 47 | 48 |
49 | ROS2 Installation 50 | 51 | ### Step 1: Set UTF-8 52 | Open a terminal, check weather your system supports UTF-8. 53 | ```bash 54 | locale 55 | ``` 56 | If not support (no output in terminal), please install. 57 | ```bash 58 | sudo locale-gen en_US en_US.UTF-8 59 | sudo update-locale LC_ALL=en_US.UTF-8 LANG=en_US.UTF-8 60 | export LANG=en_US.UTF-8 61 | ``` 62 | 63 | ### Step 2: Set Ubuntu Universe 64 | Open a terminal, check weather your system supports Ubuntu Universe. 65 | ```bash 66 | apt-cache policy | grep universe 67 | ``` 68 | If not support (no output in terminal), please install. 69 | ```bash 70 | sudo apt install software-properties-common 71 | sudo add-apt-repository universe 72 | ``` 73 | 74 | ### Step 3: Add ROS2 software source and key 75 | ```bash 76 | sudo apt update && sudo apt install curl gnupg2 lsb-release 77 | sudo curl -sSL https://raw.githubusercontent.com/ros/rosdistro/master/ros.key -o /usr/share/keyrings/ros-archive-keyring.gpg 78 | echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/ros-archive-keyring.gpg] http://packages.ros.org/ros2/ubuntu $(lsb_release -cs) main" | sudo tee /etc/apt/sources.list.d/ros2.list > /dev/null 79 | ``` 80 | 81 | ### Step 4: Get ROS2 82 | Install the specified version of ROS2, using **Foxy** as an example. 83 | ```bash 84 | sudo apt update 85 | sudo apt install ros-foxy-desktop 86 | ``` 87 | 88 | ### Step 5: Source bash 89 | ```bash 90 | echo "source /opt/ros/foxy/setup.bash" >> ~/.bashrc 91 | source .bashrc 92 | ``` 93 | 94 | ### Step 6: Test demo 95 | Open a terminal, start talker node. 96 | ```bash 97 | ros2 run demo_nodes_cpp talker 98 | ``` 99 | Open a new terminal, start listener node. 100 | ```bash 101 | ros2 run demo_nodes_cpp listener 102 | ``` 103 | 104 | ### Step 7: Install colcon 105 | ```bash 106 | sudo apt install python3-colcon-common-extensions 107 | ``` 108 | 109 |
110 | 111 | ## 🛠️ Installation 112 | ### Build workspace 113 | ```bash 114 | git clone https://github.com/WayneMao/RoboMatrix.git 115 | cd ~/RoboMatrix && colcon build 116 | ``` 117 | 118 | ### RoboMaster SDK 119 | Install dependencies. 120 | ```bash 121 | sudo apt install libopus-dev python3-pip 122 | python3 -m pip install -U numpy numpy-quaternion pyyaml 123 | ``` 124 | Install SDK from source code. 125 | ```bash 126 | python3 -m pip install git+https://github.com/jeguzzi/RoboMaster-SDK.git 127 | python3 -m pip install git+https://github.com/jeguzzi/RoboMaster-SDK.git#"egg=libmedia_codec&subdirectory=lib/libmedia_codec" 128 | ``` 129 | 130 | ### Dependencies 131 | ```bash 132 | pip install -r requirements.txt 133 | pip install torch==2.0.1 torchvision==0.15.2 torchaudio==2.0.2 --index-url https://download.pytorch.org/whl/cu118 134 | ``` 135 | 136 | ### Grounding-DINO-1.5-API 137 | ```bash 138 | cd ~/RoboMatrix/robomatrix_client/robomatrix_client 139 | git clone https://github.com/IDEA-Research/Grounding-DINO-1.5-API.git 140 | cd Grounding-DINO-1.5-API 141 | pip install -v -e . 142 | ``` 143 | 144 | ## Data Collection 145 | 146 | Download the RoboMaster official APP, follow the instructions to connect the robot to WiFi (only WiFi5), and connect the computer to the same WiFi to complete the connection. 147 | 148 |
149 | joystick_mapping 150 |
151 | 152 | ### Step 1: Start launch file 153 | ```bash 154 | source ~/RoboMatrixinstall/setup.bash 155 | ros2 launch robomaster_ros collect_data.launch.py name:=example idx:=1 dir:=~/RoboMatrixDatasets 156 | ``` 157 | 158 |
159 | 160 | |Parameter |Definition |Example| 161 | |----- |----- |-----| 162 | |name |A custom task name |move_to_box| 163 | |idx |The sequence number of the current episode of the task |10| 164 | |dir |The folder where the data is saved | ~/MyDatasets| 165 | 166 |
167 | 168 | ***NOTEs*** 169 | 1. Make sure the robot is successfully connected to the specified WIFI before launching the launch file. 170 | 2. Make sure the controller's button mode is **XBOX**, which you can view in the terminal. In the case of **BEITONG**, long press the `POWER` button to switch. 171 | 3. Ensure that the robot initialization is complete before proceeding with the following operations. 172 | 173 | ### Step 2: Start collecting 174 | 175 | By pressing the `START` button, the robot's status begins to be recorded and the other buttons on the handle are activated, allowing control of the robot's movement. 176 | 177 | ### Step 3: Control the robot 178 | 179 | The control mode of the robot chassis is speed control. The `RS` axis controls the translation speed of the chassis, and the `LT` and `RT` axes control the rotation speed of the chassis. 180 | 181 | The control mode of the robot arm is position control. The `HAT` key set changes the position of the end of the robot arm in the plane. Each press moves its position a fixed distance in the specified direction. 182 | 183 | The gripper control is binarized. The `A` button controls the gripper open to the maximum, and the `B` button controls the gripper closed to the maximum. 184 | 185 | ### Step 4: Save data 186 | 187 | Press the `BACK` button to save the data, then press the `POWER` button to clean the ROS2 node and wait for the video to finish saving. 188 | 189 | ## Dataset Construction 190 | 191 | Comming soon. 192 | 193 | ## Task Execution 194 | 195 | Comming soon. 196 | 197 | ## TODOs 198 | - [ ] Package Docker 199 | - [X] 🤗 Release Supervised Fine-tuning dataset 200 | - [x] Optimize VLA ROS communication 201 | - [ ] Open source VLA Skill model code 202 | - [ ] Release VLA Skill model weights 203 | - [ ] Open source Shooting code 204 | 205 | ## Citation 206 | 207 | If you find our work helpful, please cite us: 208 | 209 | ```bibtex 210 | @article{mao2024robomatrix, 211 | title={Robomatrix: A skill-centric hierarchical framework for scalable robot task planning and execution in open-world}, 212 | author={Mao, Weixin and Zhong, Weiheng and Jiang, Zhou and Fang, Dong and Zhang, Zhongyue and Lan, Zihan and Li, Haosheng and Jia, Fan and Wang, Tiancai and Fan, Haoqiang and others}, 213 | journal={arXiv preprint arXiv:2412.00171}, 214 | year={2024} 215 | } 216 | ``` 217 | 218 | ## 招聘 219 | 具身智能-具身大模型算法研究实习生 职位描述 220 | 221 | 职位描述 222 | 1. 参与多模态理解与生成大模型、VLA大模型所需的数据清洗和自动标注系统开发,确保各类型/模态数据的质量与多样性; 223 | 2. 探索高效的数据增强和数据合成方法,例如图像/视频编辑; 224 | 3. 对机器人平台实现算法的部署和调试,提高机器人策略效率; 225 | 4. 对前沿具身算法进行研究探索,包括不限于VLA、RDT、Pi0等; 226 | 5. 我们提供有力的研究指导,进行论文发表; 227 | 职位要求 228 | 1、实习时间至少6个月,每周保证4天以上实习 229 | 2、硕士及以上学历在读,计算机、自动化等相关专业优先; 230 | 3、具备较强的软件工程能力,熟练使用Python、pytorch,熟悉Linux操作系统; 231 | 4、熟悉并行化编程,熟悉三维坐标变换、计算机视觉基础知识,了解机器人运动学; 232 | 5、有较好的英文科技文献阅读及算法复现的能力; 233 | 6、有实际的机器人开发经验优先,有大规模数据生成与处理经验优先; 234 | 235 | 工作地点:北京·中关村,逐际动力北京实验室 236 | 申请方式:请将你的简历以及相关项目/研究的介绍发送至 waynemao@limxdynamics.com ,简历格式:实习_姓名_学校_方向.pdf 237 | PS. 同时也招收物理仿真实习生,视频生成/世界模型实习生,运动控制实习生, 还有少量全职HC。 238 | PPS. 请简历优先投递邮箱,走内推通道 239 | 240 | ## Acknowledgments 241 | - Implementation of Vision-Language-Action (VLA) skill model is based on [LLaVA](https://github.com/haotian-liu/LLaVA/). 242 | - RoboMatrix-ROS is based on official [RoboMaster-SDK](https://github.com/dji-sdk/RoboMaster-SDK), modified [RoboMaster-SDK](https://github.com/jeguzzi/RoboMaster-SDK) and [ROS2](https://github.com/ros2). 243 | - Some additional libraries: [Grounding-DINO-1.5](https://github.com/IDEA-Research/Grounding-DINO-1.5-API), [YOLO-World](https://github.com/AILab-CVC/YOLO-World). 244 | 245 | 246 | --- 247 | ## WeChat 248 | 249 | 250 |
251 | wechat 252 |
253 | -------------------------------------------------------------------------------- /README_zh-CN.md: -------------------------------------------------------------------------------- 1 |
2 | logo 3 |
4 | 5 |
6 | 7 | [English](README.md) | 简体中文 8 | 9 |
10 | 11 | # RoboMatrix: A Skill-centric Hierarchical Framework for Scalable Robot Task Planning and Execution in Open-World 12 | 13 | ### 📝[论文]() | 🌍[项目主页](https://robo-matrix.github.io/) | 🛢️[数据](https://huggingface.co/datasets/WayneMao/RoboMatrix) 14 | 15 | ![eight_skills](resources/eight_skills.gif) 16 | 17 | 18 | 19 | ## 发布 20 | - [2024/12/04] 🤗 发布RoboMatrix有监督微调数据集,该SFT数据集包含1,500条高质量人类标注的演示视频。 [[数据](https://huggingface.co/datasets/WayneMao/RoboMatrix)] 21 | 22 | ## Demo 23 | ### 动态对抗交互 24 | 26 | 27 | https://private-user-images.githubusercontent.com/35285052/392642975-b78e28aa-45c2-4bb0-9e70-b6a08c678f85.mp4 28 | 29 | ## 🛠️ 安装 30 | ### 1. 安装 ROS2 31 | 32 | **注意:如果您的系统上已安装 ROS2,请跳过此步骤。** 33 | 34 | **适配您 Ubuntu 系统的 ROS2 版本** 35 | * Ubuntu 20.04 ---> ROS2 Foxy ---> [官方安装指南](https://docs.ros.org/en/foxy/Installation/Ubuntu-Install-Debians.html) 36 | * Ubuntu 22.04 ---> ROS2 Humble ---> [官方安装指南](https://docs.ros.org/en/humble/Installation/Ubuntu-Install-Debians.html) 37 | 38 | **安装 colcon** 39 | ```bash 40 | sudo apt install python3-colcon-common-extensions 41 | ``` 42 | 43 | ### 2. 构建工作空间 44 | ```bash 45 | mkdir -p ~/RoboMatrix/src && cd ~/RoboMatrix/src 46 | git clone https://github.com/WayneMao/RoboMatrix.git 47 | cd ~/RoboMatrix && colcon build 48 | ``` 49 | 50 | ### 3. 安装依赖 51 | ```bash 52 | sudo apt install libopus-dev python3-pip 53 | python3 -m pip install -U numpy numpy-quaternion pyyaml 54 | 55 | # 安装 RoboMaster-SDK 56 | python3 -m pip install git+https://github.com/jeguzzi/RoboMaster-SDK.git 57 | python3 -m pip install git+https://github.com/jeguzzi/RoboMaster-SDK.git#"egg=libmedia_codec&subdirectory=lib/libmedia_codec" 58 | 59 | # 安装依赖和 PyTorch 60 | pip install -r requirements.txt 61 | pip install torch==2.0.1 torchvision==0.15.2 torchaudio==2.0.2 --index-url https://download.pytorch.org/whl/cu118 62 | ``` 63 | ### 4. 第三方依赖 64 | **Grounding-DINO-1.5-API** 65 | ```bash 66 | cd src/robomatrix_client/robomatrix_client 67 | git clone https://github.com/IDEA-Research/Grounding-DINO-1.5-API.git 68 | cd Grounding-DINO-1.5-API 69 | pip install -v -e . 70 | ``` 71 | 72 | ## 真机部署 73 | 74 | ## TODO 75 | - [ ] 打包Docker 76 | - [x] 🤗 发布有监督微调数据集 77 | - [ ] 优化VLA ROS通信 78 | - [ ] 开源VLA Skill model代码 79 | - [ ] 开放VLA Skill Model权重 80 | - [ ] 开源Shooting代码 81 | 82 | ## Citation 83 | 84 | If you find our work helpful, please cite us: 85 | 86 | ```bibtex 87 | @article{mao2024robomatrix, 88 | title={Robomatrix: A skill-centric hierarchical framework for scalable robot task planning and execution in open-world}, 89 | author={Mao, Weixin and Zhong, Weiheng and Jiang, Zhou and Fang, Dong and Zhang, Zhongyue and Lan, Zihan and Li, Haosheng and Jia, Fan and Wang, Tiancai and Fan, Haoqiang and others}, 90 | journal={arXiv preprint arXiv:2412.00171}, 91 | year={2024} 92 | } 93 | ``` 94 | 95 | ## 招聘 96 | 具身智能-具身大模型算法研究实习生 97 | 职位描述 98 | 1. 参与多模态理解与生成大模型、VLA大模型所需的数据清洗和自动标注系统开发,确保各类型/模态数据的质量与多样性; 99 | 2. 探索高效的数据增强和数据合成方法,例如图像/视频编辑; 100 | 3. 对机器人平台实现算法的部署和调试,提高机器人策略效率; 101 | 4. 对前沿具身算法进行研究探索,包括不限于VLA、RDT、Pi0等; 102 | 5. 我们提供有力的研究指导,进行论文发表; 103 | 职位要求 104 | 1、实习时间至少6个月,每周保证4天以上实习 105 | 2、硕士及以上学历在读,计算机、自动化等相关专业优先; 106 | 3、具备较强的软件工程能力,熟练使用Python、pytorch,熟悉Linux操作系统; 107 | 4、熟悉并行化编程,熟悉三维坐标变换、计算机视觉基础知识,了解机器人运动学; 108 | 5、有较好的英文科技文献阅读及算法复现的能力; 109 | 6、有实际的机器人开发经验优先,有大规模数据生成与处理经验优先; 110 | 111 | 工作地点:北京·中关村,逐际动力北京实验室 112 | 申请方式:请将你的简历以及相关项目/研究的介绍发送至 waynemao@limxdynamics.com ,简历格式:实习_姓名_学校_方向.pdf 113 | PS. 同时也招收**物理仿真实习生,视频生成/世界模型实习生,运动控制实习生**, 还有少量全职HC。 114 | PPS. 请简历优先投递邮箱,走内推通道 115 | ## 致谢 116 | - 视觉-语言-动作 (VLA) 技能模型的实现基于 [LLaVA](https://github.com/haotian-liu/LLaVA/)。 117 | - RoboMatrix-ROS 基于 [RoboMaster-SDK](https://github.com/dji-sdk/RoboMaster-SDK) 和 [ROS2](https://github.com/ros2)。 118 | - 其他一些库包括:[Grounding-DINO-1.5](https://github.com/IDEA-Research/Grounding-DINO-1.5-API)、[YOLO-World](https://github.com/AILab-CVC/YOLO-World)。 119 | 120 | 121 | --- 122 | ## 微信群 123 | weichatweichat 124 | -------------------------------------------------------------------------------- /config.yaml: -------------------------------------------------------------------------------- 1 | OPENAI_API_TYPE: 'openai' #'azure' OR 'openai' 2 | 3 | # for 'openai' api key 4 | OPENAI_KEY: 'xxxx' 5 | OPENAI_API_BASE: 'xxxx' 6 | 7 | # for 'azure' 8 | AZURE_MODEL: 'XXXXX' # your deploment_model_name 9 | AZURE_API_BASE: https://xxxxxxxx.openai.azure.com/ # your deployment endpoint 10 | AZURE_API_KEY: 'xxxxxx' # your deployment key 11 | AZURE_API_VERSION: '2023-03-15-preview' 12 | 13 | # For STEP1 14 | STEP1_API_KEY: 'xxxx' 15 | STEP1_API_BASE: 'https://xxxx/v1/' 16 | 17 | # For Grounding-DINO-1.5 18 | Grounding_DINO_1.5_API: xxxx 19 | 20 | # Audio2text api 21 | AssemblyAI_API: xxxx -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | # langchain==0.0.225 2 | # openai==0.27.7 3 | matplotlib==3.7.1 4 | numpy==1.24.3 5 | rich 6 | PyYAML==6.0.1 7 | openai==1.35.14 8 | langchain==0.2.8 9 | langchain-community==0.2.0 10 | langchain-core==0.2.20 11 | langchain-openai==0.1.16 12 | langchain-text-splitters==0.2.2 13 | langsmith==0.1.88 14 | # langserve==0.2.2 15 | # pip install "langserve[all]" 16 | 17 | # torch==2.0.1 18 | 19 | pyquaternion==0.9.9 20 | 21 | # Joystick Control dependencies 22 | pygame 23 | 24 | dds-cloudapi-sdk==0.2.1 25 | gradio==4.22.0 26 | scipy 27 | noisereduce 28 | assemblyai -------------------------------------------------------------------------------- /resources/WeChat/WeChat03.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WayneMao/RoboMatrix/71b077ee5904078f1cbf9008e3fb1256643ba832/resources/WeChat/WeChat03.png -------------------------------------------------------------------------------- /resources/WeChat/WechatIMG2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WayneMao/RoboMatrix/71b077ee5904078f1cbf9008e3fb1256643ba832/resources/WeChat/WechatIMG2.jpg -------------------------------------------------------------------------------- /resources/WeChat/WechatIMG3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WayneMao/RoboMatrix/71b077ee5904078f1cbf9008e3fb1256643ba832/resources/WeChat/WechatIMG3.jpg -------------------------------------------------------------------------------- /resources/eight_skills.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WayneMao/RoboMatrix/71b077ee5904078f1cbf9008e3fb1256643ba832/resources/eight_skills.gif -------------------------------------------------------------------------------- /resources/joystick_mapping.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WayneMao/RoboMatrix/71b077ee5904078f1cbf9008e3fb1256643ba832/resources/joystick_mapping.png -------------------------------------------------------------------------------- /resources/robomatrix-logo3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WayneMao/RoboMatrix/71b077ee5904078f1cbf9008e3fb1256643ba832/resources/robomatrix-logo3.png -------------------------------------------------------------------------------- /robomaster_driver/robomaster_msgs/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.5) 2 | project(robomaster_msgs) 3 | 4 | # Default to C99 5 | if(NOT CMAKE_C_STANDARD) 6 | set(CMAKE_C_STANDARD 99) 7 | endif() 8 | 9 | # Default to C++14 10 | if(NOT CMAKE_CXX_STANDARD) 11 | set(CMAKE_CXX_STANDARD 14) 12 | endif() 13 | 14 | if(CMAKE_COMPILER_IS_GNUCXX OR CMAKE_CXX_COMPILER_ID MATCHES "Clang") 15 | add_compile_options(-Wall -Wextra -Wpedantic) 16 | endif() 17 | 18 | # find dependencies 19 | find_package(ament_cmake REQUIRED) 20 | # uncomment the following section in order to fill in 21 | # further dependencies manually. 22 | # find_package( REQUIRED) 23 | 24 | if(BUILD_TESTING) 25 | find_package(ament_lint_auto REQUIRED) 26 | # the following line skips the linter which checks for copyrights 27 | # uncomment the line when a copyright and license is not present in all source files 28 | #set(ament_cmake_copyright_FOUND TRUE) 29 | # the following line skips cpplint (only works in a git repo) 30 | # uncomment the line when this package is not in a git repo 31 | #set(ament_cmake_cpplint_FOUND TRUE) 32 | ament_lint_auto_find_test_dependencies() 33 | endif() 34 | 35 | find_package(rosidl_default_generators REQUIRED) 36 | 37 | find_package(std_msgs REQUIRED) 38 | 39 | rosidl_generate_interfaces(${PROJECT_NAME} 40 | "msg/ChassisAttitute.msg" 41 | "msg/ChassisPosition.msg" 42 | "msg/ChassisSingleEsc.msg" 43 | "msg/ChassisEsc.msg" 44 | "msg/ChassisIMU.msg" 45 | "msg/GripperStatus.msg" 46 | "msg/ServoSingleStatus.msg" 47 | "msg/ServoStatus.msg" 48 | "msg/RoboticArmPosition.msg" 49 | DEPENDENCIES std_msgs 50 | ) 51 | 52 | ament_package() 53 | -------------------------------------------------------------------------------- /robomaster_driver/robomaster_msgs/msg/ChassisAttitute.msg: -------------------------------------------------------------------------------- 1 | # Chassis raw RPY 2 | 3 | std_msgs/Header header 4 | float32 roll 5 | float32 pitch 6 | float32 yaw 7 | -------------------------------------------------------------------------------- /robomaster_driver/robomaster_msgs/msg/ChassisEsc.msg: -------------------------------------------------------------------------------- 1 | # Chassis esc 2 | 3 | std_msgs/Header header 4 | 5 | robomaster_msgs/ChassisSingleEsc front_right 6 | robomaster_msgs/ChassisSingleEsc front_left 7 | robomaster_msgs/ChassisSingleEsc back_left 8 | robomaster_msgs/ChassisSingleEsc back_right 9 | -------------------------------------------------------------------------------- /robomaster_driver/robomaster_msgs/msg/ChassisIMU.msg: -------------------------------------------------------------------------------- 1 | # Chassis IMU 2 | 3 | std_msgs/Header header 4 | float32 acc_x 5 | float32 acc_y 6 | float32 acc_z 7 | float32 gyro_x 8 | float32 gyro_y 9 | float32 gyro_z 10 | -------------------------------------------------------------------------------- /robomaster_driver/robomaster_msgs/msg/ChassisPosition.msg: -------------------------------------------------------------------------------- 1 | # Chassis raw XYZ 2 | 3 | std_msgs/Header header 4 | float32 x 5 | float32 y 6 | float32 z 7 | -------------------------------------------------------------------------------- /robomaster_driver/robomaster_msgs/msg/ChassisSingleEsc.msg: -------------------------------------------------------------------------------- 1 | # Chassis single esc 2 | 3 | uint16 angle 4 | int16 speed 5 | uint32 time_stamp 6 | uint8 status 7 | -------------------------------------------------------------------------------- /robomaster_driver/robomaster_msgs/msg/GripperStatus.msg: -------------------------------------------------------------------------------- 1 | # gripper status 2 | 3 | std_msgs/Header header 4 | string status 5 | uint8 power 6 | string action 7 | -------------------------------------------------------------------------------- /robomaster_driver/robomaster_msgs/msg/RoboticArmPosition.msg: -------------------------------------------------------------------------------- 1 | # arm end-effector position 2 | 3 | std_msgs/Header header 4 | 5 | int64 x 6 | int64 y 7 | int64 z 8 | -------------------------------------------------------------------------------- /robomaster_driver/robomaster_msgs/msg/ServoSingleStatus.msg: -------------------------------------------------------------------------------- 1 | # servo single status 2 | 3 | bool valid 4 | int32 value 5 | int32 speed 6 | -------------------------------------------------------------------------------- /robomaster_driver/robomaster_msgs/msg/ServoStatus.msg: -------------------------------------------------------------------------------- 1 | # Chassis esc 2 | 3 | std_msgs/Header header 4 | 5 | bool[4] valid # whenever a servo in connected to a port 6 | int32[4] value # the angle of the servos (1024 corresponds to 180 degrees) 7 | int32[4] speed # the speed of the servos (1024 corresponds to about 36 degrees per seconds) 8 | 9 | robomaster_msgs/ServoSingleStatus servo_0 10 | robomaster_msgs/ServoSingleStatus servo_1 11 | robomaster_msgs/ServoSingleStatus servo_2 12 | robomaster_msgs/ServoSingleStatus servo_3 13 | -------------------------------------------------------------------------------- /robomaster_driver/robomaster_msgs/package.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | robomaster_msgs 5 | 0.2.0 6 | TODO: Package description 7 | weiheng_z 8 | TODO: License declaration 9 | 10 | ament_cmake 11 | 12 | ament_lint_auto 13 | ament_lint_common 14 | 15 | rosidl_default_generators 16 | 17 | rosidl_default_runtime 18 | 19 | rosidl_interface_packages 20 | 21 | 22 | ament_cmake 23 | 24 | 25 | -------------------------------------------------------------------------------- /robomaster_driver/robomaster_ros/launch/collect_data.launch.py: -------------------------------------------------------------------------------- 1 | from launch import LaunchDescription 2 | from launch_ros.actions import Node 3 | from launch.actions import ExecuteProcess, DeclareLaunchArgument 4 | from launch.substitutions import LaunchConfiguration 5 | 6 | import os 7 | home_path = os.path.expanduser("~") 8 | 9 | def generate_launch_description(): 10 | return LaunchDescription([ 11 | DeclareLaunchArgument('name', default_value='mobile_manipulation', description='task name'), 12 | DeclareLaunchArgument('idx', default_value='0', description='episode index'), 13 | DeclareLaunchArgument('dir', default_value=home_path+'/RoboMatrixDatasets', description='vedio save directory'), 14 | Node( 15 | package='joystick_driver', 16 | executable='xbox_driver', 17 | name='xbox_driver' 18 | ), 19 | Node( 20 | package='robomaster_ros', 21 | executable='teleoperation', 22 | name='teleoperation' 23 | ), 24 | Node( 25 | package='robomaster_ros', 26 | executable='data_collection', 27 | name='data_collection', 28 | output='screen', 29 | parameters=[{ 30 | 'task_name' : LaunchConfiguration('name'), 31 | "episode_idx": LaunchConfiguration('idx'), 32 | "save_dir" : LaunchConfiguration('dir'), 33 | }] 34 | ), 35 | ]) 36 | -------------------------------------------------------------------------------- /robomaster_driver/robomaster_ros/package.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | robomaster_ros 5 | 0.2.0 6 | TODO: Package description 7 | weiheng_z 8 | TODO: License declaration 9 | 10 | ament_copyright 11 | ament_flake8 12 | ament_pep257 13 | python3-pytest 14 | 15 | rclpy 16 | robomaster_msgs 17 | sensor_msgs 18 | geometry_msgs 19 | std_msgs 20 | std_srvs 21 | cv_bridge 22 | nav_msgs 23 | 24 | ament_python 25 | 26 | 27 | -------------------------------------------------------------------------------- /robomaster_driver/robomaster_ros/resource/robomaster_ros: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WayneMao/RoboMatrix/71b077ee5904078f1cbf9008e3fb1256643ba832/robomaster_driver/robomaster_ros/resource/robomaster_ros -------------------------------------------------------------------------------- /robomaster_driver/robomaster_ros/robomaster_ros/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WayneMao/RoboMatrix/71b077ee5904078f1cbf9008e3fb1256643ba832/robomaster_driver/robomaster_ros/robomaster_ros/__init__.py -------------------------------------------------------------------------------- /robomaster_driver/robomaster_ros/robomaster_ros/data_collection.py: -------------------------------------------------------------------------------- 1 | import rclpy 2 | from rclpy.node import Node 3 | from sensor_msgs.msg import Image 4 | from std_msgs.msg import Bool 5 | from cv_bridge import CvBridge 6 | 7 | from robomaster_msgs.msg import ChassisAttitute, ChassisPosition, ChassisEsc, ChassisIMU 8 | from robomaster_msgs.msg import GripperStatus 9 | from robomaster_msgs.msg import ServoStatus 10 | from robomaster_msgs.msg import RoboticArmPosition 11 | 12 | from collections import deque 13 | from queue import Queue 14 | 15 | import os, time 16 | import json 17 | import cv2 18 | import threading 19 | 20 | 21 | FPS = 30 22 | TASK_PROMPT = "None." 23 | 24 | 25 | class DataCollectionNode(Node): 26 | 27 | def __init__(self) -> None: 28 | super().__init__('rm_data_collection') 29 | 30 | # save path params 31 | from datetime import datetime 32 | now = datetime.now().date().strftime("%Y-%m-%d") 33 | self.declare_parameter("task_name", now).value 34 | 35 | self.declare_parameter("episode_idx", 0).value 36 | 37 | home_path = os.path.expanduser("~") 38 | self.declare_parameter("save_dir", home_path+'/RoboMatrixDatasets').value 39 | 40 | self._task_name: str = self.get_parameter("task_name").get_parameter_value().string_value 41 | self._episode_idx: str = self.get_parameter("episode_idx").get_parameter_value().integer_value 42 | self._save_dir: str = self.get_parameter("save_dir").get_parameter_value().string_value 43 | self.get_logger().info(f'task_name: {self._task_name}') 44 | self.get_logger().info(f'episode_idx: {self._episode_idx}') 45 | self.get_logger().info(f'save_dir: {self._save_dir}') 46 | 47 | self._initialize_parameters() 48 | 49 | # visual 50 | self._sub_image = self.create_subscription(Image, "/camera/raw/image", self._image_cb, 1000) 51 | self._image_deque = deque() 52 | self._image_queue = Queue(maxsize=2000) 53 | 54 | # subscribe robotic arm topic to ger end effector position 55 | self._sub_arm_position = self.create_subscription(RoboticArmPosition, "/arm/raw/position", self._arm_position_cb, 1000) 56 | self._arm_position_deque = deque() 57 | 58 | # subscribe servo topic to get joint value 59 | self._sub_servo_value = self.create_subscription(ServoStatus, "/servo/raw/value", self._servo_value_cb, 1000) 60 | self._servo_value_deque = deque() 61 | 62 | # subscribe gripper topic to get status 63 | self._sub_gripper_status = self.create_subscription(GripperStatus, "/gripper/raw/status", self._gripper_status_cb, 1000) 64 | self._gripper_status_deque = deque() 65 | 66 | # subscribe chassis topic to get raw data 67 | self._sub_chassis_esc = self.create_subscription(ChassisEsc, "/chassis/raw/esc", self._chassis_esc_cb, 1000) 68 | self._chassis_esc_data_deque = deque() 69 | self._sub_chassis_position = self.create_subscription(ChassisPosition, "/chassis/raw/position", self._chassis_position_cb, 1000) 70 | self._chassis_position_deque = deque() 71 | self._sub_chassis_attitute = self.create_subscription(ChassisAttitute, "/chassis/raw/attitute", self._chassis_attitute_cb, 1000) 72 | self._chassis_attitute_deque = deque() 73 | self._sub_chassis_imu = self.create_subscription(ChassisIMU, "/chassis/raw/imu", self._chassis_imu_cb, 1000) 74 | self._chassis_imu_deque = deque() 75 | 76 | self._sub_action = self.create_subscription(Bool, '/data_collection_start', self._action_cb, 10) 77 | 78 | self._cv_bridge = CvBridge() 79 | 80 | self._rate = 30 81 | self._max_frame_num = 10000 82 | 83 | self._can_action = False 84 | 85 | self.thread = threading.Thread(target=self._collect) 86 | self.thread.start() 87 | self._lock = threading.Lock() 88 | 89 | self.end = False 90 | 91 | def _initialize_parameters(self): 92 | # self._file_name = f'episode_{self._episode_idx}' 93 | self._file_name = f"{self._task_name}_{self._episode_idx}" 94 | # 数据集文件夹 95 | if not os.path.exists(self._save_dir): os.mkdir(self._save_dir) 96 | # 任务文件夹 97 | self._task_dir = os.path.join(self._save_dir, self._task_name) 98 | if not os.path.exists(self._task_dir): os.mkdir(self._task_dir) 99 | # 视频文件夹 100 | self._video_dir = os.path.join(self._task_dir, "videos") 101 | if not os.path.exists(self._video_dir): os.mkdir(self._video_dir) 102 | # 标注文件夹 103 | self._anno_dir = os.path.join(self._task_dir, "annotations") 104 | if not os.path.exists(self._anno_dir): os.mkdir(self._anno_dir) 105 | 106 | def _image_cb(self, msg: Image): 107 | try: 108 | if len(self._image_deque) >= 1: self._image_deque.popleft() 109 | except: pass 110 | if not self._can_action: return 111 | time_stamp = self.to_sec(msg.header.stamp) 112 | image = self._cv_bridge.imgmsg_to_cv2(msg, 'passthrough') 113 | data = (time_stamp, image) 114 | self._image_deque.append(data) 115 | 116 | def _arm_position_cb(self, msg: RoboticArmPosition): 117 | if len(self._arm_position_deque) >= 100: self._arm_position_deque.popleft() 118 | time_stamp = self.to_sec(msg.header.stamp) 119 | x, y = msg.x, msg.y 120 | data = (time_stamp, [x, y]) 121 | self._arm_position_deque.append(data) 122 | 123 | def _servo_value_cb(self, msg: ServoStatus): 124 | if len(self._servo_value_deque) >= 100: self._servo_value_deque.popleft() 125 | time_stamp = self.to_sec(msg.header.stamp) 126 | values = [msg.servo_0.value, msg.servo_1.value] 127 | speeds = [msg.servo_0.speed, msg.servo_1.speed] 128 | data = (time_stamp, values, speeds) 129 | self._servo_value_deque.append(data) 130 | 131 | def _gripper_status_cb(self, msg: GripperStatus): 132 | if len(self._gripper_status_deque) >= 100: self._gripper_status_deque.popleft() 133 | time_stamp = self.to_sec(msg.header.stamp) 134 | gripper_status = msg.status 135 | gripper_power = msg.power 136 | gripper_action = msg.action 137 | data = (time_stamp, gripper_status, gripper_power, gripper_action) 138 | self._gripper_status_deque.append(data) 139 | 140 | def _chassis_esc_cb(self, msg: ChassisEsc): 141 | if len(self._chassis_esc_data_deque) >= 100: self._chassis_esc_data_deque.popleft() 142 | time_stamp = self.to_sec(msg.header.stamp) 143 | angles = [msg.front_right.angle, msg.front_left.angle, msg.back_left.angle, msg.back_right.angle] 144 | speeds = [msg.front_right.speed, msg.front_left.speed, msg.back_left.speed, msg.back_right.speed] 145 | data = (time_stamp, angles, speeds) 146 | self._chassis_esc_data_deque.append(data) 147 | 148 | def _chassis_position_cb(self, msg: ChassisPosition): 149 | if len(self._chassis_position_deque) >= 100: self._chassis_position_deque.popleft() 150 | time_stamp = self.to_sec(msg.header.stamp) 151 | position = [round(msg.x * 1000, 2), round(msg.y * 1000, 2)] 152 | data = (time_stamp, position) 153 | self._chassis_position_deque.append(data) 154 | 155 | def _chassis_attitute_cb(self, msg: ChassisAttitute): 156 | if len(self._chassis_attitute_deque) >= 100: self._chassis_attitute_deque.popleft() 157 | time_stamp = self.to_sec(msg.header.stamp) 158 | attitute = [round(msg.roll, 2), round(msg.pitch, 2), round(msg.yaw, 2)] 159 | data = (time_stamp, attitute) 160 | self._chassis_attitute_deque.append(data) 161 | 162 | def _chassis_imu_cb(self, msg: ChassisIMU): 163 | if len(self._chassis_imu_deque) >= 100: self._chassis_imu_deque.popleft() 164 | time_stamp = self.to_sec(msg.header.stamp) 165 | linear_acceleration = [msg.acc_x, msg.acc_y, msg.acc_z] 166 | angular_velocity = [msg.gyro_x, msg.gyro_y, msg.gyro_z] 167 | data = (time_stamp, linear_acceleration, angular_velocity) 168 | self._chassis_imu_deque.append(data) 169 | 170 | def _action_cb(self, msg: Bool): 171 | self._can_action = msg.data 172 | 173 | def _get_frame(self) -> bool: 174 | if not self._image_deque: 175 | # self.get_logger().error("image deque is empty") 176 | return False 177 | 178 | tolerance = 0.05 179 | 180 | frame_time = self._image_deque[-1][0] - tolerance # 图像缓存中最新的数据作为标准 181 | 182 | # get image 183 | while self._image_deque: 184 | time_stamp, image = self._image_deque.popleft() 185 | if time_stamp >= frame_time: break 186 | else: pass 187 | 188 | # get chassis position 189 | while self._chassis_position_deque: 190 | time_stamp, chassis_position = self._chassis_position_deque.popleft() 191 | if time_stamp >= frame_time: break 192 | else: 193 | self.get_logger().error("get chassis position fail") 194 | return False 195 | 196 | # get chassis attitute 197 | while self._chassis_attitute_deque: 198 | time_stamp, chassis_attitute = self._chassis_attitute_deque.popleft() 199 | if time_stamp >= frame_time: break 200 | else: 201 | self.get_logger().error("get chassis attitute fail") 202 | return False 203 | 204 | # get arm position 205 | while self._arm_position_deque: 206 | time_stamp, end_position = self._arm_position_deque.popleft() 207 | if time_stamp >= frame_time: break 208 | else: 209 | self.get_logger().error("get arm position fail") 210 | return False 211 | 212 | # get gripper status 213 | while self._gripper_status_deque: 214 | time_stamp, gripper_status, gripper_power, gripper_command = self._gripper_status_deque.popleft() 215 | gripper_action = [gripper_command, gripper_power] 216 | if time_stamp >= frame_time: break 217 | else: 218 | self.get_logger().error("get gripper status fail") 219 | return False 220 | 221 | frame_dict = { 222 | "image": image, 223 | "arm_position": end_position, 224 | "gripper_status": gripper_status, 225 | "gripper_action": gripper_action, 226 | "chassis_position": chassis_position, 227 | "chassis_attitute": chassis_attitute, 228 | } 229 | 230 | return frame_dict 231 | 232 | def _collect(self): 233 | visual = [] 234 | observations = { 235 | 'arm_position': [], 236 | 'gripper_status': [], 237 | 'gripper_action': [], 238 | "chassis_position": [], 239 | "chassis_attitute": [], 240 | } 241 | frame_count, fail_count= 0, 0 242 | 243 | while rclpy.ok(): 244 | self.get_logger().info("Wait to start data collection") 245 | while rclpy.ok() and not self._can_action: pass 246 | self.get_logger().info("start collect") 247 | 248 | start_time = None 249 | while rclpy.ok() and frame_count <= self._max_frame_num and self._can_action: 250 | fps = int(1 / (time.time() - start_time)) if start_time else 0 251 | start_time = time.time() 252 | 253 | frame_dict = self._get_frame() 254 | if not frame_dict: 255 | fail_count += 1 256 | continue 257 | else: frame_count += 1 258 | 259 | if fps <= self._rate: 260 | self.get_logger().info(f"Get a frame {frame_count} / {self._max_frame_num} ({fps} FPS).") 261 | else: 262 | self.get_logger().warn(f"Get a frame {frame_count} / {self._max_frame_num} ({fps} FPS).") 263 | 264 | for name, data in frame_dict.items(): 265 | if name == "image": 266 | data = cv2.cvtColor(data, cv2.COLOR_BGR2RGB) 267 | visual.append(data) 268 | elif name in observations.keys(): observations[name].append(data) 269 | 270 | if not rclpy.ok(): exit(-1) 271 | 272 | time.sleep(max(0, 1 / self._rate - (time.time() - start_time))) # 控制采样频率 273 | 274 | self.get_logger().warn(f"Fail count {fail_count}") 275 | # self._save_video(frame_count, visual, FPS) 276 | self._save_video_v2(visual, FPS) 277 | self._save_annotation(frame_count, observations) 278 | 279 | self._can_action = False 280 | break 281 | 282 | self.end = True 283 | 284 | def _save_video(self, frame_count, img_list, fps): 285 | if frame_count == 0: 286 | self.get_logger().error(f"No frame to save") 287 | return 288 | 289 | video_path = os.path.join(self._video_dir, f'{self._file_name}.mp4') 290 | 291 | height, width, _ = img_list[0].shape 292 | out = cv2.VideoWriter(video_path, cv2.VideoWriter_fourcc(*'mp4v'), fps, (width, height)) 293 | for t in range(frame_count): 294 | img = img_list[t] 295 | out.write(img) 296 | out.release() 297 | self.get_logger().info(f"Save video to: {video_path}") 298 | 299 | def _save_video_v2(self, images, fps): 300 | from moviepy.editor import ImageSequenceClip 301 | clip = ImageSequenceClip(images, fps=fps) 302 | video_path = os.path.join(self._video_dir, f'{self._file_name}.mp4') 303 | clip.write_videofile(video_path, codec='libx264') 304 | self.get_logger().info(f"Save video to: {video_path}") 305 | 306 | def _save_annotation(self, frame_count, observations): 307 | if frame_count == 0: 308 | self.get_logger().error(f"No annotation to save") 309 | return 310 | 311 | video_annotation = [] 312 | 313 | for index in range(frame_count): 314 | frame_annotation = {} 315 | 316 | # frame_annotation['stream'] = f"{self._task_name}/videos/{self._file_name}.mp4" # video name 317 | frame_annotation['stream'] = f"{self._file_name}.mp4" # video name 318 | frame_annotation['frame_inds'] = [index] # frame index 319 | 320 | frame_annotation["observations"] = {} 321 | for key, data in observations.items(): 322 | frame_annotation["observations"][key] = data[index] 323 | 324 | conversation = [] 325 | conv_human, conv_agent = {}, {} 326 | conv_human['from'] = 'human' 327 | conv_human['value'] = 'prompt' 328 | conv_agent['from'] = 'gpt' 329 | conv_agent['value'] = 'tokens' 330 | conversation.append(conv_human) 331 | conversation.append(conv_agent) 332 | frame_annotation['conversations'] = conversation 333 | 334 | video_annotation.append(frame_annotation) 335 | 336 | json_path = os.path.join(self._anno_dir, f'{self._file_name}.json') 337 | with open(json_path, 'w', encoding='utf-8') as f: 338 | json.dump(video_annotation, f, ensure_ascii=False, indent=4) 339 | self.get_logger().info(f"Save annotation to: {json_path}") 340 | 341 | def to_sec(self, frame_time): 342 | sec = frame_time.sec 343 | nanosec = frame_time.nanosec 344 | return sec + nanosec / 1e9 345 | 346 | def main(args=None): 347 | rclpy.init(args=args) 348 | 349 | node = DataCollectionNode() 350 | while rclpy.ok(): 351 | rclpy.spin_once(node, timeout_sec=1) 352 | if node.end: break 353 | 354 | node.destroy_node() 355 | rclpy.shutdown() 356 | -------------------------------------------------------------------------------- /robomaster_driver/robomaster_ros/robomaster_ros/teleoperation.py: -------------------------------------------------------------------------------- 1 | import rclpy 2 | from rclpy.node import Node 3 | 4 | from robomaster import robot 5 | 6 | from sensor_msgs.msg import Imu 7 | from pyquaternion import Quaternion 8 | from sensor_msgs.msg import Image 9 | from nav_msgs.msg import Odometry 10 | from geometry_msgs.msg import PointStamped, TransformStamped 11 | from std_msgs.msg import Bool 12 | import tf2_ros 13 | 14 | from joystick_msgs.msg import XBox360Controller 15 | from robomaster_msgs.msg import ChassisAttitute, ChassisPosition, ChassisStatus, ChassisEsc, ChassisIMU 16 | from robomaster_msgs.msg import GripperStatus 17 | from robomaster_msgs.msg import ServoStatus 18 | from robomaster_msgs.msg import RoboticArmPosition 19 | 20 | from cv_bridge import CvBridge 21 | 22 | from collections import deque 23 | from threading import Thread, Event 24 | import time 25 | 26 | 27 | INIT_POS = (180, -70) 28 | FINAL_POS = (100, 100) 29 | 30 | 31 | class TeleOperationNode(Node): 32 | 33 | def __init__(self) -> None: 34 | super().__init__('rm_teleoperation') 35 | self._log = False 36 | 37 | self._robot = robot.Robot() 38 | 39 | self._joint_angle = [None] * 2 40 | self._joint_speed = [None] * 2 41 | self._end_position = [None] * 2 + [0] 42 | 43 | # arm pub 44 | self._pub_arm_position = self.create_publisher(RoboticArmPosition, "/arm/raw/position", 3) # end position publisher 45 | 46 | # servo pub 47 | self._pub_servo_value = self.create_publisher(ServoStatus, "/servo/raw/value", 3) # joint value publisher 48 | 49 | # gripper pub 50 | self._pub_gripper_status = self.create_publisher(GripperStatus, "/gripper/raw/status", 3) # gripper status publisher 51 | 52 | # img pub 53 | self._pub_image = self.create_publisher(Image, "/camera/raw/image", 3) # image publisher 54 | 55 | # chassis pub 56 | self._pub_chassis_attitute = self.create_publisher(ChassisAttitute, "/chassis/raw/attitute", 3) # rpy publisher 57 | self._pub_chassis_position = self.create_publisher(ChassisPosition, "/chassis/raw/position", 3) # xyz publisher 58 | self._pub_chassis_status = self.create_publisher(ChassisStatus, "/chassis/raw/status", 3) # status publisher 59 | self._pub_chassis_esc = self.create_publisher(ChassisEsc, "/chassis/raw/esc", 3) # esc publisher 60 | self._pub_chassis_imu = self.create_publisher(ChassisIMU, "/chassis/raw/imu", 3) # imu publisher 61 | 62 | # action pub 63 | self._pub_action = self.create_publisher(Bool, '/data_collection_start', 3) # code flag publisher 64 | 65 | # joy sub 66 | self._sub_joy = self.create_subscription(XBox360Controller, '/joystick', self._joy_cb, 3) 67 | 68 | self._cv_bridge = CvBridge() 69 | 70 | self._axis_threshold = 0.1 71 | 72 | self._event = Event() 73 | 74 | # arm 75 | self._arm_deque = deque() 76 | self._arm_moving = False 77 | self._arm_thread = Thread(target=self._arm_action) 78 | self._arm_thread.start() 79 | self._arm_postion = [] 80 | 81 | # flags 82 | self._action_flag = True 83 | self._action_activate = False 84 | 85 | # main 86 | self._main_thread = Thread(target=self._start) 87 | self._main_thread.start() 88 | 89 | # chassis 90 | self._chassis_deque = deque() 91 | self._chassis_moving = False 92 | self._chassis_thread = Thread(target=self._chassis_action) 93 | self._chassis_thread.start() 94 | 95 | # gripper 96 | self._gripper_deque = deque() 97 | self._gripper_status = None 98 | self._gripper_moving = False 99 | self._gripper_command = "None" 100 | self._gripper_power = None 101 | self._gripper_thread = Thread(target=self._gripper_action) 102 | self._gripper_thread.start() 103 | 104 | self.shutdown = False 105 | self._gripper_action_end = False 106 | self._chassis_action_end = False 107 | self._arm_action_end = False 108 | 109 | def _init_sub(self, freq=50) -> bool: 110 | if freq not in (1, 5, 10, 20, 50): return False 111 | # 末端 112 | self._arm.sub_position(freq, self._arm_position_cb) 113 | # 关节 114 | self._servo.sub_servo_info(freq, self._servo_value_cb) 115 | # 夹爪 116 | self._gripper.sub_status(freq, self._gripper_status_cb) 117 | # 相机 118 | self._vision.start_video_stream(display=False) 119 | self._cam_thread = Thread(target=self._get_image) 120 | self._cam_thread.start() 121 | # 底盘姿态 122 | self._init_orientation, self._last_attitude = None, None 123 | self._chassis.sub_attitude(freq, self._chassis_attitude_cb) 124 | # 底盘位置 125 | self._chassis.sub_position(0, freq, self._odom_cb) 126 | # IMU 127 | self._chassis.sub_imu(freq, self._chassis_imu_cb) 128 | # 底盘状态 129 | self._chassis.sub_status(freq, self._chassis_status_cb) 130 | # 电调 131 | self._chassis.sub_esc(freq, self._chassis_esc_cb) 132 | return True 133 | 134 | def _unsub(self): 135 | self._arm.unsub_position() 136 | self._gripper.unsub_status() 137 | self._servo.unsub_servo_info() 138 | 139 | self._vision.stop_video_stream() 140 | 141 | self._chassis.unsub_attitude() 142 | self._chassis.unsub_position() 143 | self._chassis.unsub_imu() 144 | self._chassis.unsub_status() 145 | self._chassis.unsub_esc() 146 | 147 | self._robot.close() 148 | 149 | def arm_move(self, position): 150 | x, y = position 151 | self._arm.move(x, y).wait_for_completed(timeout=1) 152 | 153 | def arm_moveto(self, position): 154 | x, y = position 155 | self._arm.moveto(x, y).wait_for_completed(timeout=1) 156 | 157 | def arm_recenter(self): 158 | self._arm.moveto(5, 5).wait_for_completed() 159 | self.get_logger().info("Arm recenter") 160 | 161 | def gripper(self, action, power=50): 162 | if power not in [power for power in range(1,100)]: return False 163 | 164 | self._gripper_command = action 165 | self._gripper_power = power 166 | if action == "grab": self._gripper.close(power) 167 | elif action == "release": self._gripper.open(power) 168 | elif action == "pause": self._gripper.pause() 169 | else: return 170 | 171 | def _get_image(self): 172 | while rclpy.ok(): 173 | try: 174 | # tsn = time.time() 175 | img = self._vision.read_cv2_image(timeout=0.1, strategy='newest') 176 | # tsn2 = time.time() 177 | msg = self._cv_bridge.cv2_to_imgmsg(img, encoding="bgr8") 178 | # msg.header.stamp = tsn + (tsn2 - tsn) / 2.0 179 | msg.header.stamp = self.get_clock().now().to_msg() 180 | msg.header.frame_id = "camera_link_optical_frame" 181 | self._pub_image.publish(msg) 182 | except: 183 | pass 184 | 185 | def chassis_move(self, x_dis, y_dis, z_angle, xy_vel=0, z_w=0): 186 | self._chassis.move(x_dis, y_dis, z_angle, xy_vel, z_w).wait_for_completed(timeout=10) 187 | 188 | def chassis_wheels(self, wheel_speed, /, *, timeout=1): 189 | w1, w2, w3, w4 = wheel_speed 190 | self._chassis.drive_wheels(w1, w2, w3, w4, timeout) 191 | 192 | def chassis_speed(self, x_vel, y_vel, z_w, /, *, timeout=1): 193 | self._chassis.drive_speed(x_vel, y_vel, z_w, timeout) 194 | 195 | def _chassis_attitude_cb(self, attitude_data): 196 | yaw, pitch, roll = attitude_data 197 | if self._log: self.get_logger().info("chassis attitude: yaw:{0}".format(yaw)) 198 | 199 | msg = ChassisAttitute() 200 | msg.header.stamp = self.get_clock().now().to_msg() 201 | msg.header.frame_id = "chassis_attitude_raw" 202 | msg.yaw = yaw 203 | msg.pitch = pitch 204 | msg.roll = roll 205 | self._pub_chassis_attitute.publish(msg) 206 | 207 | self._last_attitude = Quaternion(axis=(0, 0, 1), degrees=-yaw) * Quaternion(axis=(0, 1, 0), degrees=-pitch) * Quaternion(axis=(1, 0, 0), degrees=roll) 208 | if self._init_orientation is None: self._init_orientation = self._last_attitude 209 | 210 | def _odom_cb(self, position_data): 211 | x, y, z = position_data 212 | if self._log: self.get_logger().info("chassis position: x:{0}, y:{1}".format(x, y)) # x 前正 y 右正 213 | 214 | msg = ChassisPosition() 215 | msg.header.stamp = self.get_clock().now().to_msg() 216 | msg.header.frame_id = "chassis_position_raw" 217 | msg.x = x 218 | msg.y = y 219 | msg.z = z 220 | self._pub_chassis_position.publish(msg) 221 | 222 | def _chassis_imu_cb(self, imu_data): 223 | acc_x, acc_y, acc_z, gyro_x, gyro_y, gyro_z = imu_data 224 | if self._log: print("chassis imu: acc_x:{0}, acc_y:{1}, acc_z:{2}, gyro_x:{3}, gyro_y:{4}, gyro_z:{5}".format(acc_x, acc_y, acc_z, gyro_x, gyro_y, gyro_z)) 225 | 226 | msg = ChassisIMU() 227 | msg.header.stamp = self.get_clock().now().to_msg() 228 | msg.header.frame_id = "chassis_imu_raw" 229 | msg.acc_x = acc_x 230 | msg.acc_y = acc_y 231 | msg.acc_z = acc_z 232 | msg.gyro_x = gyro_x 233 | msg.gyro_y = gyro_y 234 | msg.gyro_z = gyro_z 235 | self._pub_chassis_imu.publish(msg) 236 | 237 | def _chassis_status_cb(self, status_info): 238 | static_flag, up_hill, down_hill, on_slope, pick_up, slip_flag, impact_x, impact_y, impact_z, roll_over, hill_static = status_info 239 | if self._log: print("chassis status: static_flag:{0}, up_hill:{1}, down_hill:{2}, on_slope:{3}, " 240 | "pick_up:{4}, slip_flag:{5}, impact_x:{6}, impact_y:{7}, impact_z:{8}, roll_over:{9}, " 241 | "hill_static:{10}".format(static_flag, up_hill, down_hill, on_slope, pick_up, 242 | slip_flag, impact_x, impact_y, impact_z, roll_over, hill_static)) 243 | 244 | msg = ChassisStatus() 245 | msg.header.stamp = self.get_clock().now().to_msg() 246 | msg.header.frame_id = "chassis_status_raw" 247 | msg.is_static = bool(static_flag) 248 | msg.up_hill = bool(up_hill) 249 | msg.down_hill = bool(down_hill) 250 | msg.on_slope = bool(on_slope) 251 | msg.is_pick_up = bool(pick_up) 252 | msg.slip = bool(slip_flag) 253 | msg.impact_x = bool(impact_x) 254 | msg.impact_y = bool(impact_y) 255 | msg.impact_z = bool(impact_z) 256 | msg.roll_over = bool(roll_over) 257 | msg.hill = bool(hill_static) 258 | self._pub_chassis_status.publish(msg) 259 | 260 | def _chassis_esc_cb(self, esc_data): 261 | speed, angle, timestamp, state = esc_data 262 | if self._log: print("chassis esc: speed:{0}, angle:{1}, timestamp:{2}, state:{3}".format(speed, angle, timestamp, state)) 263 | 264 | msg = ChassisEsc() 265 | msg.header.stamp = self.get_clock().now().to_msg() 266 | msg.header.frame_id = "chassis_esc_raw" 267 | # 前左 268 | msg.front_right.angle = angle[0] 269 | msg.front_right.speed = speed[0] 270 | msg.front_right.time_stamp = timestamp[0] 271 | msg.front_right.status = state[0] 272 | # 前右 273 | msg.front_left.angle = angle[1] 274 | msg.front_left.speed = speed[1] 275 | msg.front_left.time_stamp = timestamp[1] 276 | msg.front_left.status = state[1] 277 | # 后左 278 | msg.back_left.angle = angle[2] 279 | msg.back_left.speed = speed[2] 280 | msg.back_left.time_stamp = timestamp[2] 281 | msg.back_left.status = state[2] 282 | # 后右 283 | msg.back_right.angle = angle[3] 284 | msg.back_right.speed = speed[3] 285 | msg.back_right.time_stamp = timestamp[3] 286 | msg.back_right.status = state[3] 287 | self._pub_chassis_esc.publish(msg) 288 | 289 | def _arm_position_cb(self, position_data): 290 | pos_x, pos_y = position_data 291 | self._arm_postion = [pos_x, pos_y] 292 | if self._log: self.get_logger().info("Arm: x = {0}, y = {1}".format(pos_x, pos_y)) 293 | 294 | msg = RoboticArmPosition() 295 | msg.header.stamp = self.get_clock().now().to_msg() 296 | msg.header.frame_id = "robotic_arm_position" 297 | msg.x = pos_x 298 | msg.y = pos_y 299 | msg.z = 0 300 | self._pub_arm_position.publish(msg) 301 | 302 | def _gripper_status_cb(self, status_info): 303 | self._gripper_status = status_info 304 | if self._log: print("Gripper: {}".format(self._gripper_status)) 305 | 306 | msg = GripperStatus() 307 | msg.header.stamp = self.get_clock().now().to_msg() 308 | msg.header.frame_id = "gripper_status_raw" 309 | msg.status = self._gripper_status 310 | msg.power = self._gripper_power 311 | msg.action = self._gripper_command 312 | self._pub_gripper_status.publish(msg) 313 | 314 | def _servo_value_cb(self, servo_data): 315 | status, speed, angle = servo_data 316 | if self._log: print("Servo: status: {}, speed: {}, angle: {}".format(status, speed, angle)) 317 | 318 | msg = ServoStatus() 319 | msg.header.stamp = self.get_clock().now().to_msg() 320 | msg.servo_0.valid = bool(status[0]) 321 | msg.servo_0.value = angle[0] 322 | msg.servo_0.speed = speed[0] 323 | msg.servo_1.valid = bool(status[1]) 324 | msg.servo_1.value = angle[1] 325 | msg.servo_1.speed = speed[1] 326 | msg.servo_2.valid = bool(status[2]) 327 | msg.servo_2.value = angle[2] 328 | msg.servo_2.speed = speed[2] 329 | msg.servo_3.valid = bool(status[3]) 330 | msg.servo_3.value = angle[3] 331 | msg.servo_3.speed = speed[3] 332 | 333 | self._pub_servo_value.publish(msg) 334 | 335 | def _start(self): 336 | try: 337 | while not self._robot.initialize(conn_type="sta", sn="3JKCJC400301RW"): 338 | self.get_logger().error("Initialize fail") 339 | self._robot.set_robot_mode(mode=robot.FREE) 340 | self.get_logger().info("RM init") 341 | 342 | self._arm = self._robot.robotic_arm 343 | self._gripper = self._robot.gripper 344 | self._servo = self._robot.servo 345 | self._vision = self._robot.camera 346 | self._chassis = self._robot.chassis 347 | 348 | self._init_sub() 349 | 350 | self._event.set() 351 | 352 | self.get_logger().info("Press START button to start") 353 | while rclpy.ok(): 354 | if self._action_activate: break 355 | self.get_logger().info("Press BACK button to shut down") 356 | while rclpy.ok(): 357 | if not self._action_activate: break 358 | except KeyboardInterrupt: 359 | self.get_logger().error("Shut down") 360 | finally: 361 | while not self._gripper_action_end or not self._chassis_action_end or not self._arm_action_end: pass 362 | self._unsub() 363 | self.get_logger().info("Node End") 364 | self.shutdown = True 365 | 366 | # core code 367 | def _joy_cb(self, msg: XBox360Controller): 368 | current_time = time.time() 369 | 370 | # data collection 371 | if msg.button_start == 1 and self._action_flag: 372 | action = Bool() 373 | action.data = True 374 | self._pub_action.publish(action) 375 | self._action_activate = True 376 | self._action_flag = False 377 | if msg.button_back == 1 and not self._action_flag: 378 | action = Bool() 379 | action.data = False 380 | self._pub_action.publish(action) 381 | self._action_activate = False 382 | self._action_flag = True 383 | 384 | # arm 385 | if msg.hat_x != 0 or msg.hat_y != 0: 386 | # k = 10 387 | k = 20 388 | if len(self._arm_deque) > 20: self._arm_deque.popleft() 389 | self._arm_deque.append((current_time, self.arm_move, (k*msg.hat_y, k*msg.hat_x))) 390 | # if msg.button_rb == 1: self._arm_deque.append((current_time, self.arm_moveto, (75, 145))) 391 | # if msg.button_lb == 1: self._arm_deque.append((current_time, self.arm_moveto, (200, -50))) 392 | 393 | # gripper 394 | if msg.button_a == 1: self._gripper_deque.append((current_time, self.gripper, ("release", 70))) 395 | if msg.button_b == 1: self._gripper_deque.append((current_time, self.gripper, ("grab", 15))) 396 | # if msg.button_x == 1: self._gripper_deque.append((current_time, self.gripper, ("pause", 50))) 397 | 398 | # chassis 399 | if msg.button_rs == 1: k, k_w = 0.4, 40 400 | # else: k, k_w = 0.1, 15 401 | else: k, k_w = 0.1, 5 402 | x_vel = -msg.axis_rs_y if abs(msg.axis_rs_y) >= self._axis_threshold else 0 403 | y_vel = msg.axis_rs_x if abs(msg.axis_rs_x) >= self._axis_threshold else 0 404 | delta = msg.axis_lt - msg.axis_rt 405 | z_w = -delta if abs(delta) >= self._axis_threshold else 0 406 | # z_w = 0 407 | if x_vel == 0 and y_vel == 0 and z_w == 0 and not self._chassis_moving: return 408 | if x_vel == 0 and y_vel == 0 and z_w == 0: self._chassis_moving = False 409 | else: self._chassis_moving = True 410 | self._chassis_deque.append((current_time, self.chassis_speed, (x_vel*k, y_vel*k, z_w*k_w))) 411 | 412 | def _gripper_action(self): 413 | self._event.wait() 414 | self.gripper("release", 70) 415 | try: 416 | while rclpy.ok() and not self._action_activate: pass 417 | self.get_logger().info("Gripper action activate") 418 | while rclpy.ok(): 419 | if not self._action_activate: break 420 | if not self._gripper_deque: continue 421 | 422 | current_time = time.time() 423 | while self._gripper_deque: 424 | stamp, func, args = self._gripper_deque.popleft() 425 | if stamp > current_time: break 426 | func(*args) 427 | except KeyboardInterrupt: 428 | self.get_logger().error("Gripper action shut down") 429 | finally: 430 | self.gripper("release", 70) 431 | self.get_logger().info("Gripper action End") 432 | self._gripper_action_end = True 433 | 434 | def _chassis_action(self): 435 | self._event.wait() 436 | try: 437 | while rclpy.ok() and not self._action_activate: pass 438 | self.get_logger().info("Chassis action activate") 439 | while rclpy.ok(): 440 | if not self._action_activate: break 441 | if not self._chassis_deque: continue 442 | 443 | current_time = time.time() 444 | while self._chassis_deque: 445 | stamp, func, args = self._chassis_deque.popleft() 446 | if stamp > current_time: break 447 | func(*args) 448 | except KeyboardInterrupt: 449 | self.get_logger().error("Chassis action shut down") 450 | finally: 451 | self.get_logger().info("Chassis action End") 452 | self._chassis_action_end = True 453 | 454 | def _arm_action(self): 455 | self._event.wait() 456 | # self.arm_moveto((100, 100)) 457 | # time.sleep(0.5) 458 | self.arm_moveto(INIT_POS) 459 | time.sleep(0.5) 460 | try: 461 | while rclpy.ok() and not self._action_activate: pass 462 | self.get_logger().info("Arm action activate") 463 | while rclpy.ok(): 464 | if not self._action_activate: break 465 | if not self._arm_deque: continue 466 | 467 | current_time = time.time() 468 | while self._arm_deque: 469 | stamp, func, position = self._arm_deque.popleft() 470 | if stamp > current_time: break 471 | func(position) 472 | except KeyboardInterrupt: 473 | self.get_logger().error("Arm action shut down") 474 | finally: 475 | self.arm_moveto(FINAL_POS) 476 | self.get_logger().info("Arm action End") 477 | self._arm_action_end = True 478 | 479 | 480 | def main(args=None): 481 | rclpy.init(args=args) 482 | 483 | node = TeleOperationNode() 484 | try: 485 | while rclpy.ok(): 486 | rclpy.spin_once(node, timeout_sec=1) 487 | if node.shutdown: break 488 | except KeyboardInterrupt: pass 489 | finally: 490 | node.destroy_node() 491 | rclpy.shutdown() 492 | -------------------------------------------------------------------------------- /robomaster_driver/robomaster_ros/setup.cfg: -------------------------------------------------------------------------------- 1 | [develop] 2 | script-dir=$base/lib/robomaster_ros 3 | [install] 4 | install-scripts=$base/lib/robomaster_ros 5 | -------------------------------------------------------------------------------- /robomaster_driver/robomaster_ros/setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup 2 | import glob 3 | 4 | package_name = 'robomaster_ros' 5 | 6 | setup( 7 | name=package_name, 8 | version='0.0.0', 9 | packages=[package_name], 10 | data_files=[ 11 | ('share/ament_index/resource_index/packages', 12 | ['resource/' + package_name]), 13 | ('share/' + package_name, ['package.xml']), 14 | ('share/' + package_name + '/launch', glob.glob('launch/*.launch')), 15 | ('share/' + package_name + '/launch', glob.glob('launch/*.launch.py')), 16 | ('share/' + package_name + '/config', glob.glob('config/*')) 17 | ], 18 | install_requires=['setuptools', 'numpy', 'numpy-quaternion', 'pyyaml', 'robomaster'], 19 | zip_safe=True, 20 | maintainer='weiheng zhong', 21 | maintainer_email='weiheng_z@bit.edu.cn', 22 | description='TODO: Package description', 23 | license='TODO: License declaration', 24 | tests_require=['pytest'], 25 | entry_points={ 26 | 'console_scripts': [ 27 | 'teleoperation = robomaster_ros.teleoperation:main', 28 | 'data_collection = robomaster_ros.data_collection:main', 29 | ], 30 | }, 31 | ) 32 | -------------------------------------------------------------------------------- /robomatrix_client/package.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | robomatrix_client 5 | 0.0.0 6 | RoboMatrix client for task planning and managing 7 | jzian 8 | TODO: License declaration 9 | 10 | rclpy 11 | 12 | ament_copyright 13 | ament_flake8 14 | ament_pep257 15 | python3-pytest 16 | 17 | 18 | ament_python 19 | 20 | 21 | -------------------------------------------------------------------------------- /robomatrix_client/resource/robomatrix_client: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WayneMao/RoboMatrix/71b077ee5904078f1cbf9008e3fb1256643ba832/robomatrix_client/resource/robomatrix_client -------------------------------------------------------------------------------- /robomatrix_client/robomatrix_client/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WayneMao/RoboMatrix/71b077ee5904078f1cbf9008e3fb1256643ba832/robomatrix_client/robomatrix_client/__init__.py -------------------------------------------------------------------------------- /robomatrix_client/robomatrix_client/agent_propmts.py: -------------------------------------------------------------------------------- 1 | RULE_PROMPT = 'In the skill library, represents using the Vision-Language Model, and represents using the shooting model. \ 2 | In the final output, `` must not be discarded.' -------------------------------------------------------------------------------- /robomatrix_client/robomatrix_client/agent_skills.py: -------------------------------------------------------------------------------- 1 | SKILLS = [': move to ', 2 | ': grasp ', 3 | ': position Over the ' 4 | ': release the ', 5 | ': Open the drawer', 6 | ': Close the drawer', 7 | ': shooting ', 8 | ] -------------------------------------------------------------------------------- /robomatrix_client/robomatrix_client/audio2text.py: -------------------------------------------------------------------------------- 1 | from pydub import AudioSegment 2 | import assemblyai as aai 3 | import noisereduce as nr 4 | import numpy as np 5 | from scipy.io import wavfile 6 | import os 7 | 8 | 9 | class AudioProcessor: 10 | def __init__(self, audio_file, api_key): 11 | self.audio_file = audio_file 12 | self.api_key = api_key 13 | 14 | # Get the directory of the input audio file 15 | self.audio_dir = os.path.dirname(audio_file) 16 | 17 | # Define paths for intermediate and processed files 18 | self.temp_file = os.path.join(self.audio_dir, "temp.wav") 19 | self.reduced_file = os.path.join(self.audio_dir, "reduced_noise.wav") 20 | self.enhanced_file = os.path.join(self.audio_dir, "enhanced_audio.wav") 21 | 22 | def process_audio(self): 23 | # Convert the input audio file to WAV format 24 | audio = AudioSegment.from_file(self.audio_file) 25 | audio.export(self.temp_file, format="wav") 26 | 27 | # Read the WAV file and apply noise reduction 28 | rate, data = wavfile.read(self.temp_file) 29 | reduced_noise = nr.reduce_noise(y=data, sr=rate) 30 | 31 | # Save the noise-reduced audio 32 | wavfile.write(self.reduced_file, rate, reduced_noise) 33 | 34 | # Increase the volume of the noise-reduced audio 35 | cleaned_audio = AudioSegment.from_file(self.reduced_file) 36 | louder_audio = cleaned_audio + 30 # Increase volume by 30dB 37 | louder_audio.export(self.enhanced_file, format="wav") 38 | 39 | print(f"Processed audio saved at: {self.enhanced_file}") 40 | 41 | def transcribe_audio(self): 42 | # Set AssemblyAI API key 43 | aai.settings.api_key = self.api_key 44 | transcriber = aai.Transcriber() 45 | 46 | # Transcribe the enhanced audio 47 | transcript = transcriber.transcribe(self.enhanced_file) 48 | return transcript.text 49 | 50 | 51 | # if __name__ == "__main__": 52 | # # Input audio file path (can be relative or absolute) 53 | # input_audio_file = "/home/jzian/ws/RM/test.wav" # Replace with your audio file path 54 | # api_key = "aed2e39f5b324388b68db696db3c9ad3" 55 | 56 | # processor = AudioProcessor(audio_file=input_audio_file, api_key=api_key) 57 | # processor.process_audio() 58 | 59 | # # Transcribe the audio and print the transcript 60 | # transcript_text = processor.transcribe_audio() 61 | # print(transcript_text) 62 | -------------------------------------------------------------------------------- /robomatrix_client/robomatrix_client/robomatrix_client.py: -------------------------------------------------------------------------------- 1 | import os 2 | import yaml 3 | import re 4 | import json 5 | import sys 6 | import threading 7 | import requests 8 | import ast 9 | import logging 10 | from rich.logging import RichHandler 11 | from time import time 12 | from typing import List, Dict, Any 13 | 14 | from PIL import Image 15 | 16 | import openai 17 | from langchain_core.prompts import ChatPromptTemplate 18 | from langchain_core.output_parsers import StrOutputParser 19 | from langchain_openai import ChatOpenAI, OpenAI 20 | from langchain_core.messages import HumanMessage, SystemMessage 21 | 22 | from gdino import GroundingDINOAPIWrapper, visualize 23 | 24 | from .agent_propmts import RULE_PROMPT 25 | from .agent_skills import SKILLS, META_SKILLS, TASK 26 | from .audio2text import AudioProcessor 27 | import ipdb # noqa 28 | 29 | # import ros2 30 | import rclpy 31 | from rclpy.node import Node 32 | from sensor_msgs.msg import Image 33 | from cv_bridge import CvBridge, CvBridgeError 34 | from robomatrix_interface.srv import Vla, Audio 35 | import requests 36 | import subprocess 37 | import cv2 38 | timeout_duration = 10 * 60 # 10 minutes in seconds 39 | 40 | # # 设置基本配置 41 | # logging.basicConfig( 42 | # level=logging.DEBUG, # 设置日志级别 43 | # format="%(asctime)s - %(levelname)s - %(message)s", # 日志输出格式 44 | # datefmt="%Y-%m-%d %H:%M:%S" 45 | # ) 46 | 47 | def check_network(): 48 | """Check if the network is accessible by trying to access Google and Baidu""" 49 | try: 50 | response = requests.get("https://www.google.com", timeout=5) 51 | return response.status_code == 200 52 | except requests.ConnectionError: 53 | try: 54 | response = requests.get("https://www.baidu.com", timeout=5) 55 | return response.status_code == 200 56 | except requests.ConnectionError: 57 | return False 58 | 59 | def run_proxy_command(): 60 | """Run the proxy command to set up the network proxy""" 61 | command = f"eval $(curl -s http://deploy.i.brainpp.cn/httpproxy)" 62 | try: 63 | subprocess.call(command, shell=True, check=True) 64 | print("Proxy command executed successfully.") 65 | except subprocess.CalledProcessError as e: 66 | print(f"Failed to execute proxy command: {e}") 67 | 68 | # 配置日志记录器 69 | logging.basicConfig( 70 | level="INFO", 71 | format="%(message)s", 72 | datefmt="%Y-%m-%d %H:%M:%S", 73 | handlers=[RichHandler()] 74 | ) 75 | 76 | logger = logging.getLogger("rich_logger") 77 | 78 | root_path= os.getcwd() 79 | cfg_path = os.path.join(root_path, "src", "config.yaml") 80 | 81 | with open(cfg_path, 'r') as f: 82 | OPENAI_CONFIG = yaml.load(f, Loader=yaml.FullLoader) 83 | 84 | Grounding_DINO_15_API = OPENAI_CONFIG['Grounding_DINO_1.5_API'] 85 | 86 | class RoboMatrix(Node): 87 | 88 | def __init__(self, api_type='ChatOpenAI'): 89 | super().__init__('rmm') 90 | # get image from ros2 img_topic 91 | self._sub_image = self.create_subscription(Image, "/camera/raw/image", self._image_cb, 1000) 92 | self._cv_bridge = CvBridge() 93 | self.cur_image = None 94 | 95 | # create client 96 | self.vla_client = self.create_client(Vla, 'vla_service') 97 | self.audio_client = self.create_client(Audio, 'audio_service') 98 | while not self.vla_client.wait_for_service(timeout_sec=1.0): 99 | self.get_logger().info('VLA Service not available, waiting again...') 100 | while not self.audio_client.wait_for_service(timeout_sec=1.0): 101 | self.get_logger().info('Audio Service not available, waiting again...') 102 | self.vla_req = Vla.Request() 103 | self.audio_req = Audio.Request() 104 | 105 | self.audio_path = "src/robomatrix_client/robomatrix_client/audios/task.wav" # Replace with your audio file path 106 | api_key = OPENAI_CONFIG['AssemblyAI_API'] 107 | 108 | self.processor = AudioProcessor(audio_file=self.audio_path, api_key=api_key) 109 | 110 | # Init config 111 | self.api_type = api_type 112 | if api_type == 'OpenAI': 113 | openai.api_key = os.environ["OPENAI_API_KEY"] 114 | openai.base_url = os.environ["OPENAI_API_BASE"] 115 | model = OpenAI(model_name="text-davinci-003", max_tokens=1024) 116 | elif api_type == 'ChatOpenAI': 117 | print('LLM: ChatOpenAI') 118 | os.environ["OPENAI_API_KEY"] = OPENAI_CONFIG['OPENAI_KEY'] 119 | os.environ["OPENAI_API_BASE"] = OPENAI_CONFIG['OPENAI_API_BASE'] 120 | model = ChatOpenAI( 121 | temperature=0, 122 | model_name='gpt-4o-mini', 123 | # model_name='gpt-4o', 124 | # model_name='o1-mini', 125 | max_tokens=1024, 126 | # request_timeout=30 127 | timeout=30 128 | ) 129 | elif api_type == 'STEP1': 130 | print('LLM: STEP1') 131 | os.environ["OPENAI_API_KEY"] = OPENAI_CONFIG['STEP1_API_KEY'] 132 | os.environ["OPENAI_API_BASE"] = OPENAI_CONFIG['STEP1_API_BASE'] 133 | model = ChatOpenAI(model_name="open_prod_model") 134 | 135 | # Task system template 136 | system_template = f"""You are an intelligent mobile robot, The skills you have are {SKILLS}. \ 137 | {RULE_PROMPT} \ 138 | The current environment is an office setting. \ 139 | Based on the tasks I provide, help me break down the tasks into multiple actionable steps. \ 140 | Let's think step by step. Here is an example. "Place the red cola can in the trash bin." Once you made a final result, output it in the following format:\n 141 | ``` 142 | "steps_1":": Move to the red cola can", 143 | "steps_2":": Grasp the red cola can", 144 | "steps_3":": Move to the trash bin", 145 | "steps_4":": Position the red cola can over the trash bin", 146 | "steps_5":": Release the red cola can" 147 | ```\n 148 | """ 149 | system_template_obj = "Extract the object in the sentence. Example: 'Grasp the red cola can', output the 'cola can' without any other word." 150 | 151 | print(system_template) 152 | prompt_template = ChatPromptTemplate.from_messages([ 153 | ('system', system_template), 154 | ('user', '{text}') 155 | ]) 156 | 157 | prompt_obj = ChatPromptTemplate.from_messages([ 158 | ('system', system_template_obj), 159 | ('user', '{text}') 160 | ]) 161 | # 3. Create parser 162 | parser = StrOutputParser() 163 | # 4. Create chain 164 | self.chain = prompt_template | model | parser # task planning 165 | self.chain_obj = prompt_obj | model | parser # split obj for gdino detection 166 | 167 | 168 | def _image_cb(self, msg: Image): 169 | 170 | self.cur_image = self._cv_bridge.imgmsg_to_cv2(msg, 'passthrough') 171 | 172 | def split_task(self, text): 173 | print("Start split task") 174 | action = self.chain.invoke({"text": text}) 175 | print(action) 176 | return action 177 | 178 | def get_gdino(self, text=''): 179 | gdino = GroundingDINOAPIWrapper(Grounding_DINO_15_API) 180 | if self.cur_image is None: 181 | print('Can not get img!') 182 | return None 183 | prompts = dict(image=self.cur_image, prompt=text) 184 | results = gdino.inference(prompts) 185 | 186 | return results 187 | 188 | def vis_gdino(self, img_path, results, save_path): 189 | # now visualize the results 190 | image_pil = Image.open(img_path) 191 | image_pil = visualize(image_pil, results) 192 | # dump the image to the disk 193 | if save_path is None: 194 | save_path = img_path.replace('.jpg', '_output.jpg') 195 | image_pil.save(save_path) 196 | print(f'The output image has been saved to {save_path}') 197 | 198 | 199 | 200 | def format_output(self, result, type='json'): 201 | # ipdb.set_trace() 202 | if type == 'json': 203 | try: 204 | if "`" in result: 205 | # result = result.replace("`", "") 206 | result = re.findall(r'```(.*?)```', result, re.DOTALL) 207 | json_str = "{" + result[0] + "}" 208 | result = json.loads(json_str) 209 | # ipdb.set_trace() 210 | return result 211 | except ValueError: 212 | # ipdb.set_trace() 213 | logging.error("This is an incorrect json format.") 214 | return {} 215 | 216 | def core_loop(self): 217 | 218 | while True: 219 | # ---- call sever ---- # 220 | self.audio_req.path = self.audio_path 221 | res = self.audio_client.call_async(self.audio_req) 222 | rclpy.spin_until_future_complete(self, res) 223 | # Transcribe the audio 224 | self.processor.process_audio() 225 | input_text = self.processor.transcribe_audio() 226 | print("Identify task: ", input_text) 227 | 228 | # print("User: ", end="") 229 | # input_text = input() 230 | # if input_text == '': 231 | # # input_text = "Place the red cola can into the white box." 232 | # input_text = "Put the red coke can in the open drawer and then close the drawer" 233 | # print(input_text) 234 | 235 | timer = threading.Timer(timeout_duration, exit_program) 236 | timer.start() 237 | 238 | task_steps = self.split_task(input_text) 239 | print("Task steps: \n", task_steps) 240 | task_steps_dict = self.format_output(task_steps) 241 | print("Task steps dict: \n", task_steps_dict) 242 | 243 | # import pdb;pdb.set_trace() 244 | _, obj_prompt = task_steps_dict['steps_1'].split(':') 245 | obj = self.chain_obj.invoke({"text": obj_prompt.strip()}) 246 | print(obj) 247 | info = self.get_gdino(obj) 248 | confidence = any([score > 0.5 for score in info['scores']]) 249 | if not confidence: 250 | print(f"Did not observe the {obj}! Task reset!") 251 | continue 252 | 253 | for i,item in enumerate(task_steps_dict.values()): 254 | task, prompt = item.split(':') 255 | print(f"Step {i+1}: {item}") 256 | print(f"task: {task}", f"prompt: {prompt.strip()}") 257 | 258 | self.vla_req.task = TASK[task] 259 | self.vla_req.msg = prompt.strip() 260 | 261 | # ---- call sever ---- # 262 | self.future = self.vla_client.call_async(self.vla_req) 263 | rclpy.spin_until_future_complete(self, self.future) 264 | # ----- # 265 | print("Processed input and reset timer.") 266 | 267 | 268 | 269 | # if no input for 10 minutes, exit the program 270 | def exit_program(): 271 | print(f"No new input for {timeout_duration} seconds. Exiting...") 272 | sys.exit(0) 273 | # os._exit(0) 274 | 275 | def main(): 276 | rclpy.init(args=None) 277 | # Check if the network is accessible 278 | if check_network(): 279 | print("Network is working") 280 | else: 281 | print("Network connection failed") 282 | run_proxy_command() 283 | 284 | logging.info("Init RoboMatrix...") 285 | robot_matrix = RoboMatrix() 286 | 287 | # setting up the timer 288 | timer = threading.Timer(timeout_duration, exit_program) 289 | 290 | # start timer 291 | timer.start() 292 | loop = threading.Thread(target=robot_matrix.core_loop) 293 | loop.start() 294 | 295 | if __name__=="__main__": 296 | main() -------------------------------------------------------------------------------- /robomatrix_client/setup.cfg: -------------------------------------------------------------------------------- 1 | [develop] 2 | script-dir=$base/lib/robomatrix_client 3 | [install] 4 | install-scripts=$base/lib/robomatrix_client 5 | -------------------------------------------------------------------------------- /robomatrix_client/setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup 2 | 3 | package_name = 'robomatrix_client' 4 | 5 | setup( 6 | name=package_name, 7 | version='0.0.0', 8 | packages=[package_name], 9 | data_files=[ 10 | ('share/ament_index/resource_index/packages', 11 | ['resource/' + package_name]), 12 | ('share/' + package_name, ['package.xml']), 13 | ], 14 | install_requires=['setuptools'], 15 | zip_safe=True, 16 | maintainer='jzian', 17 | maintainer_email='jiangzhou@megvii.com', 18 | description='TODO: Package description', 19 | license='TODO: License declaration', 20 | tests_require=['pytest'], 21 | entry_points={ 22 | 'console_scripts': [ 23 | "robomatrix_client = robomatrix_client.main:robomatrix_client" 24 | ], 25 | }, 26 | ) 27 | -------------------------------------------------------------------------------- /robomatrix_interface/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.8) 2 | project(robomatrix_interface) 3 | 4 | if(CMAKE_COMPILER_IS_GNUCXX OR CMAKE_CXX_COMPILER_ID MATCHES "Clang") 5 | add_compile_options(-Wall -Wextra -Wpedantic) 6 | endif() 7 | 8 | # cmake_policy(SET CMP0148 NEW) 9 | 10 | # find dependencies 11 | find_package(ament_cmake REQUIRED) 12 | # uncomment the following section in order to fill in 13 | # further dependencies manually. 14 | # find_package( REQUIRED) 15 | 16 | if(BUILD_TESTING) 17 | find_package(ament_lint_auto REQUIRED) 18 | # the following line skips the linter which checks for copyrights 19 | # comment the line when a copyright and license is added to all source files 20 | set(ament_cmake_copyright_FOUND TRUE) 21 | # the following line skips cpplint (only works in a git repo) 22 | # comment the line when this package is in a git repo and when 23 | # a copyright and license is added to all source files 24 | set(ament_cmake_cpplint_FOUND TRUE) 25 | ament_lint_auto_find_test_dependencies() 26 | endif() 27 | 28 | find_package(rosidl_default_generators REQUIRED) 29 | 30 | ################################### rm_srv 31 | 32 | set(MODULE_NAME robomatrix_srv) 33 | 34 | file(GLOB ${MODULE_NAME}_FILES "${CMAKE_CURRENT_SOURCE_DIR}/srv/*.srv") 35 | 36 | set(${MODULE_NAME}_FILE_NAMES) 37 | 38 | foreach(file ${${MODULE_NAME}_FILES}) 39 | get_filename_component(file_name ${file} NAME) 40 | list(APPEND ${MODULE_NAME}_FILE_NAMES "srv/${file_name}") 41 | endforeach() 42 | 43 | ################################ generate the interface 44 | ament_export_dependencies(std_msgs) 45 | ament_export_dependencies(rosidl_default_generators) 46 | rosidl_generate_interfaces(${PROJECT_NAME} 47 | ${robomatrix_srv_FILE_NAMES} 48 | # DEPENDENCIES geometry_msgs 49 | # DEPENDENCIES std_msgs 50 | ) 51 | ament_package() 52 | -------------------------------------------------------------------------------- /robomatrix_interface/package.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | robomatrix_interface 5 | 1.0.0 6 | The Ros2 Interfaces for RM 7 | blackbird 8 | BBLAB 9 | 10 | ament_cmake 11 | ament_lint_auto 12 | ament_lint_common 13 | 14 | 15 | rosidl_default_generators 16 | action_msgs 17 | 18 | 19 | 20 | 21 | rosidl_default_runtime 22 | rosidl_interface_packages 23 | 24 | rosidl_default_generators 25 | 26 | 27 | 28 | ament_cmake 29 | 30 | 31 | -------------------------------------------------------------------------------- /robomatrix_interface/srv/Audio.srv: -------------------------------------------------------------------------------- 1 | string path 2 | --- 3 | bool success 4 | -------------------------------------------------------------------------------- /robomatrix_interface/srv/Vla.srv: -------------------------------------------------------------------------------- 1 | int32 task 2 | string msg 3 | --- 4 | bool success 5 | string msg -------------------------------------------------------------------------------- /robomatrix_server/package.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | robomatrix_server 5 | 0.0.0 6 | TODO: Package description 7 | jzian 8 | TODO: License declaration 9 | 10 | example_interfaces 11 | std_msgs 12 | rclpy 13 | robomatrix_interface 14 | pysoem 15 | py_trees 16 | 17 | rosidl_default_generators 18 | rosidl_default_runtime 19 | 20 | 21 | ament_copyright 22 | ament_flake8 23 | ament_pep257 24 | python3-pytest 25 | 26 | 27 | rosidl_interface_packages 28 | 29 | 30 | ament_python 31 | 32 | 33 | -------------------------------------------------------------------------------- /robomatrix_server/resource/robomatrix_server: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WayneMao/RoboMatrix/71b077ee5904078f1cbf9008e3fb1256643ba832/robomatrix_server/resource/robomatrix_server -------------------------------------------------------------------------------- /robomatrix_server/robomatrix_server/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WayneMao/RoboMatrix/71b077ee5904078f1cbf9008e3fb1256643ba832/robomatrix_server/robomatrix_server/__init__.py -------------------------------------------------------------------------------- /robomatrix_server/robomatrix_server/resource/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WayneMao/RoboMatrix/71b077ee5904078f1cbf9008e3fb1256643ba832/robomatrix_server/robomatrix_server/resource/__init__.py -------------------------------------------------------------------------------- /robomatrix_server/robomatrix_server/resource/funcs.py: -------------------------------------------------------------------------------- 1 | import os 2 | import re 3 | import time 4 | import cv2 5 | import numpy as np 6 | import matplotlib.pyplot as plt 7 | from .utils import HOME_PATH, load_json, save_json, load_yaml, save_yaml, log_info, log_warn, log_error, extract_last_number, save_video_v2 8 | from moviepy.editor import VideoFileClip 9 | from concurrent.futures import ThreadPoolExecutor, as_completed 10 | import uuid 11 | from robomaster import robot 12 | from collections import deque 13 | from threading import Thread 14 | import requests 15 | from sensor_msgs.msg import Image 16 | from cv_bridge import CvBridge 17 | 18 | class BaseTokenizeDataset(): 19 | 20 | def __init__(self, dataset_directory) -> None: 21 | self._dataset_dir = dataset_directory 22 | 23 | def check_dataset(self): 24 | if not os.path.exists(self._dataset_dir): return False 25 | 26 | yaml_file = "dataset_information.yaml" 27 | if yaml_file not in os.listdir(self._dataset_dir): return False 28 | 29 | data = load_yaml(os.path.join(self._dataset_dir, yaml_file)) 30 | 31 | self._task_name = data["dataset_name"] 32 | self._episode_info = data["episode_info"] 33 | self._episode_names = sorted(list(data["episode_info"].keys()), key=lambda name: extract_last_number(name, "")) 34 | return True 35 | 36 | def check_distribution(self): 37 | if not os.path.exists(self._dataset_dir): return False 38 | 39 | json_file = "data_distribution.json" 40 | if json_file not in os.listdir(self._dataset_dir): return False 41 | 42 | data = load_json(os.path.join(self._dataset_dir, json_file)) 43 | 44 | self._data_distribution = data 45 | return True 46 | 47 | 48 | class Decoder(): 49 | 50 | def __init__(self, name, data_distribution, vocab_size, log=False) -> None: 51 | self._name = name 52 | self._data_distribution = data_distribution 53 | self._vocab_size = vocab_size 54 | 55 | self._min_value = data_distribution[0] 56 | self._max_value = data_distribution[1] 57 | 58 | self._log = log 59 | 60 | def decode(self, token: str): 61 | if not 0 <= int(token) <= 255: 62 | if self._log: print(f"[Decoder {self._name}] Wrong token: {token}") 63 | return None 64 | tile = np.linspace(self._min_value, self._max_value, self._vocab_size)[:, None] 65 | value = tile[int(token)][0] 66 | if self._log: print(f"[Decoder {self._name}] token {token} -> value {value}") 67 | return value 68 | 69 | 70 | class BaseRobotInference(): 71 | 72 | def __init__(self, 73 | node, 74 | # sn="159CK9W0060AHS", 75 | sn="3JKCJC400301RW", 76 | dataset_dir=os.path.join(HOME_PATH, "RoboMatrixDatasetsDIY"), 77 | task_name="", 78 | url="http://localhost:7893/process_frame", 79 | arm_init_pose=(180, -70), 80 | ) -> None: 81 | 82 | self.node = node 83 | # self.publisher_ = self.node.create_publisher(Image, 'image_topic', 10) 84 | self.publisher_ = self.node.create_publisher(Image, "/camera/raw/image", 3) # image publisher 85 | self.bridge = CvBridge() 86 | 87 | self._url = url 88 | log_info(f"[BaseRobotInference] URL: {self._url}") 89 | 90 | self._robot = robot.Robot() 91 | self._robot_sn = sn 92 | log_info(f"[BaseRobotInference] Robot SN: {self._robot_sn}") 93 | self._robot_ok = False 94 | 95 | self.arm_init_pose = arm_init_pose 96 | log_info(f"[BaseRobotInference] Arm init pose: {self.arm_init_pose}") 97 | 98 | distribution_file = os.path.join(dataset_dir, task_name, "data_distribution.json") 99 | self._distribution = load_json(distribution_file) 100 | log_info(f"[BaseRobotInference] Distribution file: {distribution_file}") 101 | 102 | self._range = { 103 | "arm": 104 | { 105 | "x": self._distribution["arm"][0], 106 | "y": self._distribution["arm"][1], 107 | }, 108 | "chassis": 109 | { 110 | "d_x": self._distribution["chassis"][0], 111 | "d_y": self._distribution["chassis"][1], 112 | "d_yaw": self._distribution["chassis"][2], 113 | } 114 | } 115 | log_info(f"[BaseRobotInference] Distribution info:") 116 | for s in self._range["chassis"]: 117 | s_min = self._range["chassis"][s][0] 118 | s_max = self._range["chassis"][s][1] 119 | log_info(f"Chassis {s}: {s_min} ~ {s_max}") 120 | for s in self._range["arm"]: 121 | s_min = self._range["arm"][s][0] 122 | s_max = self._range["arm"][s][1] 123 | log_info(f"Arm {s}: {s_min} ~ {s_max}") 124 | 125 | self._arm_position_x_decoder = Decoder("arm_x", self._range["arm"]["x"], 256) 126 | self._arm_position_y_decoder = Decoder("arm_y", self._range["arm"]["y"], 256) 127 | self._chassis_position_x_decoder = Decoder("chassis_d_x", self._range["chassis"]["d_x"], 256) 128 | self._chassis_position_y_decoder = Decoder("chassis_d_y", self._range["chassis"]["d_y"], 256) 129 | self._chassis_position_yaw_decoder = Decoder("chassis_d_yaw", self._range["chassis"]["d_yaw"], 256) 130 | 131 | self._rate = 30 132 | self._log = False 133 | 134 | self._images = [] 135 | 136 | self.init_robot() 137 | 138 | def init_robot(self): 139 | while not self._robot.initialize(conn_type="sta", sn=self._robot_sn): 140 | log_error("[BaseRobotInference] Initialize robot fail") 141 | 142 | freq = 50 143 | 144 | # 末端 145 | self._arm = self._robot.robotic_arm 146 | 147 | self._arm.sub_position(freq, self._update_arm_position) 148 | 149 | self._arm_position_deque = deque() 150 | 151 | self.arm_moveto(self.arm_init_pose) 152 | 153 | # 关节 154 | self._servo = self._robot.servo 155 | 156 | self._servo.sub_servo_info(freq, self._update_servo) 157 | 158 | self._servo_value_deque = deque() 159 | 160 | # 夹爪 161 | self._gripper = self._robot.gripper 162 | 163 | self._gripper.sub_status(freq, self._update_gripper) 164 | 165 | self._gripper_status_deque = deque() 166 | 167 | self._gripper_status = None 168 | self._gripper_command = "None" 169 | self._gripper_power = None 170 | 171 | self.gripper("release", 70) 172 | 173 | # 相机 174 | self._vision = self._robot.camera 175 | 176 | self._vision.start_video_stream(display=False) 177 | 178 | self._image_deque = deque() 179 | self._current_image_data = None 180 | 181 | self._cam_thread = Thread(target=self._update_image) 182 | self._cam_thread.start() 183 | 184 | # 底盘 185 | self._chassis = self._robot.chassis 186 | # 姿态 187 | self._chassis.sub_attitude(freq, self._update_chassis_attitude) 188 | self._chassis_attitute_deque = deque() 189 | # 位置 190 | self._chassis.sub_position(0, freq, self._update_chassis_position) 191 | self._chassis_position_deque = deque() 192 | # IMU 193 | self._chassis.sub_imu(freq, self._update_chassis_imu) 194 | self._chassis_imu_deque = deque() 195 | # 状态 196 | self._chassis.sub_status(freq, self._update_chassis_status) 197 | self._chassis_status_deque = deque() 198 | # 电调 199 | self._chassis.sub_esc(freq, self._update_chassis_esc) 200 | self._chassis_esc_data_deque = deque() 201 | 202 | self._robot_ok = True 203 | self._robot.led.set_led(comp="all", r=0, g=255, b=0, effect='breath', freq=1) 204 | log_info("[BaseRobotInference] Initialize robot success") 205 | return self._robot_ok 206 | 207 | def start_robot(self): 208 | if not self._robot_ok: return False 209 | 210 | self._core_thread = Thread(target=self._core_loop) 211 | self._core_thread.start() 212 | 213 | def close_robot(self): 214 | if not self._robot_ok: return 215 | self._robot_ok = False 216 | 217 | self._arm.unsub_position() 218 | self._gripper.unsub_status() 219 | self._servo.unsub_servo_info() 220 | 221 | self._vision.stop_video_stream() 222 | 223 | self._chassis.unsub_attitude() 224 | self._chassis.unsub_position() 225 | self._chassis.unsub_imu() 226 | self._chassis.unsub_status() 227 | self._chassis.unsub_esc() 228 | 229 | self._robot.close() 230 | 231 | def _update_arm_position(self, position_data): 232 | if len(self._arm_position_deque) >= 100: self._arm_position_deque.popleft() 233 | 234 | pos_x, pos_y = position_data 235 | time_stamp = time.time() 236 | data = (time_stamp, [pos_x, pos_y]) 237 | 238 | self._arm_x, self._arm_y = pos_x, pos_y 239 | 240 | self._arm_position_deque.append(data) 241 | 242 | if self._log: print("End position: x: {0}, y: {1}".format(pos_x, pos_y)) 243 | 244 | def _update_servo(self, servo_data): 245 | if len(self._servo_value_deque) >= 100: self._servo_value_deque.popleft() 246 | 247 | time_stamp = time.time() 248 | status, speeds, angles = servo_data 249 | data = (time_stamp, angles[:2], speeds[:2]) 250 | 251 | self._servo_value_deque.append(data) 252 | 253 | if self._log: print("Servo: status: {}, speed: {}, angle: {}".format(status, speeds, angles)) 254 | 255 | def _update_gripper(self, status_info): 256 | if len(self._gripper_status_deque) >= 100: self._gripper_status_deque.popleft() 257 | 258 | time_stamp = time.time() 259 | self._gripper_status = status_info 260 | data = (time_stamp, self._gripper_status, self._gripper_power, self._gripper_command) 261 | 262 | self._gripper_status_deque.append(data) 263 | 264 | if self._log: print("Gripper: {}".format(self._gripper_status)) 265 | 266 | def _update_image(self): 267 | while True: 268 | try: 269 | if len(self._image_deque) >= 100: self._image_deque.popleft() 270 | time_stamp = time.time() 271 | 272 | image = self._vision.read_cv2_image(timeout=0.1, strategy='newest') 273 | 274 | 275 | data = (time_stamp, image) 276 | # self._images.append(image) 277 | self._images.append(cv2.cvtColor(image, cv2.COLOR_BGR2RGB)) 278 | 279 | self._image_deque.append(data) 280 | self._current_image_data = data 281 | except: 282 | continue 283 | ros_image = self.bridge.cv2_to_imgmsg(image, encoding='bgr8') 284 | ros_image.header.stamp = self.node.get_clock().now().to_msg() 285 | # print(ros_image.header.stamp) 286 | ros_image.header.frame_id = "camera_link_optical_frame" 287 | self.publisher_.publish(ros_image) 288 | 289 | def _update_chassis_attitude(self, attitude_data): 290 | if len(self._chassis_attitute_deque) >= 100: self._chassis_attitute_deque.popleft() 291 | 292 | yaw, pitch, roll = attitude_data 293 | 294 | time_stamp = time.time() 295 | attitute = [yaw, pitch, roll] 296 | data = (time_stamp, attitute) 297 | 298 | self._chassis_attitute_deque.append(data) 299 | 300 | if self._log: print("chassis attitude: yaw:{0}, pitch:{1}, roll:{2} ".format(yaw, pitch, roll)) 301 | 302 | def _update_chassis_position(self, position_data): 303 | if len(self._chassis_position_deque) >= 100: self._chassis_position_deque.popleft() 304 | 305 | x, y, z = position_data 306 | time_stamp = time.time() 307 | position = [x, y] 308 | data = (time_stamp, position) 309 | 310 | self._chassis_position_deque.append(data) 311 | 312 | if self._log: print("chassis position: x:{0}, y:{1}, z:{2}".format(x, y, z)) 313 | 314 | def _update_chassis_imu(self, imu_data): 315 | if len(self._chassis_imu_deque) >= 100: self._chassis_imu_deque.popleft() 316 | 317 | acc_x, acc_y, acc_z, gyro_x, gyro_y, gyro_z = imu_data 318 | 319 | time_stamp = time.time() 320 | linear_acceleration = [acc_x, acc_y, acc_z] 321 | angular_velocity = [gyro_x, gyro_y, gyro_z] 322 | data = (time_stamp, linear_acceleration, angular_velocity) 323 | 324 | self._chassis_imu_deque.append(data) 325 | 326 | if self._log: print("chassis imu: acc_x:{0}, acc_y:{1}, acc_z:{2}, gyro_x:{3}, gyro_y:{4}, gyro_z:{5}".format(acc_x, acc_y, acc_z, gyro_x, gyro_y, gyro_z)) 327 | 328 | def _update_chassis_status(self, status_info): 329 | static_flag, up_hill, down_hill, on_slope, pick_up, slip_flag, impact_x, impact_y, impact_z, roll_over, hill_static = status_info 330 | 331 | if self._log: print("chassis status: static_flag:{0}, up_hill:{1}, down_hill:{2}, on_slope:{3}, " 332 | "pick_up:{4}, slip_flag:{5}, impact_x:{6}, impact_y:{7}, impact_z:{8}, roll_over:{9}, " 333 | "hill_static:{10}".format(static_flag, up_hill, down_hill, on_slope, pick_up, 334 | slip_flag, impact_x, impact_y, impact_z, roll_over, hill_static)) 335 | 336 | def _update_chassis_esc(self, esc_data): 337 | if len(self._chassis_esc_data_deque) >= 100: self._chassis_esc_data_deque.popleft() 338 | 339 | speed, angle, timestamp, state = esc_data 340 | 341 | time_stamp = time.time() 342 | angles = list(angle) 343 | speeds = list(speed) 344 | data = (time_stamp, angles, speeds) 345 | 346 | self._chassis_esc_data_deque.append(data) 347 | 348 | if self._log: print("chassis esc: speed:{0}, angle:{1}, timestamp:{2}, state:{3}".format(speed, angle, timestamp, state)) 349 | 350 | def arm_moveto(self, position): 351 | x, y = position 352 | self._arm.moveto(x, y).wait_for_completed(timeout=1) 353 | 354 | def gripper(self, action, power=50): 355 | if power not in [power for power in range(1,100)]: return False 356 | 357 | self._gripper_command = action 358 | self._gripper_power = power 359 | if action == "grab": self._gripper.close(power) 360 | elif action == "release": self._gripper.open(power) 361 | elif action == "pause": self._gripper.pause() 362 | else: return 363 | 364 | def chassis_move(self, x_dis, y_dis, z_angle, xy_vel=0.5, z_w=30): 365 | self._chassis.move(x_dis, y_dis, z_angle, xy_vel, z_w).wait_for_completed(timeout=None) 366 | 367 | def get_image(self): 368 | if not self._current_image_data: return False 369 | _, image = self._current_image_data 370 | return image 371 | 372 | def get_llava_output(self, image, prompt, temp=1.0): 373 | file_path = 'ep.jpg' 374 | cv2.imwrite(file_path, image) 375 | with open(file_path, 'rb') as image_file: 376 | files = {'image': (file_path, image_file, 'image/jpg')} 377 | 378 | text = prompt 379 | data={"text": text, 380 | "temperature": temp} 381 | 382 | response = requests.post(self._url, files=files, data=data) 383 | time.sleep(0.3) 384 | return response.json()['response'] 385 | 386 | def _core_loop(self): 387 | pass 388 | 389 | 390 | if __name__ == "__main__": 391 | DATASET_DIR = os.path.join(HOME_PATH, "RoboMatrixDatasets", "move_to_object") 392 | 393 | # transfer_video_v2(os.path.join(DATASET_DIR, "videos")) 394 | -------------------------------------------------------------------------------- /robomatrix_server/robomatrix_server/resource/inference_multistage.py: -------------------------------------------------------------------------------- 1 | import os 2 | import time 3 | 4 | from .utils import HOME_PATH, log_info, log_warn, save_video_v2 5 | from .funcs import BaseRobotInference 6 | 7 | LED = { 8 | 0: (255, 0, 255), 9 | 1: (0, 255, 255), 10 | } 11 | 12 | 13 | class RobotInference(BaseRobotInference): 14 | 15 | def __init__(self, node, dataset_dir, task_name, url="http://localhost:7893/process_frame", debug=True) -> None: 16 | super().__init__(node, task_name=task_name, dataset_dir=dataset_dir, url=url) 17 | self.debug = debug 18 | 19 | def exec_vla_task(self, task, prompt): 20 | frame_count, fail_count= 0, 0 21 | try: 22 | log_warn(f"Press ENTER to start.") 23 | input() 24 | 25 | current_stage = 1 26 | total = 0 27 | temp_init = 1.0 28 | temp_high = 1.1 29 | self._robot.led.set_led(comp="all", r=LED[current_stage%2][0], g=LED[current_stage%2][1], b=LED[current_stage%2][2], effect='on', freq=1) 30 | while self._robot_ok: 31 | image = self.get_image() 32 | 33 | if not image.any(): 34 | fail_count += 1 35 | continue 36 | frame_count += 1 37 | print(f"Get a frame {frame_count}") 38 | 39 | print(f"Prompt: {prompt}") 40 | 41 | # temperature 42 | temp = temp_init if total < 5 else temp_high 43 | 44 | token = self.get_llava_output(image, prompt, temp=temp) 45 | print(f"LLaVa response: {token}") 46 | 47 | # decode 48 | tokens = token.split() 49 | flag = False 50 | if len(tokens) != 7: continue 51 | for token in tokens: 52 | if not token.isdigit(): 53 | flag = True 54 | break 55 | if flag: continue 56 | 57 | # stop 58 | stage_end = tokens[0] 59 | if stage_end == "1" \ 60 | or ("release" in prompt.lower() and total > 5) \ 61 | or (("grasp" in prompt.lower() or "position" in prompt.lower()) and total > 10) \ 62 | or ("move" in prompt.lower() and total > 15): 63 | 64 | log_info(f"Current stage {current_stage} is end.") 65 | current_stage += 1 66 | total = 0 67 | self._robot.led.set_led(comp="all", r=LED[current_stage%2][0], g=LED[current_stage%2][1], b=LED[current_stage%2][2], effect='on', freq=1) 68 | return True 69 | 70 | # chassis 71 | scale = 10 72 | 73 | d_x = self._chassis_position_x_decoder.decode(tokens[1]) 74 | if abs(d_x) < 1: d_x = 0 75 | elif abs(d_x) < 10: d_x = d_x * scale / 1000 76 | else: d_x /= 1000 77 | 78 | d_y = self._chassis_position_y_decoder.decode(tokens[2]) 79 | if abs(d_y) < 1: d_y = 0 80 | elif abs(d_y) < 10: d_y = d_y * scale / 1000 81 | else: d_y /= 1000 82 | 83 | d_yaw = self._chassis_position_yaw_decoder.decode(tokens[3]) 84 | if abs(d_yaw) < 1: d_yaw = 0 85 | d_yaw = - d_yaw 86 | 87 | # arm 88 | x = self._arm_position_x_decoder.decode(tokens[4]) 89 | y = self._arm_position_y_decoder.decode(tokens[5]) 90 | 91 | arm_d_x, arm_d_y = x - self._arm_x, y - self._arm_y 92 | if "grasp" in prompt.lower() or "position" in prompt.lower(): 93 | if 2 < arm_d_y < 5: y = self._arm_y + 5 94 | elif 5 <= arm_d_y < 8: y = self._arm_y + 8 95 | elif arm_d_y >= 8: y = self._arm_y + 10 96 | 97 | # gripper 98 | if tokens[6] == "0": g = "release" 99 | elif tokens[6] == "1": g = "grab" 100 | else: g = "pause" 101 | 102 | # info 103 | log_info(f"Chassis: {round(d_x,2)}, {round(d_y,2)}, {round(d_yaw,2)}") 104 | # print(f"Arm: {x}, {y}") 105 | log_info(f"Arm: {round(arm_d_x,2)}, {round(arm_d_y,2)}") 106 | log_info(f"Gripper: {g}") 107 | 108 | log_warn(f"Wait: {total}, temp: {temp}") 109 | 110 | # action 111 | self.chassis_move(0, 0, float(d_yaw)) 112 | self.chassis_move(float(d_x), float(d_y), 0) 113 | self.arm_moveto((int(x), int(y))) 114 | self.gripper(g, 20) 115 | 116 | if ("move" in prompt.lower() and d_x == 0 and d_y == 0 and d_yaw == 0) \ 117 | or ("release" in prompt.lower() and g == "release") \ 118 | or ("grasp" in prompt.lower() and abs(arm_d_x) < 2 and abs(arm_d_y) < 2): 119 | last = True 120 | else: 121 | last = False 122 | total = 0 123 | if last: 124 | if ("move" in prompt.lower() and d_x == 0 and d_y == 0 and d_yaw == 0) \ 125 | or ("release" in prompt.lower() and g == "release") \ 126 | or (("grasp" in prompt.lower() or "position" in prompt.lower()) and abs(arm_d_x) < 2 and abs(arm_d_y) < 2): 127 | total += 1 128 | except KeyboardInterrupt: pass 129 | 130 | def exec_get_audio(self, file_path): 131 | print("Recording... ") 132 | res = self._vision.record_audio(save_file=file_path, seconds=5, sample_rate=16000) 133 | print("audio file save in: ", file_path) 134 | return res 135 | 136 | -------------------------------------------------------------------------------- /robomatrix_server/robomatrix_server/resource/utils.py: -------------------------------------------------------------------------------- 1 | import os 2 | import json 3 | import yaml 4 | import cv2 5 | import re 6 | from moviepy.editor import ImageSequenceClip 7 | import shutil 8 | 9 | 10 | HOME_PATH = os.path.expanduser("~") 11 | 12 | RED = '\033[31m' 13 | GREEN = '\033[32m' 14 | YELLOW = '\033[33m' 15 | RESET = '\033[0m' 16 | 17 | def log_info(log): 18 | print(f"{GREEN}{log}{RESET}") 19 | 20 | def log_warn(log): 21 | print(f"{YELLOW}{log}{RESET}") 22 | 23 | def log_error(log): 24 | print(f"{RED}{log}{RESET}") 25 | 26 | def load_json(file_path): 27 | with open(file_path, 'r') as file: 28 | data = json.load(file) 29 | return data 30 | 31 | def save_json(data, save_directory: str, file_name: str, log=True): 32 | if not os.path.exists(save_directory): 33 | os.mkdir(save_directory) 34 | if log: log_info(f"[Save Json] Create directory: {save_directory}") 35 | if not file_name.endswith('.json'): 36 | file_name += '.json' 37 | if log: log_warn(f"[Save Json] Change file name to: {file_name}") 38 | json_path = os.path.join(save_directory, file_name) 39 | with open(json_path, 'w', encoding='utf-8') as f: 40 | json.dump(data, f, ensure_ascii=False, indent=4) 41 | if log: log_info(f"[Save Json] Save json file to: {json_path}") 42 | 43 | def load_yaml(file_path): 44 | with open(file_path, 'r') as file: 45 | data = yaml.safe_load(file) 46 | return data 47 | 48 | def save_yaml(data, save_directory: str, file_name: str, log=True): 49 | if not os.path.exists(save_directory): 50 | os.mkdir(save_directory) 51 | if log: log_warn(f"[Save Yaml] Create directory: {save_directory}") 52 | if not file_name.endswith('.yaml'): 53 | file_name += '.yaml' 54 | if log: log_warn(f"[Save Yaml] Change file name to: {file_name}") 55 | yaml_path = os.path.join(save_directory, file_name) 56 | with open(yaml_path, 'w') as file: 57 | yaml.dump(data, file, default_flow_style=False, allow_unicode=True) 58 | if log: log_info(f"[Save Yaml] Save yaml file to: {yaml_path}") 59 | 60 | # 放弃 61 | def save_video(images: list, fps: int, save_path: str): 62 | frame_count = len(images) 63 | if frame_count == 0: 64 | log_error(f"[Save video] Image list is empty") 65 | return None 66 | log_warn(f"[Save video] Saving video to {save_path}, don't close.") 67 | height, width, _ = images[0].shape 68 | out = cv2.VideoWriter(save_path, cv2.VideoWriter_fourcc(*'mp4v'), fps, (width, height)) 69 | for t in range(frame_count): 70 | img = images[t] 71 | out.write(img) 72 | out.release() 73 | log_info(f"[Save video] Save video to {save_path}") 74 | 75 | # 可用 76 | def save_video_v2(images: list, fps: int, save_path: str): 77 | frame_count = len(images) 78 | if frame_count == 0: 79 | log_error(f"[Save video] Image list is empty") 80 | return None 81 | log_warn(f"[Save video] Saving video to {save_path}, don't close.") 82 | clip = ImageSequenceClip(images, fps=fps) 83 | clip.write_videofile(save_path, codec='libx264') 84 | log_info(f"[Save video] Save video to {save_path}") 85 | 86 | def extract_last_number(string, suffix): 87 | ''' 88 | 用于sort的key 89 | ''' 90 | # 使用正则表达式提取最后的数字 91 | match = re.search(r'_(\d+)' + re.escape(suffix) + r'$', string) 92 | if match: return int(match.group(1)) 93 | return None 94 | 95 | def find_ranges(nums): 96 | ''' 97 | 从有序正整数列表提取区间 98 | ''' 99 | if not nums: return [] 100 | 101 | ranges_str, ranges_list = [], [] 102 | start = nums[0] 103 | 104 | for i in range(1, len(nums)): 105 | # 检查当前数字与前一个数字的差是否为 1 106 | if nums[i] != nums[i - 1] + 1: 107 | # 结束当前区间 108 | if start == nums[i - 1]: 109 | ranges_str.append(f"{start}") 110 | ranges_list.append([start]) 111 | else: 112 | ranges_str.append(f"{start}~{nums[i - 1]}") 113 | ranges_list.append([start, nums[i - 1]]) 114 | start = nums[i] 115 | 116 | # 添加最后一个区间 117 | if start == nums[-1]: 118 | ranges_str.append(f"{start}") 119 | ranges_list.append([start]) 120 | else: 121 | ranges_str.append(f"{start}~{nums[-1]}") 122 | ranges_list.append([start, nums[-1]]) 123 | 124 | return ranges_str, ranges_list 125 | 126 | def compare_intervals(intervals_A, intervals_B): 127 | ''' 128 | 比较B区间是否被A区间包含 129 | ''' 130 | def normalize_intervals(intervals): 131 | normalized = [] 132 | for interval in intervals: 133 | if len(interval) == 1: # 处理只有一个端点的情况 134 | normalized.append([interval[0], interval[0]]) 135 | else: 136 | normalized.append(interval) 137 | return normalized 138 | 139 | # 先规范化 B 区间 140 | intervals_B = normalize_intervals(intervals_B) 141 | 142 | for interval_B in intervals_B: 143 | start_B, end_B = interval_B 144 | covered = False 145 | 146 | # 遍历 A 区间 147 | for start_A, end_A in intervals_A: 148 | # 检查 B 区间是否被 A 区间包含 149 | if start_A <= start_B and end_B <= end_A: 150 | covered = True 151 | break # 找到一个包含的 A 区间,停止检查 152 | 153 | if not covered: 154 | return False # 如果任何 B 区间不被 A 区间包含,返回 False 155 | 156 | return True # 所有 B 区间均被 A 区间包含 157 | 158 | def analyse_folder(folder_path, log=True): 159 | if not os.path.exists(folder_path): 160 | log_error(f"[Analyse Folder] Cannot find: {folder_path}") 161 | return None 162 | 163 | information = {} 164 | 165 | for file in os.listdir(folder_path): 166 | name, suffix = os.path.splitext(file) # 分割名称和后缀 167 | if suffix not in information: information[suffix] = {} 168 | match = re.match(r'^(.*)_(\d+)$', name) # 分割任务描述和数字 169 | if match: 170 | task_description = match.group(1) 171 | episode_index = match.group(2) 172 | if task_description not in information[suffix]: information[suffix][task_description] = [] 173 | information[suffix][task_description].append(int(episode_index)) 174 | else: log_warn(f"[Analyse Folder] Cannot match: {file}") 175 | 176 | for suffix in sorted(information.keys()): 177 | if log: log_info(f"[Analyse Folder] File type: {suffix}") 178 | group = information[suffix] 179 | for task_description in sorted(group.keys()): 180 | # log_info(f"Task description: {task_description}") 181 | if log: log_info(f"{task_description}") 182 | group[task_description].sort() 183 | ranges_str, ranges_list = find_ranges(group[task_description]) 184 | # log_info(f"Episode index range: {''.join(ranges_str)}, count: {len(group[task_description])}") 185 | if log: log_info(f"Range: {' '.join(ranges_str)}, count: {len(group[task_description])}") 186 | group[task_description] = ranges_list 187 | 188 | return information 189 | 190 | def compare_information(videos_information, annotations_information): 191 | if ".mp4" not in videos_information: 192 | log_error(f"[Compare] No MP4 in videos resource.") 193 | return False 194 | if ".json" not in annotations_information: 195 | log_error(f"[Compare] No JSON in annotations resource.") 196 | return False 197 | status = True 198 | for task_description in sorted(videos_information[".mp4"]): 199 | if task_description not in annotations_information[".json"]: 200 | log_error(f"[Compare] Task not in annotations: {task_description}") 201 | videos_index_ranges = videos_information[".mp4"][task_description] 202 | annotations_index_ranges = annotations_information[".json"][task_description] 203 | if videos_index_ranges == annotations_index_ranges: 204 | log_info(f"[Compare] OK: {task_description}") 205 | else: 206 | status = False 207 | log_error(f"[Compare] Wrong: {task_description}") 208 | return status 209 | 210 | def check_dataset(videos_resource=os.path.join(HOME_PATH, "RoboMatrixDatasetsALL", "videos"), 211 | annotations_resource=os.path.join(HOME_PATH, "RoboMatrixDatasetsALL", "annotations")): 212 | videos_info = analyse_folder(videos_resource) 213 | save_json(videos_info, os.path.join(HOME_PATH, "RoboMatrixDatasetsALL"), "videos_information.json") 214 | annos_info = analyse_folder(annotations_resource) 215 | save_json(annos_info, os.path.join(HOME_PATH, "RoboMatrixDatasetsALL"), "annotations_information.json") 216 | return compare_information(videos_info, annos_info) 217 | 218 | def rename_dataset(videos_folder, annos_folder, *chosen_group): 219 | analyse_folder(videos_folder) 220 | analyse_folder(annos_folder) 221 | 222 | old_names, new_names = [], [] 223 | for old_name, new_name in chosen_group: 224 | old_names.append(old_name) 225 | new_names.append(new_name) 226 | log_info(f"[Rename] Your change: {old_name} -> {new_name}") 227 | 228 | group = set() 229 | file_names = {} 230 | for file_name in os.listdir(videos_folder): 231 | name, suffix = os.path.splitext(file_name) 232 | if suffix == ".mp4": 233 | match = re.match(r'^(.*)_(\d+)$', name) 234 | if match: 235 | task = match.group(1) 236 | index = match.group(2) 237 | if task not in group: 238 | file_names[task] = [] 239 | group.add(task) 240 | file_names[task].append(int(index)) 241 | else: log_warn(f"[Rename] {file_name} cannot match") 242 | else: log_warn(f"[Rename] Skip non-mp4 file: {file_name}") 243 | for key in file_names: 244 | try: file_names[key].sort() 245 | except: 246 | log_error(f"[Rename] Sort failed: {key}") 247 | if key in old_names: return 248 | range, _ = find_ranges(file_names[key]) 249 | log_info(f"[Rename] {key} count: {len(file_names[key])} range: {' '.join(range)}") 250 | 251 | for s in old_names: 252 | if s not in group: 253 | log_error(f"[Rename] Not in group: {s}") 254 | return 255 | 256 | log_warn(f"[Rename] Press ENTER to continue") 257 | input() 258 | 259 | for i, s in enumerate(old_names): 260 | indexs = file_names[s] 261 | names = [f"{s}_{num}" for num in indexs] 262 | task_name = new_names[i] 263 | for j, name in enumerate(names): 264 | new_name = f'{task_name}_{j + 1}' 265 | new_json_name = f'{new_name}.json' 266 | new_video_name = f'{new_name}.mp4' 267 | 268 | # 对json处理 269 | old_json_name = f"{name}.json" 270 | old_json_path = os.path.join(annos_folder, old_json_name) 271 | data = load_json(old_json_path) 272 | for frame in data: 273 | frame["stream"] = new_video_name # 改stream 274 | save_json(data, annos_folder, new_json_name) 275 | log_info(f"[Rename] New annotation: {annos_folder} -> {new_json_name}") 276 | if old_json_name != new_json_name: 277 | os.remove(old_json_path) 278 | 279 | # 对video处理 280 | old_video_name = f"{name}.mp4" 281 | old_video_file = os.path.join(videos_folder, old_video_name) 282 | new_video_file = os.path.join(videos_folder, new_video_name) 283 | os.rename(old_video_file, new_video_file) 284 | log_info(f"[Rename] New video: {new_video_file}") 285 | 286 | def create_dataset(dataset_folder, 287 | *groups, 288 | videos_resource=os.path.join(HOME_PATH, "RoboMatrixDatasetsALL", "videos"), 289 | annotations_resource=os.path.join(HOME_PATH, "RoboMatrixDatasetsALL", "annotations")): 290 | ''' 291 | *groups: (move_to_object, [[1, 10], [20], [30, 40]]) 292 | ''' 293 | # 检查是否已经存在 294 | if os.path.exists(dataset_folder): 295 | log_error(f"[Create Dataset] Already exist: {dataset_folder}") 296 | return 297 | 298 | # 检查原始数据集 299 | videos_info = analyse_folder(videos_resource) 300 | annos_info = analyse_folder(annotations_resource) 301 | if not compare_information(videos_info, annos_info): 302 | log_error(f"[Create Dataset] Dataset error") 303 | return 304 | 305 | # 检查任务描述和范围 306 | log_info(f"[Create Dataset] Your choice:") 307 | status = True 308 | for task_name, index_ranges in groups: 309 | log_info(f"Task: {task_name}") 310 | log_info(f"Range: {index_ranges}") 311 | 312 | if task_name not in videos_info[".mp4"] or task_name not in annos_info[".json"]: 313 | log_error(f"Invalid task description: {task_name}") 314 | status = False 315 | continue 316 | 317 | main_ranges = videos_info[".mp4"][task_name] 318 | if not compare_intervals(main_ranges, index_ranges): 319 | log_error(f"Invalid episode range: {index_ranges}") 320 | status = False 321 | if not status: return 322 | 323 | # 继续 324 | log_info(f"[Create Dataset] Press ENTER to continue") 325 | input() 326 | 327 | # 创建文件夹 328 | if not os.path.exists(dataset_folder): 329 | os.mkdir(dataset_folder) 330 | os.mkdir(os.path.join(dataset_folder, "videos")) 331 | os.mkdir(os.path.join(dataset_folder, "annotations")) 332 | log_info(f"[Create Dataset] Create dataset: {dataset_folder}") 333 | 334 | # 复制文件 335 | for task_name, index_ranges in groups: 336 | log_info(f"[Create Dataset] Copy: {task_name}") 337 | log_info(f"Range: {index_ranges}") 338 | indexs = [] 339 | for r in index_ranges: 340 | if len(r) == 1: indexs.append(r[0]) 341 | elif len(r) == 2: 342 | start, end = r 343 | for i in range(start, end+1): indexs.append(i) 344 | log_info(f"Count: {len(indexs)}") 345 | log_info(f"Press ENTER to continue") 346 | input() 347 | for index in indexs: 348 | name = f"{task_name}_{index}" 349 | resource_video = os.path.join(videos_resource, f"{name}.mp4") 350 | resource_anno = os.path.join(annotations_resource, f"{name}.json") 351 | destination_video = os.path.join(dataset_folder, "videos", f"{name}.mp4") 352 | destination_anno = os.path.join(dataset_folder, "annotations", f"{name}.json") 353 | shutil.copy(resource_video, destination_video) 354 | shutil.copy(resource_anno, destination_anno) 355 | log_info(f"OK: {name}") 356 | 357 | -------------------------------------------------------------------------------- /robomatrix_server/robomatrix_server/vla_service.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import rclpy 4 | from rclpy.node import Node 5 | # from robomatrix_server.srv import Vla 6 | # import pdb; pdb.set_trace() 7 | from robomatrix_interface.srv import Vla, Audio 8 | from .resource.inference_multistage import * 9 | import time 10 | 11 | class VLAServiceServer(Node): 12 | def __init__(self, robot: RobotInference): 13 | super().__init__('vla_service_server') 14 | self.robot_vla = robot 15 | 16 | self.vla_server = self.create_service(Vla, 'vla_service', self.handle_vla_service) 17 | self.audio_server = self.create_service(Audio, 'audio_service', self.handle_audio_service) 18 | 19 | def handle_vla_service(self, request, response): 20 | self.get_logger().info(f'Received: {request.task}, {request.msg}') 21 | # vla task 22 | self.robot_vla.exec_vla_task(request.task, request.msg) 23 | 24 | response.msg = f'Task {request.msg} complete!' 25 | response.success = True 26 | self.get_logger().info(f'Task {request.msg} complete!') 27 | 28 | return response 29 | 30 | def handle_audio_service(self, request, response): 31 | self.get_logger().info(f'Received audio task') 32 | # audio task 33 | res = self.robot_vla.exec_get_audio(request.path) 34 | 35 | response.success = True 36 | 37 | self.get_logger().info(f'Audio record!') 38 | 39 | return response 40 | 41 | def main(args=None): 42 | rclpy.init(args=args) 43 | # robot = RobotInference() 44 | robot = RobotInference( 45 | node=VLAServiceServer('vla_service_server'), 46 | dataset_dir=os.path.join(HOME_PATH, "RoboMatrixDatasetsDIY"), 47 | task_name="skill_model_11_12", 48 | url="http://localhost:7893/process_frame", 49 | debug=False) 50 | service_server = VLAServiceServer(robot) 51 | # robot.init_robot() 52 | print("RM service wait for call!") 53 | rclpy.spin(service_server) # 保持节点活跃,等待请求 54 | rclpy.shutdown() 55 | 56 | if __name__ == '__main__': 57 | main() 58 | -------------------------------------------------------------------------------- /robomatrix_server/setup.cfg: -------------------------------------------------------------------------------- 1 | [develop] 2 | script_dir=$base/lib/robomatrix_server 3 | [install] 4 | install_scripts=$base/lib/robomatrix_server 5 | -------------------------------------------------------------------------------- /robomatrix_server/setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup, find_packages 2 | from glob import glob 3 | import os 4 | 5 | package_name = "robomatrix_server" 6 | module_name = "robomatrix_server/resource" 7 | 8 | setup( 9 | name=package_name, 10 | version="0.0.0", 11 | packages=[package_name, module_name], 12 | # packages=[package_name 13 | # ], 14 | data_files=[ 15 | ("share/ament_index/resource_index/packages", ["resource/" + package_name]), 16 | ("share/" + package_name, ["package.xml"]), 17 | # ( 18 | # "share/" + package_name + "/resource", 19 | # ), 20 | ], 21 | install_requires=["setuptools"], 22 | zip_safe=True, 23 | maintainer="blackbird", 24 | maintainer_email="zwh@163.com", 25 | description="TODO: Package description", 26 | license="TODO: License declaration", 27 | tests_require=["pytest"], 28 | entry_points={ 29 | "console_scripts": [ 30 | "vla_service = robomatrix_server.vla_service:main", 31 | # "inference_multistage = robomatrix_server.inference_multistage:main" 32 | ], 33 | }, 34 | ) 35 | -------------------------------------------------------------------------------- /teleoperation/joystick_driver/joystick_driver/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WayneMao/RoboMatrix/71b077ee5904078f1cbf9008e3fb1256643ba832/teleoperation/joystick_driver/joystick_driver/__init__.py -------------------------------------------------------------------------------- /teleoperation/joystick_driver/joystick_driver/xbox_controller.py: -------------------------------------------------------------------------------- 1 | import rclpy 2 | from rclpy.node import Node 3 | 4 | import pygame 5 | 6 | import time 7 | import threading 8 | 9 | from joystick_msgs.msg import XBox360Controller 10 | 11 | 12 | class XBoxDriver(Node): 13 | 14 | BUTTON_NAME = ['A', 'B', 'X', "Y", 'LB', 'RB', 'BACK', 'START', 'POWER', 'LS', 'RS'] 15 | AXIS_NAME = ['LS_X', 'LS_Y', 'LT', 'RS_X', 'RS_Y', 'RT'] 16 | HAT_NAME = ['X', 'Y'] 17 | 18 | def __init__(self) -> None: 19 | super().__init__('xbox_360_controller') 20 | 21 | pygame.init() 22 | pygame.joystick.init() 23 | self._device = None 24 | 25 | self._rate = 30 26 | 27 | self._joystick_publisher = self.create_publisher(XBox360Controller, '/joystick', 3) 28 | 29 | self._is_activate = False 30 | 31 | self._joystick_thread = threading.Thread(target=self._joystick_loop) 32 | self._joystick_thread.start() 33 | 34 | self.shutdown = False 35 | 36 | def _get_joystick(self): 37 | if self._device: return self._device 38 | 39 | if pygame.joystick.get_count() == 0: 40 | self.get_logger().error("No joystick detected") 41 | return False 42 | elif pygame.joystick.get_count() > 1: 43 | self.get_logger().error("Multi joystick detected") 44 | return False 45 | else: 46 | self._device = pygame.joystick.Joystick(0) 47 | self._device.init() 48 | 49 | self._name = self._device.get_name() 50 | self.get_logger().info(f"{self._name} is connected") 51 | self._button_num = self._device.get_numbuttons() 52 | self._axis_num = self._device.get_numaxes() 53 | self._hat_num = self._device.get_numhats() 54 | 55 | return self._device 56 | 57 | def _joystick_loop(self): 58 | try: 59 | while not self._get_joystick(): time.sleep(1) 60 | self._is_activate = True 61 | 62 | while rclpy.ok(): 63 | if self.shutdown: break 64 | 65 | events = pygame.event.get() 66 | 67 | if not self._is_activate: 68 | if self._device.get_button(self.BUTTON_NAME.index('POWER')) == 1: 69 | self._is_activate = True 70 | self.get_logger().info("Joystick activated, press POWER button to deactivate") 71 | time.sleep(0.2) 72 | else: self.get_logger().info("Joystick activated, press POWER button to deactivate") 73 | 74 | while self._is_activate: 75 | events = pygame.event.get() 76 | 77 | # if self._device.get_button(self.BUTTON_NAME.index('POWER')) == 1: 78 | # self._is_activate = False 79 | # self.get_logger().info("Joystick deactivated, press POWER button to activate") 80 | # time.sleep(0.2) 81 | # continue 82 | if self._device.get_button(self.BUTTON_NAME.index('POWER')) == 1: 83 | self._is_activate = False 84 | self.get_logger().info("Joystick shutdown") 85 | self.shutdown = True 86 | time.sleep(0.2) 87 | break 88 | 89 | button_list, axis_list, hat_list = [], [], [] 90 | for i in range(self._button_num): 91 | button_list.append(int(self._device.get_button(i))) 92 | for i in range(self._axis_num): 93 | axis_list.append(float(self._device.get_axis(i))) 94 | for i in range(self._hat_num): 95 | x, y = self._device.get_hat(i) 96 | hat_list.append(int(x)) 97 | hat_list.append(int(y)) 98 | 99 | msg = XBox360Controller() 100 | msg.header.stamp = self.get_clock().now().to_msg() 101 | msg.header.frame_id = "joystick_data" 102 | msg.name = self._name 103 | msg.buttons = button_list 104 | msg.axes = axis_list 105 | msg.hats = hat_list 106 | try: 107 | msg.button_a = button_list[self.BUTTON_NAME.index('A')] 108 | msg.button_b = button_list[self.BUTTON_NAME.index('B')] 109 | msg.button_x = button_list[self.BUTTON_NAME.index('X')] 110 | msg.button_y = button_list[self.BUTTON_NAME.index('Y')] 111 | msg.button_lb = button_list[self.BUTTON_NAME.index('LB')] 112 | msg.button_rb = button_list[self.BUTTON_NAME.index('RB')] 113 | msg.button_back = button_list[self.BUTTON_NAME.index('BACK')] 114 | msg.button_start = button_list[self.BUTTON_NAME.index('START')] 115 | msg.button_power = button_list[self.BUTTON_NAME.index('POWER')] 116 | msg.button_ls = button_list[self.BUTTON_NAME.index('LS')] 117 | msg.button_rs = button_list[self.BUTTON_NAME.index('RS')] 118 | except: pass 119 | try: 120 | msg.axis_ls_x = axis_list[self.AXIS_NAME.index('LS_X')] 121 | msg.axis_ls_y = axis_list[self.AXIS_NAME.index('LS_Y')] 122 | msg.axis_lt = axis_list[self.AXIS_NAME.index('LT')] 123 | msg.axis_rs_x = axis_list[self.AXIS_NAME.index('RS_X')] 124 | msg.axis_rs_y = axis_list[self.AXIS_NAME.index('RS_Y')] 125 | msg.axis_rt = axis_list[self.AXIS_NAME.index('RT')] 126 | except: pass 127 | try: 128 | msg.hat_x = hat_list[self.HAT_NAME.index('X')] 129 | msg.hat_y = hat_list[self.HAT_NAME.index('Y')] 130 | except: pass 131 | 132 | self._joystick_publisher.publish(msg) 133 | time.sleep(1 / self._rate) 134 | except KeyboardInterrupt: 135 | self.get_logger().warn("Keyboard Interrupt") 136 | finally: 137 | self._device.quit() 138 | pygame.joystick.quit() 139 | pygame.quit() 140 | self.get_logger().info("Node end") 141 | 142 | 143 | def main(args=None): 144 | rclpy.init(args=args) 145 | 146 | node = XBoxDriver() 147 | try: 148 | while rclpy.ok(): 149 | rclpy.spin_once(node, timeout_sec=1) 150 | if node.shutdown: break 151 | 152 | except KeyboardInterrupt: print("Keyboard Interrupt") 153 | finally: 154 | node.destroy_node() 155 | rclpy.shutdown() 156 | -------------------------------------------------------------------------------- /teleoperation/joystick_driver/package.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | joystick_driver 5 | 0.0.0 6 | TODO: Package description 7 | jyxc 8 | TODO: License declaration 9 | 10 | ament_copyright 11 | ament_flake8 12 | ament_pep257 13 | python3-pytest 14 | 15 | rclpy 16 | joystick_msgs 17 | 18 | 19 | ament_python 20 | 21 | 22 | -------------------------------------------------------------------------------- /teleoperation/joystick_driver/resource/joystick_driver: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WayneMao/RoboMatrix/71b077ee5904078f1cbf9008e3fb1256643ba832/teleoperation/joystick_driver/resource/joystick_driver -------------------------------------------------------------------------------- /teleoperation/joystick_driver/setup.cfg: -------------------------------------------------------------------------------- 1 | [develop] 2 | script-dir=$base/lib/joystick_driver 3 | [install] 4 | install-scripts=$base/lib/joystick_driver 5 | -------------------------------------------------------------------------------- /teleoperation/joystick_driver/setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import find_packages, setup 2 | 3 | package_name = 'joystick_driver' 4 | 5 | setup( 6 | name=package_name, 7 | version='0.1.0', 8 | packages=find_packages(exclude=['test']), 9 | data_files=[ 10 | ('share/ament_index/resource_index/packages', 11 | ['resource/' + package_name]), 12 | ('share/' + package_name, ['package.xml']), 13 | ], 14 | install_requires=['setuptools'], 15 | zip_safe=True, 16 | maintainer='weiheng zhong', 17 | maintainer_email='weiheng_z@bit.edu.cn', 18 | description='TODO: Package description', 19 | license='TODO: License declaration', 20 | tests_require=['pytest'], 21 | entry_points={ 22 | 'console_scripts': [ 23 | 'xbox_driver = joystick_driver.xbox_controller:main', 24 | ], 25 | }, 26 | ) 27 | -------------------------------------------------------------------------------- /teleoperation/joystick_msgs/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.8) 2 | project(joystick_msgs) 3 | 4 | if(CMAKE_COMPILER_IS_GNUCXX OR CMAKE_CXX_COMPILER_ID MATCHES "Clang") 5 | add_compile_options(-Wall -Wextra -Wpedantic) 6 | endif() 7 | 8 | # find dependencies 9 | find_package(ament_cmake REQUIRED) 10 | find_package(rosidl_default_generators REQUIRED) 11 | find_package(std_msgs REQUIRED) 12 | # uncomment the following section in order to fill in 13 | # further dependencies manually. 14 | # find_package( REQUIRED) 15 | 16 | if(BUILD_TESTING) 17 | find_package(ament_lint_auto REQUIRED) 18 | # the following line skips the linter which checks for copyrights 19 | # comment the line when a copyright and license is added to all source files 20 | set(ament_cmake_copyright_FOUND TRUE) 21 | # the following line skips cpplint (only works in a git repo) 22 | # comment the line when this package is in a git repo and when 23 | # a copyright and license is added to all source files 24 | set(ament_cmake_cpplint_FOUND TRUE) 25 | ament_lint_auto_find_test_dependencies() 26 | endif() 27 | 28 | rosidl_generate_interfaces(${PROJECT_NAME} 29 | "msg/XBox360Controller.msg" 30 | "msg/Button.msg" 31 | "msg/Hat.msg" 32 | "msg/Axis.msg" 33 | DEPENDENCIES std_msgs 34 | ) 35 | 36 | ament_export_dependencies(rosidl_default_runtime) 37 | 38 | ament_package() 39 | -------------------------------------------------------------------------------- /teleoperation/joystick_msgs/msg/Axis.msg: -------------------------------------------------------------------------------- 1 | # XBOX 360 Controller 2 | 3 | # AXIS_NAME = ['LS_X', 'LS_Y', 'LT', 'RS_X', 'RS_Y', 'RT'] 4 | 5 | float32 ls_x 6 | float32 ls_y 7 | float32 lt 8 | float32 rs_x 9 | float32 rs_y 10 | float32 rt 11 | -------------------------------------------------------------------------------- /teleoperation/joystick_msgs/msg/Button.msg: -------------------------------------------------------------------------------- 1 | # XBOX 360 Controller 2 | 3 | # BUTTON_NAME = ['A', 'B', 'X', "Y", 'LB', 'RB', 'BACK', 'START', 'POWER', 'LS', 'RS'] 4 | 5 | uint8 a 6 | uint8 b 7 | uint8 x 8 | uint8 y 9 | uint8 lb 10 | uint8 rb 11 | uint8 back 12 | uint8 start 13 | uint8 power 14 | uint8 ls 15 | uint8 rs 16 | -------------------------------------------------------------------------------- /teleoperation/joystick_msgs/msg/Hat.msg: -------------------------------------------------------------------------------- 1 | # XBOX 360 Controller 2 | 3 | # HAT_NAME = ['X', 'Y'] 4 | 5 | int8 x 6 | int8 y 7 | -------------------------------------------------------------------------------- /teleoperation/joystick_msgs/msg/XBox360Controller.msg: -------------------------------------------------------------------------------- 1 | # XBOX 360 Controller 2 | 3 | std_msgs/Header header 4 | 5 | uint8[] buttons 6 | float32[] axes 7 | int8[] hats 8 | 9 | string name 10 | 11 | # BUTTON_NAME = ['A', 'B', 'X', "Y", 'LB', 'RB', 'BACK', 'START', 'POWER', 'LS', 'RS'] 12 | # AXIS_NAME = ['LS_X', 'LS_Y', 'LT', 'RS_X', 'RS_Y', 'RT'] 13 | # HAT_NAME = ['X', 'Y'] 14 | 15 | uint8 button_a 16 | uint8 button_b 17 | uint8 button_x 18 | uint8 button_y 19 | uint8 button_lb 20 | uint8 button_rb 21 | uint8 button_back 22 | uint8 button_start 23 | uint8 button_power 24 | uint8 button_ls 25 | uint8 button_rs 26 | 27 | float32 axis_ls_x 28 | float32 axis_ls_y 29 | float32 axis_lt 30 | float32 axis_rs_x 31 | float32 axis_rs_y 32 | float32 axis_rt 33 | 34 | int8 hat_x 35 | int8 hat_y 36 | -------------------------------------------------------------------------------- /teleoperation/joystick_msgs/package.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | joystick_msgs 5 | 0.0.0 6 | TODO: Package description 7 | jyxc 8 | TODO: License declaration 9 | 10 | ament_cmake 11 | 12 | ament_lint_auto 13 | ament_lint_common 14 | 15 | rosidl_default_generators 16 | std_msgs 17 | rosidl_default_runtime 18 | std_msgs 19 | 20 | rosidl_interface_packages 21 | 22 | 23 | ament_cmake 24 | 25 | 26 | --------------------------------------------------------------------------------