6.1 Python接口使用示例

本章节将带您实现索引所示若干功能

构建 & 运行说明

  • 进入到 AimDK 顶层目录(机器人上为 ~/aimdk),执行以下命令

    source /opt/ros/humble/setup.bash
    colcon build
    source install/local_setup.bash
    ros2 run py_examples '对应功能名称如: get_mc_action'
    

📝 代码说明

完整的代码实现包含完整的错误处理、信号处理、超时处理等机制,确保程序的健壮性。请您在 py_examples 目录下查看/修改

小心

ROS的服务(service)机制在跨板通信时存在一些待优化问题, 二次开发时请参考例程添加异常处理、快速重试等保护机制

机器人处于稳定站立 / 走跑模式时, 请勿批量快速启动 ROS 节点(建议不超过 2 个/秒), 短时间内大量节点参与 DDS 发现会造成通信拥塞、影响运控实时性, 存在机器人失稳摔倒风险

6.1.1 进入开发者模式

通过调用'GetSystemState'服务获取机器人当前的系统状态,并且通过'MigrateSystemState'进入对应的开发者模式。

开发者模式定义

  1#!/usr/bin/env python3
  2"""
  3System State Migrator - Synchronous Python version
  4"""
  5
  6import sys
  7import time
  8import rclpy
  9from rclpy.node import Node
 10import aimdk_msgs.srv as aimdk_srv
 11import aimdk_msgs.msg as aimdk_msg
 12
 13
 14class SyncSystemStateMigrator(Node):
 15    """Synchronous version of system state migrator"""
 16
 17    def __init__(self, target_state: str):
 18        super().__init__('sync_system_state_migrator')
 19        self.target_state = target_state
 20
 21        # Create service clients
 22        self.migrate_client = self.create_client(
 23            aimdk_srv.MigrateSystemState,
 24            '/aimdk_5Fmsgs/srv/MigrateSystemState'
 25        )
 26
 27        self.get_state_client = self.create_client(
 28            aimdk_srv.GetSystemState,
 29            '/aimdk_5Fmsgs/srv/GetSystemState'
 30        )
 31
 32        self.get_logger().info(f'Target state: {self.target_state}')
 33
 34    def wait_for_services(self, timeout_sec: float = 30.0) -> bool:
 35        """Wait for services to become available"""
 36        self.get_logger().info('Waiting for services...')
 37
 38        start_time = time.time()
 39        while time.time() - start_time < timeout_sec and rclpy.ok():
 40            if (self.migrate_client.wait_for_service(timeout_sec=1.0) and
 41                    self.get_state_client.wait_for_service(timeout_sec=1.0)):
 42                self.get_logger().info('Services are available')
 43                return True
 44
 45            self.get_logger().info('Services not available, waiting...')
 46            time.sleep(0.5)
 47
 48        self.get_logger().error(
 49            f'Timeout waiting for services after {timeout_sec} seconds')
 50        return False
 51
 52    def migrate_state_sync(self, max_retries: int = 5, timeout_sec: float = 30.0) -> bool:
 53        """Send migration request synchronously with retry"""
 54        request = aimdk_srv.MigrateSystemState.Request()
 55        now = self.get_clock().now()
 56        request.header.header.stamp.sec = now.nanoseconds // 1_000_000_000
 57        request.header.header.stamp.nanosec = now.nanoseconds % 1_000_000_000
 58        request.state = self.target_state
 59
 60        for attempt in range(1, max_retries + 1):
 61            self.get_logger().info(
 62                f'Sending migration request to {self.target_state} '
 63                f'(attempt {attempt}/{max_retries})...'
 64            )
 65
 66            future = None
 67            try:
 68                future = self.migrate_client.call_async(request)
 69
 70                start_time = time.time()
 71                while rclpy.ok() and time.time() - start_time < timeout_sec:
 72                    rclpy.spin_once(self, timeout_sec=0.1)
 73                    if future.done():
 74                        response = future.result()
 75                        # SUCCESS (1)
 76                        if response.header.status.value == aimdk_msg.CommonState.SUCCESS:
 77                            self.get_logger().info('Migration request accepted')
 78                            return True
 79                        else:
 80                            self.get_logger().error(
 81                                f'Migration failed: {response.header.message}'
 82                            )
 83                            return False
 84
 85                self.get_logger().warning(
 86                    f'Timeout waiting for migration response '
 87                    f'(attempt {attempt}/{max_retries})'
 88                )
 89
 90            except Exception as e:
 91                self.get_logger().error(
 92                    f'Migration service call failed (attempt {attempt}/{max_retries}): {e}'
 93                )
 94            finally:
 95                if future is not None and not future.done():
 96                    future.cancel()
 97
 98            if attempt < max_retries:
 99                self.get_logger().info('Retrying migration request in 2.0 seconds...')
100                time.sleep(2.0)
101
102        self.get_logger().error(
103            f'Migration request failed after {max_retries} attempts'
104        )
105        return False
106
107    def get_current_state_sync(self, max_retries: int = 3, timeout_sec: float = 10.0):
108        """Get current system state synchronously with retry"""
109        request = aimdk_srv.GetSystemState.Request()
110        now = self.get_clock().now()
111        request.header.header.stamp.sec = now.nanoseconds // 1_000_000_000
112        request.header.header.stamp.nanosec = now.nanoseconds % 1_000_000_000
113
114        for attempt in range(1, max_retries + 1):
115            future = None
116            try:
117                future = self.get_state_client.call_async(request)
118
119                start_time = time.time()
120                while rclpy.ok() and time.time() - start_time < timeout_sec:
121                    rclpy.spin_once(self, timeout_sec=0.1)
122                    if future.done():
123                        return future.result()
124
125                self.get_logger().warning(
126                    f'Timeout getting system state (attempt {attempt}/{max_retries})'
127                )
128
129            except Exception as e:
130                self.get_logger().error(
131                    f'GetSystemState service call failed (attempt {attempt}/{max_retries}): {e}'
132                )
133            finally:
134                if future is not None and not future.done():
135                    future.cancel()
136
137            if attempt < max_retries:
138                time.sleep(1.0)
139
140        self.get_logger().error(
141            f'Failed to get system state after {max_retries} attempts'
142        )
143        return None
144
145    def monitor_migration_sync(self, max_checks: int = 300) -> bool:
146        """Monitor migration progress synchronously"""
147        self.get_logger().info('Monitoring migration progress...')
148
149        check_count = 0
150        start_time = time.time()
151
152        while check_count < max_checks and rclpy.ok():
153            # Wait between checks
154            time.sleep(1.0)
155
156            # Check current state
157            response = self.get_current_state_sync()
158            if response is None:
159                self.get_logger().warning('Failed to get system state, will retry...')
160                continue
161
162            check_count += 1
163
164            # Check if migration is complete
165            # Ready auto-transitions to Business, so also accept Business
166            # when the target is Ready
167            current_state_lower = response.cur_state.lower()
168            target_state_lower = self.target_state.lower()
169
170            state_match = (
171                current_state_lower == target_state_lower or
172                (target_state_lower == 'ready' and
173                 current_state_lower == 'business')
174            )
175            ready_auto_transitioned = (
176                target_state_lower == 'ready' and
177                current_state_lower == 'business'
178            )
179            status_match = (
180                ready_auto_transitioned or
181                response.curr_status.value == aimdk_msg.SystemStatus.IN_READY
182            )
183
184            self.get_logger().info(
185                f'Check {check_count}: State="{response.cur_state}" '
186                f'(match={state_match}), Status={response.curr_status.value} (match={status_match})'
187            )
188
189            if state_match and status_match:
190                self.get_logger().info(
191                    f'Migration to {self.target_state} completed successfully!')
192                return True
193
194            # Check timeout
195            if time.time() - start_time > 300.0:  # 5 minutes total timeout
196                self.get_logger().error('Migration timeout after 5 minutes')
197                return False
198
199        self.get_logger().error(f'Migration failed after {max_checks} checks')
200        return False
201
202    def run_migration(self) -> bool:
203        """Run complete migration process"""
204        # Step 1: Wait for services
205        if not self.wait_for_services():
206            return False
207
208        # Step 2: Send migration request
209        if not self.migrate_state_sync():
210            return False
211
212        # Step 3: Monitor migration progress
213        return self.monitor_migration_sync()
214
215
216def main():
217    """
218    Main function
219    """
220    # Check command line arguments
221    if len(sys.argv) != 2:
222        print(f'Usage: {sys.argv[0]} <target_state>')
223        print('\nAvailable target states: Ready, Develop_Nav, Develop_Audio_Linux, Develop_Audio_ROS, Develop_MC')
224        return 1
225
226    # Get target state from command line argument
227    target_state = sys.argv[1]
228
229    # Validate target state
230    if not target_state:
231        print('Error: Target state cannot be empty', file=sys.stderr)
232        return 1
233
234    # Initialize ROS2
235    rclpy.init()
236
237    result = 1
238    node = None
239
240    try:
241        # Create migrator node
242        node = SyncSystemStateMigrator(target_state)
243
244        # Run migration
245        if node.run_migration():
246            print(f'\nMigration to {target_state} completed successfully!')
247            result = 0
248        else:
249            print(f'\nMigration to {target_state} failed')
250            result = 1
251
252    except KeyboardInterrupt:
253        print('\nProgram interrupted by user')
254        result = 1
255    except Exception as e:
256        print(f'Error: {e}', file=sys.stderr)
257        result = 1
258    finally:
259        # Cleanup
260        if node:
261            node.destroy_node()
262        if rclpy.ok():
263            rclpy.shutdown()
264
265    return result
266
267
268if __name__ == '__main__':
269    sys.exit(main())

使用说明

ros2 run py_examples migrate_system_state Ready

输出示例

Ready 是开机后的过渡态,系统会自动迁移到 Business。因此以 Ready 为目标时,监控循环可能在 Business 状态下完成(视为迁移成功)。

[INFO] [1770125663.176574907] [sync_system_state_migrator]: Target state: Ready
[INFO] [1770125663.176758843] [sync_system_state_migrator]: Waiting for services...
[INFO] [1770125663.678726368] [sync_system_state_migrator]: Services are available
[INFO] [1770125663.679642487] [sync_system_state_migrator]: Sending migration request to Ready (attempt 1/5)...
[INFO] [1770125663.692394889] [sync_system_state_migrator]: Migration request accepted
[INFO] [1770125663.693117341] [sync_system_state_migrator]: Monitoring migration progress...
[INFO] [1770125664.728735142] [sync_system_state_migrator]: Check 1: State="Business" (match=True), Status=2 (match=True)
[INFO] [1770125664.728735142] [sync_system_state_migrator]: Migration to Ready completed successfully!

6.1.2 获取机器人模式

通过调用GetMcAction服务获取机器人当前的运行模式,包括名称和状态信息。

运动模式定义

 1#!/usr/bin/env python3
 2
 3import rclpy
 4import rclpy.logging
 5from rclpy.node import Node
 6
 7from aimdk_msgs.srv import GetMcAction
 8from aimdk_msgs.msg import CommonRequest
 9
10
11class GetMcActionClient(Node):
12    def __init__(self):
13        super().__init__('get_mc_action_client')
14        self.client = self.create_client(
15            GetMcAction, '/aimdk_5Fmsgs/srv/GetMcAction')
16        self.get_logger().info('✅ GetMcAction client node created.')
17
18        # Wait for the service to become available
19        while not self.client.wait_for_service(timeout_sec=2.0):
20            self.get_logger().info('⏳ Service unavailable, waiting...')
21
22        self.get_logger().info('🟢 Service available, ready to send request.')
23
24    def send_request(self):
25        request = GetMcAction.Request()
26        request.request = CommonRequest()
27
28        self.get_logger().info('📨 Sending request to get robot mode')
29        for i in range(8):
30            request.request.header.stamp = self.get_clock().now().to_msg()
31            future = self.client.call_async(request)
32            rclpy.spin_until_future_complete(self, future, timeout_sec=0.25)
33
34            if future.done():
35                break
36
37            # retry as remote peer is NOT handled well by ROS
38            self.get_logger().info(f'trying ... [{i}]')
39
40        response = future.result()
41        if response is None:
42            self.get_logger().error('❌ Service call failed or timed out.')
43            return
44
45        self.get_logger().info('✅ Robot mode get successfully.')
46        self.get_logger().info(f'Mode name: {response.info.action_desc}')
47        self.get_logger().info(f'Mode status: {response.info.status.value}')
48
49
50def main(args=None):
51    rclpy.init(args=args)
52    node = None
53    try:
54        node = GetMcActionClient()
55        node.send_request()
56    except KeyboardInterrupt:
57        pass
58    except Exception as e:
59        rclpy.logging.get_logger('main').error(
60            f'Program exited with exception: {e}')
61
62    if node:
63        node.destroy_node()
64    if rclpy.ok():
65        rclpy.shutdown()
66
67
68if __name__ == '__main__':
69    main()

使用说明

ros2 run py_examples get_mc_action

输出示例

[INFO] [1779173975.378396584] [get_mc_action_client]: ✅ GetMcAction client node created.
[INFO] [1779173975.961720731] [get_mc_action_client]: 🟢 Service available, ready to send request.
[INFO] [1779173975.961868725] [get_mc_action_client]: 📨 Sending request to get robot mode
[INFO] [1779173975.965180630] [get_mc_action_client]: ✅ Robot mode get successfully.
[INFO] [1779173975.965282450] [get_mc_action_client]: Mode name: PASSIVE_DEFAULT
[INFO] [1779173975.965307345] [get_mc_action_client]: Mode status: 100

接口参考

  • 服务:/aimdk_5Fmsgs/srv/GetMcAction

  • 消息:aimdk_msgs/srv/GetMcAction

6.1.3 设置机器人模式

该示例中用到了SetMcAction服务,运行节点程序后终端输入模式对应的字段值,机器人会尝试进入相应的运动模式。
切入稳定站立(STAND_DEFAULT)模式前,确保机器人已立起且双脚着地。
模式切换需遵循模式转换图, 跨模式转换不能成功
走跑模式(LOCOMOTION_DEFAULT)和稳定站立模式一体化自动切换,按模式转换图流转到两者中较近的即可

  1#!/usr/bin/env python3
  2
  3import sys
  4import rclpy
  5import rclpy.logging
  6from rclpy.node import Node
  7
  8from aimdk_msgs.srv import SetMcAction
  9from aimdk_msgs.msg import RequestHeader, CommonState, McActionCommand
 10
 11
 12class SetMcActionClient(Node):
 13    def __init__(self):
 14        super().__init__('set_mc_action_client')
 15        self.client = self.create_client(
 16            SetMcAction, '/aimdk_5Fmsgs/srv/SetMcAction'
 17        )
 18        self.get_logger().info('✅ SetMcAction client node created.')
 19
 20        # Wait for the service to become available
 21        while not self.client.wait_for_service(timeout_sec=2.0):
 22            self.get_logger().info('⏳ Service unavailable, waiting...')
 23
 24        self.get_logger().info('🟢 Service available, ready to send request.')
 25
 26    def send_request(self, action_name: str):
 27        req = SetMcAction.Request()
 28        req.header = RequestHeader()
 29        req.source = 'node.set_mc_action'
 30        #
 31        cmd = McActionCommand()
 32        cmd.action_desc = action_name
 33        req.command = cmd
 34
 35        self.get_logger().info(
 36            f'📨 Sending request to set robot mode: {action_name}')
 37        for i in range(8):
 38            req.header.stamp = self.get_clock().now().to_msg()
 39            future = self.client.call_async(req)
 40            rclpy.spin_until_future_complete(self, future, timeout_sec=0.25)
 41
 42            if future.done():
 43                break
 44
 45            # retry as remote peer is NOT handled well by ROS
 46            self.get_logger().info(f'trying ... [{i}]')
 47
 48        response = future.result()
 49        if response is None:
 50            self.get_logger().error('❌ Service call failed or timed out.')
 51            return
 52
 53        # SUCCESS (1)
 54        if response.response.status.value == CommonState.SUCCESS:
 55            self.get_logger().info('✅ Robot mode set successfully.')
 56        else:
 57            self.get_logger().error(
 58                f'❌ Failed (code={response.response.header.code}): '
 59                f'{response.response.message}'
 60            )
 61
 62
 63def main(args=None):
 64    action_info = {
 65        'PASSIVE_DEFAULT': ('PD', 'joints with zero torque'),
 66        'DAMPING_DEFAULT': ('DD', 'joints in damping mode'),
 67        'JOINT_DEFAULT': ('JD', 'Position Control Stand (joints locked)'),
 68        'STAND_DEFAULT': ('SD', 'Stable Stand (auto-balance)'),
 69        'LOCOMOTION_DEFAULT': ('LD', 'locomotion mode (walk or run)'),
 70        'HEAD_ONLY': ('HO', 'head-only control'),
 71        'UPPERBODY_REMOTE_SPLIT': ('URS', 'upper body remote split (head + arms + hands)'),
 72    }
 73
 74    choices = {}
 75    for k, v in action_info.items():
 76        choices[v[0]] = k
 77
 78    rclpy.init(args=args)
 79    node = None
 80    try:
 81        # Prefer command-line argument, otherwise prompt for input
 82        if len(sys.argv) > 1:
 83            motion = sys.argv[1]
 84        else:
 85            print('{:<4} - {:<20} : {}'.format('abbr',
 86                  'robot mode', 'description'))
 87            for k, v in action_info.items():
 88                print(f'{v[0]:<4} - {k:<20} : {v[1]}')
 89            motion = input('Enter abbr of robot mode:')
 90
 91        action_name = choices.get(motion)
 92        if not action_name:
 93            raise ValueError(f'Invalid abbr of robot mode: {motion}')
 94
 95        node = SetMcActionClient()
 96        node.send_request(action_name)
 97    except KeyboardInterrupt:
 98        pass
 99    except Exception as e:
100        rclpy.logging.get_logger('main').error(
101            f'Program exited with exception: {e}')
102
103    if node:
104        node.destroy_node()
105    if rclpy.ok():
106        rclpy.shutdown()
107
108
109if __name__ == '__main__':
110    main()

使用说明

# 使用命令行参数设置模式(推荐)
ros2 run py_examples set_mc_action JD  # 零力矩模式>>站姿预备(位控站立)
ros2 run py_examples set_mc_action SD  # 机器人已立起且脚着地后方可执行,站姿预备>>稳定站立
# 稳定站立>>走跑模式 自动切换,无需手动切换

# 或者不带参数运行,程序会提示用户输入双字母缩写代码
ros2 run py_examples set_mc_action

输出示例

...
[INFO] [1764066567.502968540] [set_mc_action_client]: ✅ Robot mode set successfully.

注意事项

  • 切入STAND_DEFAULT模式前,确保机器人已立起且双脚着地

  • 模式切换可能需要几秒钟时间完成

接口参考

  • 服务:/aimdk_5Fmsgs/srv/SetMcAction

  • 消息:aimdk_msgs/srv/SetMcAction

6.1.4 设置机器人动作

该示例中用到了preset_motion_client,进入稳定站立模式后,启动节点程序,输入相应的字段值可实现左手(右手)的握手(举手、挥手、飞吻)动作。

可供调用的参数见预设动作表

  1#!/usr/bin/env python3
  2
  3import rclpy
  4import rclpy.logging
  5from rclpy.node import Node
  6
  7from aimdk_msgs.srv import SetMcPresetMotion, SetMcInputSource
  8from aimdk_msgs.msg import (McPresetMotion, McControlArea, McInputAction,
  9                            RequestHeader, CommonState)
 10
 11INPUT_SOURCE_NAME = "node.preset_motion"
 12INPUT_SOURCE_PRIORITY = 40
 13INPUT_SOURCE_TIMEOUT = 1000
 14
 15
 16class SetMcPresetMotionClient(Node):
 17    def __init__(self):
 18        super().__init__('preset_motion_client')
 19        self.client = self.create_client(
 20            SetMcPresetMotion, '/aimdk_5Fmsgs/srv/SetMcPresetMotion')
 21        self.input_source_client = self.create_client(
 22            SetMcInputSource, '/aimdk_5Fmsgs/srv/SetMcInputSource')
 23        self.get_logger().info('✅ SetMcPresetMotion client node created.')
 24
 25        # Wait for the service to become available
 26        while not self.client.wait_for_service(timeout_sec=2.0):
 27            self.get_logger().info('⏳ Service unavailable, waiting...')
 28
 29        self.get_logger().info('🟢 Service available, ready to send request.')
 30
 31    def _call_input_source(self, action_value: int) -> bool:
 32        action_name = {
 33            McInputAction.INPUTACTION_ADD: "ADD",
 34            McInputAction.INPUTACTION_ENABLE: "ENABLE",
 35        }.get(action_value, str(action_value))
 36
 37        req = SetMcInputSource.Request()
 38        req.action.value = action_value
 39        req.input_source.name = INPUT_SOURCE_NAME
 40        req.input_source.priority = INPUT_SOURCE_PRIORITY
 41        req.input_source.timeout = INPUT_SOURCE_TIMEOUT
 42
 43        future = None
 44        for i in range(8):
 45            req.request.header.stamp = self.get_clock().now().to_msg()
 46            future = self.input_source_client.call_async(req)
 47            rclpy.spin_until_future_complete(self, future, timeout_sec=0.25)
 48            if future.done():
 49                break
 50            # retry as remote peer is NOT handled well by ROS
 51            self.get_logger().info(
 52                f'trying input source action {action_name} ({action_value})... [{i}]')
 53
 54        if future is None or not future.done():
 55            return False
 56        response = future.result()
 57        return response is not None and response.response.header.code == 0
 58
 59    def register_input_source(self) -> bool:
 60        if not self.input_source_client.wait_for_service(timeout_sec=8.0):
 61            self.get_logger().error('❌ SetMcInputSource service unavailable.')
 62            return False
 63
 64        # Try ADD first; if already registered, fall back to ENABLE
 65        if self._call_input_source(McInputAction.INPUTACTION_ADD):  # ADD (1001)
 66            self.get_logger().info(
 67                f'✅ Input source "{INPUT_SOURCE_NAME}" registered (ADD).')
 68            return True
 69        self.get_logger().info(
 70            'ADD failed (likely already registered), trying ENABLE...')
 71        if self._call_input_source(McInputAction.INPUTACTION_ENABLE):  # ENABLE (2001)
 72            self.get_logger().info(
 73                f'✅ Input source "{INPUT_SOURCE_NAME}" enabled.')
 74            return True
 75        self.get_logger().error('❌ Failed to register input source via ADD or ENABLE.')
 76        return False
 77
 78    def send_request(self, area_id: int, motion_id: int) -> bool:
 79        request = SetMcPresetMotion.Request()
 80        request.header = RequestHeader()
 81
 82        request.input_source.name = INPUT_SOURCE_NAME
 83        request.input_source.priority = INPUT_SOURCE_PRIORITY
 84        request.input_source.timeout = INPUT_SOURCE_TIMEOUT
 85
 86        motion = McPresetMotion()
 87        area = McControlArea()
 88
 89        motion.value = motion_id
 90        area.value = area_id
 91
 92        request.motion = motion
 93        request.area = area
 94        request.interrupt = False
 95
 96        self.get_logger().info(
 97            f'📨 Sending request to set preset motion: motion={motion_id}, area={area_id}')
 98
 99        for i in range(8):
100            request.header.stamp = self.get_clock().now().to_msg()
101            future = self.client.call_async(request)
102            rclpy.spin_until_future_complete(self, future, timeout_sec=0.25)
103
104            if future.done():
105                break
106
107            # retry as remote peer is NOT handled well by ROS
108            self.get_logger().info(f'trying ... [{i}]')
109
110        response = future.result()
111        if response is None:
112            self.get_logger().error('❌ Service call failed or timed out.')
113            return False
114
115        if response.response.header.code == 0:
116            self.get_logger().info(
117                f'✅ Preset motion set successfully: {response.response.task_id}')
118            return True
119        # RUNNING (400)
120        elif response.response.state.value == CommonState.RUNNING:
121            self.get_logger().info(
122                f'⏳ Preset motion executing: {response.response.task_id}')
123            return True
124        else:
125            self.get_logger().error(
126                f'❌ Failed to set preset motion: {response.response.task_id}'
127            )
128            return False
129
130
131def main(args=None):
132    rclpy.init(args=args)
133    node = None
134    try:
135        area = int(input("Enter arm area ID (1-left, 2-right): "))
136        motion = int(input(
137            "Enter preset motion ID (1001-raise, 1002-wave, 1003-handshake, 1004-airkiss): "))
138
139        node = SetMcPresetMotionClient()
140        if not node.register_input_source():
141            node.get_logger().error("❌ Input source registration failed, exiting")
142        else:
143            node.send_request(area, motion)
144    except KeyboardInterrupt:
145        pass
146    except Exception as e:
147        rclpy.logging.get_logger('main').error(
148            f'Program exited with exception: {e}')
149
150    if node:
151        node.destroy_node()
152    if rclpy.ok():
153        rclpy.shutdown()
154
155
156if __name__ == '__main__':
157    main()

6.1.5 夹爪控制

该示例中用到了hand_control,通过往话题/aima/hal/joint/hand/command发布消息来控制夹爪的运动

注意

注意⚠️ :运行本示例前需要在机器人运控计算单元(PC1)使用aima em stop-app mc关闭原生运控模块,获取机器人控制权。注意确保机器人安全

 1import rclpy
 2from rclpy.node import Node
 3from aimdk_msgs.msg import HandCommandArray, HandCommand, HandType, MessageHeader
 4
 5
 6class HandControl(Node):
 7    def __init__(self):
 8        super().__init__('hand_control')
 9
10        # Preset parameter list: [(left hand, right hand), ...]
11        self.position_pairs = [
12            (1.0, 1.0),   # fully open
13            (0.0, 0.0),   # fully closed
14            (0.5, 0.5),   # half open
15            (0.2, 0.8),   # left slightly closed, right more open
16            (0.7, 0.3)    # left more open, right slightly closed
17        ]
18        self.current_index = 0
19        self.last_switch_time = self.get_clock().now().nanoseconds / 1e9  # seconds
20
21        # Create publisher
22        self.publisher_ = self.create_publisher(
23            HandCommandArray,
24            '/aima/hal/joint/hand/command',
25            10
26        )
27
28        # 50 Hz timer
29        self.timer_ = self.create_timer(
30            0.02,  # 20 ms = 50 Hz
31            self.publish_hand_commands
32        )
33
34        self.get_logger().info("Hand control node started!")
35
36    def publish_hand_commands(self):
37        # Check time to decide whether to switch to the next preset
38        now_sec = self.get_clock().now().nanoseconds / 1e9
39        if now_sec - self.last_switch_time >= 2.0:
40            self.current_index = (self.current_index +
41                                  1) % len(self.position_pairs)
42            self.last_switch_time = now_sec
43            self.get_logger().info(
44                f"Switched to preset: {self.current_index}, left={self.position_pairs[self.current_index][0]:.2f}, right={self.position_pairs[self.current_index][1]:.2f}"
45            )
46
47        # Use current preset
48        left_position, right_position = self.position_pairs[self.current_index]
49
50        msg = HandCommandArray()
51        msg.header = MessageHeader()
52
53        # Configure left hand
54        left_hand = HandCommand()
55        left_hand.name = "left_hand"
56        left_hand.position = float(left_position)
57        left_hand.velocity = 1.0
58        left_hand.acceleration = 1.0
59        left_hand.deceleration = 1.0
60        left_hand.effort = 1.0
61
62        # Configure right hand
63        right_hand = HandCommand()
64        right_hand.name = "right_hand"
65        right_hand.position = float(right_position)
66        right_hand.velocity = 1.0
67        right_hand.acceleration = 1.0
68        right_hand.deceleration = 1.0
69        right_hand.effort = 1.0
70
71        msg.left_hand_type = HandType(value=HandType.CLAW)   # grippers (2)
72        msg.right_hand_type = HandType(value=HandType.CLAW)  # grippers (2)
73        msg.left_hands = [left_hand]
74        msg.right_hands = [right_hand]
75
76        # Publish message
77        self.publisher_.publish(msg)
78        # We only log when switching presets to avoid too much log output
79
80
81def main(args=None):
82    rclpy.init(args=args)
83    hand_control_node = HandControl()
84
85    try:
86        rclpy.spin(hand_control_node)
87    except KeyboardInterrupt:
88        pass
89    finally:
90        hand_control_node.destroy_node()
91        if rclpy.ok():
92            rclpy.shutdown()
93
94
95if __name__ == '__main__':
96    main()

6.1.6 灵巧手控制

该示例中用到了omnihand_control,通过往话题/aima/hal/joint/hand/command发布消息来控制灵巧手的运动

注意

注意⚠️ :运行本示例前需要在机器人运控计算单元(PC1)使用aima em stop-app mc关闭原生运控模块,获取机器人控制权。注意确保机器人安全

  1import rclpy
  2from rclpy.node import Node
  3from aimdk_msgs.msg import HandCommandArray, HandCommand, HandType
  4
  5
  6class HandControl(Node):
  7    def __init__(self):
  8        super().__init__('hand_control')
  9
 10        # Create publisher
 11        self.publisher_ = self.create_publisher(
 12            HandCommandArray,
 13            '/aima/hal/joint/hand/command',
 14            10
 15        )
 16
 17        self.timer_ = self.create_timer(
 18            1.0,
 19            self.publish_hand_commands
 20        )
 21
 22        # Initialize variables
 23        self.target_finger = 0
 24        self.step = 1
 25        self.increasing = True
 26        self.get_logger().info("Hand control node started!")
 27
 28    def build_hand_cmd(self, name: str) -> HandCommand:
 29        cmd = HandCommand()
 30        cmd.name = name
 31        cmd.position = 0.0
 32        cmd.velocity = 0.1
 33        cmd.acceleration = 0.0
 34        cmd.deceleration = 0.0
 35        cmd.effort = 0.0
 36        return cmd
 37
 38    def publish_hand_commands(self):
 39        msg = HandCommandArray()
 40        msg.header.stamp = self.get_clock().now().to_msg()
 41        msg.header.frame_id = 'hand_command'
 42        # dexterous hands (1)
 43        msg.left_hand_type.value = HandType.NIMBLE_HANDS
 44        # dexterous hands (1)
 45        msg.right_hand_type.value = HandType.NIMBLE_HANDS
 46
 47        left_names = ['L_thumb_roll_joint', 'L_thumb_abad_joint', 'L_thumb_mcp_joint',
 48                      'L_index_abad_joint', 'L_index_pip_joint', 'L_middle_pip_joint',
 49                      'L_ring_abad_joint', 'L_ring_pip_joint', 'L_pinky_abad_joint', 'L_pinky_pip_joint']
 50        right_names = [n.replace('L_', 'R_', 1) for n in left_names]
 51
 52        # left hand
 53        msg.left_hands = [self.build_hand_cmd(
 54            left_names[i]) for i in range(10)]
 55
 56        # right hand
 57        msg.right_hands = [self.build_hand_cmd(
 58            right_names[i]) for i in range(10)]
 59
 60        # Absolute joint targets in rad = half-grip
 61        # Joint order: [thumb_roll, thumb_abad, thumb_mcp, index_abad, index_pip,
 62        #               middle_pip, ring_abad, ring_pip, pinky_abad, pinky_pip]
 63        left_hand_target = [-0.6, 0.8, -0.4,
 64                            0.1, 0.7, 0.7, -0.1, 0.7, -0.1, 0.7]
 65        right_hand_target = [0.6, -0.8, 0.4,
 66                             -0.1, 0.7, 0.7, 0.1, 0.7, 0.1, 0.7]
 67
 68        if self.target_finger < 10:
 69            msg.right_hands[self.target_finger].position = right_hand_target[self.target_finger]
 70        else:
 71            target_finger_ = self.target_finger - 10
 72            msg.left_hands[target_finger_].position = left_hand_target[target_finger_]
 73
 74        self.publisher_.publish(msg)
 75        self.get_logger().info(
 76            f'Published hand command with target_finger: {self.target_finger}')
 77        self.update_target_finger()
 78
 79    def update_target_finger(self):
 80        if self.increasing:
 81            self.target_finger += self.step
 82            if self.target_finger >= 19:
 83                self.target_finger = 19
 84                self.increasing = False
 85        else:
 86            self.target_finger -= self.step
 87            if self.target_finger <= 0:
 88                self.target_finger = 0
 89                self.increasing = True
 90
 91
 92def main(args=None):
 93    rclpy.init(args=args)
 94    hand_control_node = HandControl()
 95
 96    try:
 97        rclpy.spin(hand_control_node)
 98    except KeyboardInterrupt:
 99        pass
100    finally:
101        hand_control_node.destroy_node()
102        if rclpy.ok():
103            rclpy.shutdown()
104
105
106if __name__ == '__main__':
107    main()

6.1.7 灵巧手触觉数据(T2.1支持)

该示例中用到了 omnihand_touch,通过订阅话题/aima/hal/joint/hand/state来接收灵巧手指尖压力传感器的数据

注意

注意⚠️ :运行本示例前需要在机器人运控计算单元(PC1)使用aima em stop-app mc关闭原生运控模块,获取机器人控制权。注意确保机器人安全

  1#!/usr/bin/env python3
  2
  3import rclpy
  4from rclpy.node import Node
  5from rclpy.qos import QoSProfile, ReliabilityPolicy
  6from aimdk_msgs.msg import HandStateArray, HandCommandArray, HandType
  7
  8
  9class HandStateSubscriber(Node):
 10    def __init__(self):
 11        super().__init__('hand_state_subscriber')
 12
 13        # Create publisher
 14        self.publisher_ = self.create_publisher(
 15            HandCommandArray,
 16            '/aima/hal/joint/hand/command',
 17            10
 18        )
 19
 20        # Create QoS profile with BEST_EFFORT reliability
 21        qos_profile = QoSProfile(depth=10)
 22        qos_profile.reliability = ReliabilityPolicy.BEST_EFFORT
 23
 24        # Create subscriber
 25        self.subscription = self.create_subscription(
 26            HandStateArray,
 27            '/aima/hal/joint/hand/state',
 28            self.topic_callback,
 29            qos_profile
 30        )
 31
 32        # Create a timer to publish once per second
 33        self.timer = self.create_timer(1.0, self.publish_command)
 34
 35        self.get_logger().info(
 36            "Subscriber started, listening to /aima/hal/joint/hand/state topic..."
 37        )
 38
 39    def topic_callback(self, msg):
 40        """Callback function for handling incoming messages"""
 41        # Print message header information
 42        self.get_logger().info(
 43            f"Message received - Sequence: {msg.header.sequence}, "
 44            f"Timestamp: {msg.header.stamp.sec}.{msg.header.stamp.nanosec:09d}"
 45        )
 46
 47        # Print left hand touch sensor data
 48        self.print_touch_sensor_data("Left Hand", msg.left_touch_sensors)
 49
 50        # Print right hand touch sensor data
 51        self.print_touch_sensor_data("Right Hand", msg.right_touch_sensors)
 52
 53        print()
 54
 55    def print_touch_sensor_data(self, hand_name, sensor_data):
 56        """
 57        Print touch sensor data for a specific hand
 58
 59        Args:
 60            hand_name: Name of the hand (Left Hand/Right Hand)
 61            sensor_data: Touch sensor data structure
 62        """
 63        print(f"=== {hand_name} Touch Sensor Data ===")
 64
 65        # Print palm touch data
 66        print("Palm Touch Data (36 elements): ", end="")
 67        self.print_array(sensor_data.palm_touch_data)
 68
 69        # Print back of hand touch data
 70        print("Back of Hand Touch Data (36 elements): ", end="")
 71        self.print_array(sensor_data.back_of_hand_touch_data)
 72
 73        # Print finger touch data
 74        print("Thumb Touch Data (16 elements): ", end="")
 75        self.print_array(sensor_data.thumb_touch_data)
 76
 77        print("Index Finger Touch Data (16 elements): ", end="")
 78        self.print_array(sensor_data.index_finger_touch_data)
 79
 80        print("Middle Finger Touch Data (16 elements): ", end="")
 81        self.print_array(sensor_data.middle_finger_touch_data)
 82
 83        print("Ring Finger Touch Data (16 elements): ", end="")
 84        self.print_array(sensor_data.ring_finger_touch_data)
 85
 86        print("Little Finger Touch Data (16 elements): ", end="")
 87        self.print_array(sensor_data.little_finger_touch_data)
 88
 89    def print_array(self, arr):
 90        """
 91        Print array of uint8_t elements
 92
 93        Args:
 94            arr: Array to print (list or tuple)
 95        """
 96        print("[", end="")
 97        for i, val in enumerate(arr):
 98            print(f"{val:3d}", end="")
 99            if i < len(arr) - 1:
100                print(" ", end="")
101        print("]")
102
103    def publish_command(self):
104        """Publish hand command message"""
105        message = HandCommandArray()
106
107        # Set header
108        message.header.stamp = self.get_clock().now().to_msg()
109        message.header.frame_id = "hand_command"
110
111        # Set the hand type
112        # dexterous hands (1)
113        message.left_hand_type.value = HandType.NIMBLE_HANDS
114        # dexterous hands (1)
115        message.right_hand_type.value = HandType.NIMBLE_HANDS
116
117        # Create hand command arrays
118        from aimdk_msgs.msg import HandCommand
119
120        # Joint names for readability, mirroring the /aima/hal/joint/hand/state feedback (URDF names).
121        # The driver routes by array index/order (see endeffector.md), not by these name strings.
122        left_names = ["L_thumb_roll_joint", "L_thumb_abad_joint", "L_thumb_mcp_joint",
123                      "L_index_abad_joint", "L_index_pip_joint", "L_middle_pip_joint",
124                      "L_ring_abad_joint", "L_ring_pip_joint", "L_pinky_abad_joint", "L_pinky_pip_joint"]
125        right_names = [n.replace("L_", "R_", 1) for n in left_names]
126
127        def build_cmd(name):
128            cmd = HandCommand()
129            cmd.name = name
130            cmd.position = 0.0
131            cmd.velocity = 0.1
132            cmd.acceleration = 0.0
133            cmd.deceleration = 0.0
134            cmd.effort = 0.0
135            return cmd
136
137        message.left_hands = [build_cmd(n) for n in left_names]
138        message.right_hands = [build_cmd(n) for n in right_names]
139
140        # Publish the message
141        self.publisher_.publish(message)
142
143        self.get_logger().info("Published hand command")
144
145
146def main(args=None):
147    rclpy.init(args=args)
148    node = HandStateSubscriber()
149    try:
150        rclpy.spin(node)
151    except KeyboardInterrupt:
152        pass
153    finally:
154        node.destroy_node()
155        if rclpy.ok():
156            rclpy.shutdown()
157
158
159if __name__ == '__main__':
160    main()

压力传感器位于五个手指的指尖、手心、手背。需要给灵巧手发送至少一个运动指令,才能获得对应的压力数据上报。

6.1.8 注册二开输入源

对于v0.7之后的版本,在实现对MC的控制前,需要先注册输入源。该示例中通过/aimdk_5Fmsgs/srv/SetMcInputSource服务来注册二开的输入源,让MC感知到,只有注册过输入源后才能实现机器人的速度控制

  1#!/usr/bin/env python3
  2
  3import rclpy
  4import rclpy.logging
  5from rclpy.node import Node
  6
  7from aimdk_msgs.srv import SetMcInputSource
  8from aimdk_msgs.msg import RequestHeader, McInputAction
  9
 10ACTION_NAMES = {
 11    McInputAction.INPUTACTION_ADD: "ADD",
 12    McInputAction.INPUTACTION_MODIFY: "MODIFY",
 13    McInputAction.INPUTACTION_DELETE: "DELETE",
 14    McInputAction.INPUTACTION_ENABLE: "ENABLE",
 15    McInputAction.INPUTACTION_DISABLE: "DISABLE",
 16}
 17
 18
 19class McInputClient(Node):
 20    def __init__(self):
 21        super().__init__('set_mc_input_source_client')
 22        self.client = self.create_client(
 23            SetMcInputSource, '/aimdk_5Fmsgs/srv/SetMcInputSource'
 24        )
 25
 26        self.get_logger().info('✅ SetMcInputSource client node created.')
 27
 28        # Wait for the service to become available
 29        while not self.client.wait_for_service(timeout_sec=2.0):
 30            self.get_logger().info('⏳ Service unavailable, waiting...')
 31
 32        self.get_logger().info('🟢 Service available, ready to send request.')
 33
 34    def send_request(self):
 35        req = SetMcInputSource.Request()
 36
 37        # header
 38        req.request.header = RequestHeader()
 39
 40        # action (e.g. ADD = register new input source)
 41        req.action = McInputAction()
 42        req.action.value = McInputAction.INPUTACTION_ADD  # ADD (1001)
 43
 44        # input source info
 45        req.input_source.name = 'node.set_mc_input_source'
 46        req.input_source.priority = 40
 47        req.input_source.timeout = 1000  # ms
 48
 49        # Send request and wait for response
 50        act_name = ACTION_NAMES.get(req.action.value, str(req.action.value))
 51        self.get_logger().info(
 52            f'📨 Sending input source request: {act_name} ({req.action.value}), '
 53            f'name={req.input_source.name}, priority={req.input_source.priority}'
 54        )
 55        for i in range(8):
 56            req.request.header.stamp = self.get_clock().now().to_msg()
 57            future = self.client.call_async(req)
 58            rclpy.spin_until_future_complete(
 59                self, future, timeout_sec=0.25)
 60
 61            if future.done():
 62                break
 63
 64            # retry as remote peer is NOT handled well by ROS
 65            self.get_logger().info(f'trying ... [{i}]')
 66
 67        if not future.done():
 68            self.get_logger().error('❌ Service call failed or timed out.')
 69            return False
 70
 71        response = future.result()
 72        ret_code = response.response.header.code
 73        task_id = response.response.task_id
 74
 75        if ret_code == 0:
 76            self.get_logger().info(
 77                f'✅ Input source set successfully. task_id={task_id}'
 78            )
 79            return True
 80        else:
 81            self.get_logger().error(
 82                f'❌ Input source set failed. ret_code={ret_code}, task_id={task_id} (duplicated ADD? or MODIFY/ENABLE/DISABLE for unknown source?)'
 83            )
 84            return False
 85
 86
 87def main(args=None):
 88    rclpy.init(args=args)
 89
 90    node = None
 91    try:
 92        node = McInputClient()
 93        ok = node.send_request()
 94        if not ok:
 95            node.get_logger().error('Input source request failed.')
 96    except KeyboardInterrupt:
 97        pass
 98    except Exception as e:
 99        rclpy.logging.get_logger('main').error(
100            f'Program exited with exception: {e}')
101
102    if node:
103        node.destroy_node()
104    if rclpy.ok():
105        rclpy.shutdown()
106
107
108if __name__ == '__main__':
109    main()

6.1.9 获取当前输入源

该示例中用到了GetCurrentInputSource服务,用于获取当前已注册的输入源信息,包括输入源名称、优先级和超时时间等信息。

 1#!/usr/bin/env python3
 2
 3import rclpy
 4from rclpy.node import Node
 5from aimdk_msgs.srv import GetCurrentInputSource
 6from aimdk_msgs.msg import CommonRequest
 7
 8
 9class GetCurrentInputSourceClient(Node):
10    def __init__(self):
11        super().__init__('get_current_input_source_client')
12        self.client = self.create_client(
13            GetCurrentInputSource,
14            '/aimdk_5Fmsgs/srv/GetCurrentInputSource'
15        )
16
17        self.get_logger().info('✅ GetCurrentInputSource client node created.')
18
19        # Wait for the service to become available
20        while not self.client.wait_for_service(timeout_sec=2.0):
21            self.get_logger().info('⏳ Service unavailable, waiting...')
22
23        self.get_logger().info('🟢 Service available, ready to send request.')
24
25    def send_request(self):
26        # Create request
27        req = GetCurrentInputSource.Request()
28        req.request = CommonRequest()
29
30        # Send request and wait for response
31        self.get_logger().info('📨 Sending request to get current input source')
32        for i in range(8):
33            req.request.header.stamp = self.get_clock().now().to_msg()
34            future = self.client.call_async(req)
35            rclpy.spin_until_future_complete(self, future, timeout_sec=0.25)
36
37            if future.done():
38                break
39
40            # retry as remote peer is NOT handled well by ROS
41            self.get_logger().info(f'trying ... [{i}]')
42
43        if not future.done():
44            self.get_logger().error('❌ Service call failed or timed out.')
45            return False
46
47        response = future.result()
48        ret_code = response.response.header.code
49        if ret_code == 0:
50            self.get_logger().info(
51                '✅ Current input source get successfully:')
52            self.get_logger().info(
53                f'Name: {response.input_source.name}')
54            self.get_logger().info(
55                f'Priority: {response.input_source.priority}')
56            self.get_logger().info(
57                f'Timeout: {response.input_source.timeout}')
58            return True
59        else:
60            self.get_logger().error(
61                f'❌ Current input source get failed, return code: {ret_code}')
62            return False
63
64
65def main(args=None):
66    rclpy.init(args=args)
67
68    node = None
69    try:
70        node = GetCurrentInputSourceClient()
71        success = node.send_request()
72    except KeyboardInterrupt:
73        pass
74    except Exception as e:
75        rclpy.logging.get_logger('main').error(
76            f'Program exited with exception: {e}')
77
78    if node:
79        node.destroy_node()
80    if rclpy.ok():
81        rclpy.shutdown()
82
83
84if __name__ == '__main__':
85    main()

使用说明

# 获取当前输入源信息
ros2 run py_examples get_current_input_source

输出示例

[INFO] [1779174540.534817548] [get_current_input_source_client]: ✅ GetCurrentInputSource client node created.
[INFO] [1779174541.371095227] [get_current_input_source_client]: 🟢 Service available, ready to send request.
[INFO] [1779174541.371216694] [get_current_input_source_client]: 📨 Sending request to get current input source
[INFO] [1779174541.382602338] [get_current_input_source_client]: ✅ Current input source get successfully:
[INFO] [1779174541.382692894] [get_current_input_source_client]: Name: node
[INFO] [1779174541.382713533] [get_current_input_source_client]: Priority: 40
[INFO] [1779174541.382726205] [get_current_input_source_client]: Timeout: 1000

注意事项

  • 确保GetCurrentInputSource服务正常运行

  • 需要在注册输入源之后才能获取到有效信息

  • 状态码为0表示查询成功

6.1.10 机器人走跑控制

该示例中用到了mc_locomotion_velocity,以下示例通过发布/aima/mc/locomotion/velocity话题控制机器人的行走,对于v0.7之后的版本,实现速度控制前需要注册输入源(该示例已实现注册输入源),具体注册步骤可参考代码。

进入稳定站立模式后执行本程序

  1#!/usr/bin/env python3
  2
  3import rclpy
  4from rclpy.node import Node
  5import time
  6import signal
  7import sys
  8
  9from aimdk_msgs.msg import McLocomotionVelocity, McInputAction, MessageHeader
 10from aimdk_msgs.srv import SetMcInputSource
 11
 12
 13class DirectVelocityControl(Node):
 14    def __init__(self):
 15        super().__init__('direct_velocity_control')
 16
 17        self.publisher = self.create_publisher(
 18            McLocomotionVelocity, '/aima/mc/locomotion/velocity', 10)
 19        self.client = self.create_client(
 20            SetMcInputSource, '/aimdk_5Fmsgs/srv/SetMcInputSource')
 21
 22        self.forward_velocity = 0.0
 23        self.lateral_velocity = 0.0
 24        self.angular_velocity = 0.0
 25
 26        # Maximum speed limits
 27        self.max_forward_speed = 1.0  # m/s
 28        self.min_forward_speed = 0.2  # m/s
 29
 30        self.max_lateral_speed = 1.0  # m/s
 31        self.min_lateral_speed = 0.2  # m/s
 32
 33        self.max_angular_speed = 1.0  # rad/s
 34        self.min_angular_speed = 0.1  # rad/s
 35
 36        self.timer = None
 37
 38        self.get_logger().info("Direct velocity control node started!")
 39
 40    def start_publish(self):
 41        if not self.timer:
 42            self.timer = self.create_timer(0.02, self.publish_velocity)
 43
 44    def _call_input_source(self, action_value):
 45        action_name = {
 46            McInputAction.INPUTACTION_ADD: "ADD",
 47            McInputAction.INPUTACTION_ENABLE: "ENABLE",
 48        }.get(action_value, str(action_value))
 49
 50        req = SetMcInputSource.Request()
 51        req.action.value = action_value
 52        req.input_source.name = "node.locomotion_velocity"
 53        req.input_source.priority = 40
 54        req.input_source.timeout = 1000
 55
 56        future = None
 57        for i in range(8):
 58            req.request.header.stamp = self.get_clock().now().to_msg()
 59            future = self.client.call_async(req)
 60            rclpy.spin_until_future_complete(self, future, timeout_sec=0.25)
 61            if future.done():
 62                break
 63            # retry as remote peer is NOT handled well by ROS
 64            self.get_logger().info(
 65                f"trying input source action {action_name} ({action_value})... [{i}]")
 66
 67        if future is None or not future.done():
 68            return False
 69        try:
 70            response = future.result()
 71        except Exception as e:
 72            self.get_logger().error(f"Service call exception: {str(e)}")
 73            return False
 74        return response is not None and response.response.header.code == 0
 75
 76    def register_input_source(self):
 77        self.get_logger().info("Registering input source...")
 78
 79        timeout_sec = 8.0
 80        start = self.get_clock().now().nanoseconds / 1e9
 81
 82        while not self.client.wait_for_service(timeout_sec=2.0):
 83            now = self.get_clock().now().nanoseconds / 1e9
 84            if now - start > timeout_sec:
 85                self.get_logger().error("Waiting for service timed out")
 86                return False
 87            self.get_logger().info("Waiting for input source service...")
 88
 89        # Try ADD first; if already registered, fall back to ENABLE
 90        if self._call_input_source(McInputAction.INPUTACTION_ADD):  # ADD (1001)
 91            self.get_logger().info("✅ Input source registered (ADD).")
 92            return True
 93        self.get_logger().info(
 94            "ADD failed (likely already registered), trying ENABLE...")
 95        if self._call_input_source(McInputAction.INPUTACTION_ENABLE):  # ENABLE (2001)
 96            self.get_logger().info("✅ Input source enabled.")
 97            return True
 98        self.get_logger().error(
 99            "❌ Failed to register input source via ADD or ENABLE.")
100        return False
101
102    def publish_velocity(self):
103        msg = McLocomotionVelocity()
104        msg.header = MessageHeader()
105        msg.header.stamp = self.get_clock().now().to_msg()
106        msg.source = "node.locomotion_velocity"
107        msg.forward_velocity = self.forward_velocity
108        msg.lateral_velocity = self.lateral_velocity
109        msg.angular_velocity = self.angular_velocity
110
111        self.publisher.publish(msg)
112
113        self.get_logger().info(
114            f"Publishing velocity: forward {self.forward_velocity:.2f} m/s, "
115            f"lateral {self.lateral_velocity:.2f} m/s, "
116            f"angular {self.angular_velocity:.2f} rad/s"
117        )
118
119    def set_forward(self, forward):
120        # check value range, mc has thresholds to start movement
121        if abs(forward) < 0.005:
122            self.forward_velocity = 0.0
123            return True
124        elif abs(forward) > self.max_forward_speed or abs(forward) < self.min_forward_speed:
125            raise ValueError("out of range")
126        else:
127            self.forward_velocity = forward
128            return True
129
130    def set_lateral(self, lateral):
131        # check value range, mc has thresholds to start movement
132        if abs(lateral) < 0.005:
133            self.lateral_velocity = 0.0
134            return True
135        elif abs(lateral) > self.max_lateral_speed or abs(lateral) < self.min_lateral_speed:
136            raise ValueError("out of range")
137        else:
138            self.lateral_velocity = lateral
139            return True
140
141    def set_angular(self, angular):
142        # check value range, mc has thresholds to start movement
143        if abs(angular) < 0.005:
144            self.angular_velocity = 0.0
145            return True
146        elif abs(angular) > self.max_angular_speed or abs(angular) < self.min_angular_speed:
147            raise ValueError("out of range")
148        else:
149            self.angular_velocity = angular
150            return True
151
152    def clear_velocity(self):
153        self.forward_velocity = 0.0
154        self.lateral_velocity = 0.0
155        self.angular_velocity = 0.0
156
157
158# Global node instance for signal handling
159global_node = None
160
161
162def signal_handler(sig, frame):
163    global global_node
164    if global_node is not None:
165        global_node.clear_velocity()
166        global_node.get_logger().info(
167            f"Received signal {sig}, clearing velocity and shutting down")
168    if rclpy.ok():
169        rclpy.shutdown()
170    sys.exit(0)
171
172
173def main():
174    global global_node
175    rclpy.init()
176
177    node = DirectVelocityControl()
178    global_node = node
179
180    signal.signal(signal.SIGINT, signal_handler)
181    signal.signal(signal.SIGTERM, signal_handler)
182
183    if not node.register_input_source():
184        node.get_logger().error("❌ Input source registration failed, exiting")
185        if rclpy.ok():
186            rclpy.shutdown()
187        return
188
189    # get and check control values
190    # notice that mc has thresholds to start movement
191    try:
192        # get input forward
193        forward = float(
194            input("Please enter forward velocity 0 or ±(0.2 ~ 1.0) m/s: "))
195        node.set_forward(forward)
196        # get input lateral
197        lateral = float(
198            input("Please enter lateral velocity 0 or ±(0.2 ~ 1.0) m/s: "))
199        node.set_lateral(lateral)
200        # get input angular
201        angular = float(
202            input("Please enter angular velocity 0 or ±(0.1 ~ 1.0) rad/s: "))
203        node.set_angular(angular)
204    except Exception as e:
205        node.get_logger().error(f"Invalid input: {e}")
206        if rclpy.ok():
207            rclpy.shutdown()
208        return
209
210    node.get_logger().info("Setting velocity, moving for 5 seconds")
211    node.start_publish()
212
213    start = node.get_clock().now()
214    while (node.get_clock().now() - start).nanoseconds / 1e9 < 5.0:
215        rclpy.spin_once(node, timeout_sec=0.1)
216        time.sleep(0.001)
217
218    node.clear_velocity()
219    node.get_logger().info("5-second motion finished, robot stopped")
220
221    try:
222        rclpy.spin(node)
223    except KeyboardInterrupt:
224        pass
225    finally:
226        if rclpy.ok():
227            rclpy.shutdown()
228
229
230if __name__ == '__main__':
231    main()

6.1.11 关节电机控制

本示例展示了如何使用ROS2和Ruckig库来控制机器人的关节运动。

注意

注意⚠️ :运行本示例前需要在机器人运控计算单元(PC1)使用aima em stop-app mc关闭原生运控模块,获取机器人控制权。注意确保机器人安全

!该示例代码直接对底层电机(即 HAL 层)进行控制,在运行程序前请检查代码中对关节的安全限位是否与所用机器人相符, 并确保安全!

机器人关节控制示例

这个示例展示了如何使用ROS2和Ruckig库来控制机器人的关节运动。示例实现了以下功能:

  1. 机器人关节模型定义

  2. 基于Ruckig的轨迹插补

  3. 多关节协同控制

  4. 实时位置、速度和加速度控制

示例功能说明

  1. 创建了四个控制器节点,分别控制:

    • 腿部x2(12个关节)

    • 腰部x1(3个关节)

    • 手臂x2(14个关节)

    • 头部x1(2个关节)

  2. 演示功能:

    • 每10秒让指定关节在正负0.5弧度之间摆动

    • 使用Ruckig库实现平滑的运动轨迹

    • 实时发布关节控制命令

自定义使用

  1. 添加新的控制逻辑:

    • 修改set_target_position函数

    • 添加新的控制回调函数

  2. 调整控制频率:

    • 修改timer的周期(当前为3s)和ruckig的dt(当前为0.002s)

  1#!/usr/bin/env python3
  2"""
  3Robot joint control example
  4This script implements a ROS2-based robot joint control system, using the Ruckig trajectory
  5planner to achieve smooth joint motion control.
  6
  7Main features:
  81. Supports controlling multiple joint areas (head, arm, waist, leg)
  92. Uses Ruckig for trajectory planning to ensure smooth motion
 103. Supports real-time control of joint position, velocity, and acceleration
 114. Provides joint limit and PID (stiffness/damping) parameter configuration
 12"""
 13
 14import rclpy
 15from rclpy.node import Node
 16from rclpy.qos import QoSProfile, ReliabilityPolicy, HistoryPolicy, DurabilityPolicy
 17from aimdk_msgs.msg import JointCommandArray, JointStateArray, JointCommand
 18import ruckig
 19from enum import Enum
 20from dataclasses import dataclass
 21from typing import List, Dict
 22from threading import Lock
 23
 24# QoS config: define ROS2 Quality of Service parameters
 25# Subscriber/Publisher QoS: best-effort reliability, keep last 10 messages
 26# (matches the robot-side joint command/state subscribers)
 27qos = QoSProfile(
 28    reliability=ReliabilityPolicy.BEST_EFFORT,
 29    history=HistoryPolicy.KEEP_LAST,
 30    depth=10,
 31    durability=DurabilityPolicy.VOLATILE
 32)
 33
 34
 35class JointArea(Enum):
 36    HEAD = 'HEAD'    # Head joints
 37    ARM = 'ARM'      # Arm joints
 38    WAIST = 'WAIST'  # Waist joints
 39    LEG = 'LEG'      # Leg joints
 40
 41
 42@dataclass
 43class JointInfo:
 44    # Joint information data class
 45    name: str           # Joint name
 46    lower_limit: float  # Joint lower angle limit
 47    upper_limit: float  # Joint upper angle limit
 48    kp: float           # Position control proportional gain
 49    kd: float           # Velocity control derivative gain
 50
 51
 52# Robot model configuration: define all joint parameters
 53robot_model: Dict[JointArea, List[JointInfo]] = {
 54    # Leg joint configuration
 55    JointArea.LEG: [
 56        # Left leg joints
 57        JointInfo("left_hip_pitch_joint", -2.704, 2.556, 40.0, 4.0),
 58        JointInfo("left_hip_roll_joint", -0.235, 2.906, 40.0, 4.0),
 59        JointInfo("left_hip_yaw_joint", -1.684, 3.430, 30.0, 3.0),
 60        JointInfo("left_knee_joint", 0.0000, 2.4073, 80.0, 8.0),
 61        JointInfo("left_ankle_pitch_joint", -0.803, 0.453, 40.0, 4.0),
 62        JointInfo("left_ankle_roll_joint", -0.2625, 0.2625, 20.0, 2.0),
 63        # Right leg joints
 64        JointInfo("right_hip_pitch_joint", -2.704, 2.556, 40.0, 4.0),
 65        JointInfo("right_hip_roll_joint", -2.906, 0.235, 40.0, 4.0),
 66        JointInfo("right_hip_yaw_joint", -3.430, 1.684, 30.0, 3.0),
 67        JointInfo("right_knee_joint", 0.0000, 2.4073, 80.0, 8.0),
 68        JointInfo("right_ankle_pitch_joint", -0.803, 0.453, 40.0, 4.0),
 69        JointInfo("right_ankle_roll_joint", -0.2625, 0.2625, 20.0, 2.0),
 70    ],
 71    # Waist joint configuration
 72    JointArea.WAIST: [
 73        JointInfo("waist_yaw_joint", -3.43, 2.382, 20.0, 4.0),
 74        JointInfo("waist_pitch_joint", -0.314, 0.314, 20.0, 4.0),
 75        JointInfo("waist_roll_joint", -0.488, 0.488, 20.0, 4.0),
 76    ],
 77    # Arm joint configuration
 78    JointArea.ARM: [
 79        # Left arm
 80        JointInfo("left_shoulder_pitch_joint", -3.08, 2.04, 20.0, 2.0),
 81        JointInfo("left_shoulder_roll_joint", -0.061, 2.993, 20.0, 2.0),
 82        JointInfo("left_shoulder_yaw_joint", -2.556, 2.556, 20.0, 2.0),
 83        JointInfo("left_elbow_joint", -2.3556, 0.0, 20.0, 2.0),
 84        JointInfo("left_wrist_yaw_joint", -2.556, 2.556, 20.0, 2.0),
 85        JointInfo("left_wrist_pitch_joint", -0.558, 0.558, 20.0, 2.0),
 86        JointInfo("left_wrist_roll_joint", -1.571, 0.724, 20.0, 2.0),
 87        # Right arm
 88        JointInfo("right_shoulder_pitch_joint", -3.08, 2.04, 20.0, 2.0),
 89        JointInfo("right_shoulder_roll_joint", -2.993, 0.061, 20.0, 2.0),
 90        JointInfo("right_shoulder_yaw_joint", -2.556, 2.556, 20.0, 2.0),
 91        JointInfo("right_elbow_joint", -2.3556, 0.0000, 20.0, 2.0),
 92        JointInfo("right_wrist_yaw_joint", -2.556, 2.556, 20.0, 2.0),
 93        JointInfo("right_wrist_pitch_joint", -0.558, 0.558, 20.0, 2.0),
 94        JointInfo("right_wrist_roll_joint", -0.724, 1.571, 20.0, 2.0),
 95    ],
 96    # Head joint configuration
 97    JointArea.HEAD: [
 98        JointInfo("head_yaw_joint", -0.366, 0.366, 20.0, 2.0),
 99        JointInfo("head_pitch_joint", -0.3838, 0.3838, 20.0, 2.0),
100    ],
101}
102
103
104class JointControllerNode(Node):
105    """
106    Joint controller node
107    Responsible for receiving joint states, using Ruckig for trajectory planning,
108    and publishing joint commands.
109    """
110
111    def __init__(self, node_name: str, sub_topic: str, pub_topic: str, area: JointArea, dofs: int):
112        """
113        Initialize joint controller
114        Args:
115            node_name: node name
116            sub_topic: topic name to subscribe (joint states)
117            pub_topic: topic name to publish (joint commands)
118            area: joint area (head/arm/waist/leg)
119            dofs: number of DOFs
120        """
121        super().__init__(node_name)
122        self.lock = Lock()
123        self.joint_info = robot_model[area]
124        self.dofs = dofs
125        self.ruckig = ruckig.Ruckig(dofs, 0.002)  # 2 ms control period
126        self.input = ruckig.InputParameter(dofs)
127        self.output = ruckig.OutputParameter(dofs)
128        self.ruckig_initialized = False
129
130        # Initialize trajectory parameters
131        self.input.current_position = [0.0] * dofs
132        self.input.current_velocity = [0.0] * dofs
133        self.input.current_acceleration = [0.0] * dofs
134
135        # Motion limits
136        self.input.max_velocity = [1.0] * dofs
137        self.input.max_acceleration = [1.0] * dofs
138        self.input.max_jerk = [25.0] * dofs
139
140        # ROS2 subscriber and publisher
141        self.sub = self.create_subscription(
142            JointStateArray,
143            sub_topic,
144            self.joint_state_callback,
145            qos
146        )
147        self.pub = self.create_publisher(
148            JointCommandArray,
149            pub_topic,
150            qos
151        )
152
153    def joint_state_callback(self, msg: JointStateArray):
154        """
155        Joint state callback
156        Receives and processes joint state messages
157        """
158        self.ruckig_initialized = True
159
160    def control_callback(self, joint_idx):
161        """
162        Control callback
163        Uses Ruckig for trajectory planning and publishes control commands
164        Args:
165            joint_idx: target joint index
166        """
167        # Run Ruckig until the target is reached
168        while self.ruckig.update(self.input, self.output) in [ruckig.Result.Working, ruckig.Result.Finished]:
169            # Update current state
170            self.input.current_position = self.output.new_position
171            self.input.current_velocity = self.output.new_velocity
172            self.input.current_acceleration = self.output.new_acceleration
173
174            # Check if target is reached
175            tolerance = 1e-6
176            current_p = self.output.new_position[joint_idx]
177            if abs(current_p - self.input.target_position[joint_idx]) < tolerance:
178                break
179
180            # Create and publish command
181            cmd = JointCommandArray()
182            for i, joint in enumerate(self.joint_info):
183                j = JointCommand()
184                j.name = joint.name
185                j.position = self.output.new_position[i]
186                j.velocity = self.output.new_velocity[i]
187                j.effort = 0.0
188                j.stiffness = joint.kp
189                j.damping = joint.kd
190                cmd.joints.append(j)
191
192            self.pub.publish(cmd)
193
194    def set_target_position(self, joint_name, position):
195        """
196        Set target joint position
197        Args:
198            joint_name: joint name
199            position: target position
200        """
201        p_s = [0.0] * self.dofs
202        joint_idx = 0
203        for i, joint in enumerate(self.joint_info):
204            if joint.name == joint_name:
205                p_s[i] = position
206                joint_idx = i
207        self.input.target_position = p_s
208        self.input.target_velocity = [0.0] * self.dofs
209        self.input.target_acceleration = [0.0] * self.dofs
210        self.control_callback(joint_idx)
211
212
213def main(args=None):
214    """
215    Main function
216    Initialize ROS2 node and start joint controller
217    """
218    rclpy.init(args=args)
219
220    # Create leg controller node
221    leg_node = JointControllerNode(
222        "leg_node",
223        "/aima/hal/joint/leg/state",
224        "/aima/hal/joint/leg/command",
225        JointArea.LEG,
226        12
227    )
228
229    # waist_node = JointControllerNode(
230    #     "waist_node",
231    #     "/aima/hal/joint/waist/state",
232    #     "/aima/hal/joint/waist/command",
233    #     JointArea.WAIST,
234    #     3
235    # )
236
237    # arm_node = JointControllerNode(
238    #     "arm_node",
239    #     "/aima/hal/joint/arm/state",
240    #     "/aima/hal/joint/arm/command",
241    #     JointArea.ARM,
242    #     14
243    # )
244
245    # head_node = JointControllerNode(
246    #     "head_node",
247    #     "/aima/hal/joint/head/state",
248    #     "/aima/hal/joint/head/command",
249    #     JointArea.HEAD,
250    #     2
251    # )
252
253    position = 0.8
254
255    # Only control the left leg joint. If you want to control a specific joint, assign it directly.
256    def timer_callback():
257        """
258        Timer callback
259        Periodically change target position to achieve oscillating motion
260        """
261        nonlocal position
262        position = -position
263        position = 1.3 + position
264        leg_node.set_target_position("left_knee_joint", position)
265
266    #     arm_node.set_target_position("left_shoulder_pitch_joint", position)
267    #     waist_node.set_target_position("waist_yaw_joint", position)
268    #     head_node.set_target_position("head_pitch_joint", position)
269
270    leg_node.create_timer(3.0, timer_callback)
271
272    # Multi-threaded executor
273    executor = rclpy.executors.MultiThreadedExecutor()
274    executor.add_node(leg_node)
275
276    # executor.add_node(waist_node)
277    # executor.add_node(arm_node)
278    # executor.add_node(head_node)
279
280    try:
281        executor.spin()
282    except KeyboardInterrupt:
283        pass
284    finally:
285        leg_node.destroy_node()
286        # waist_node.destroy_node()
287        # arm_node.destroy_node()
288        # head_node.destroy_node()
289        if rclpy.ok():
290            rclpy.shutdown()
291
292
293if __name__ == '__main__':
294    main()

6.1.12 键盘控制机器人

本示例实现了通过PC的键盘输入控制机器人的前进后退转弯的功能。

通过WASD控制机器人行走方向,增加/减少速度(±0.2 m/s),Q/E增加/减少角速度(±0.1 rad/s),ESC退出程序并释放终端资源,Space立即将速度归零,执行急停。

小心

注意:运行本示例前需要先使用手柄,将机器人切入稳定站立模式。(位控站立/走跑模式时按R2+X, 其他模式详见模式流转图进行操作),然后在机器人的终端界面使用:aima em stop-app rc关闭遥控器,防止通道占用。

实现键盘控制前需要注册输入源(该示例已实现注册输入源)

  1#!/usr/bin/env python3
  2
  3import rclpy
  4import rclpy.logging
  5from rclpy.node import Node
  6from aimdk_msgs.msg import McLocomotionVelocity, McInputAction, MessageHeader
  7from aimdk_msgs.srv import SetMcInputSource
  8import curses
  9
 10
 11class KeyboardVelocityController(Node):
 12    def __init__(self, stdscr):
 13        super().__init__('keyboard_velocity_controller')
 14        self.stdscr = stdscr
 15        self.forward_velocity = 0.0
 16        self.lateral_velocity = 0.0
 17        self.angular_velocity = 0.0
 18        self.step = 0.2
 19        self.angular_step = 0.1
 20
 21        self.publisher = self.create_publisher(
 22            McLocomotionVelocity, '/aima/mc/locomotion/velocity', 10)
 23        self.client = self.create_client(
 24            SetMcInputSource, '/aimdk_5Fmsgs/srv/SetMcInputSource')
 25
 26        if not self.register_input_source():
 27            self.get_logger().error("❌ Input source registration failed, exiting")
 28            raise RuntimeError("Failed to register input source")
 29
 30        # Configure curses
 31        curses.cbreak()
 32        curses.noecho()
 33        self.stdscr.keypad(True)
 34        self.stdscr.nodelay(True)
 35
 36        self.get_logger().info(
 37            "Control started: W/S forward/backward, A/D strafe, Q/E turn, Space stop, Esc exit")
 38
 39        # Timer: check keyboard every 50 ms
 40        self.timer = self.create_timer(0.05, self.check_key_and_publish)
 41
 42    def _call_input_source(self, action_value):
 43        action_name = {
 44            McInputAction.INPUTACTION_ADD: "ADD",
 45            McInputAction.INPUTACTION_ENABLE: "ENABLE",
 46        }.get(action_value, str(action_value))
 47
 48        req = SetMcInputSource.Request()
 49        req.action.value = action_value
 50        req.input_source.name = "node.keyboard"
 51        req.input_source.priority = 40
 52        req.input_source.timeout = 1000
 53
 54        future = None
 55        for i in range(8):
 56            req.request.header.stamp = self.get_clock().now().to_msg()
 57            future = self.client.call_async(req)
 58            rclpy.spin_until_future_complete(self, future, timeout_sec=0.25)
 59            if future.done():
 60                break
 61            # retry as remote peer is NOT handled well by ROS
 62            self.get_logger().info(
 63                f"trying input source action {action_name} ({action_value})... [{i}]")
 64
 65        if future is None or not future.done():
 66            return False
 67        try:
 68            resp = future.result()
 69        except Exception as e:
 70            self.get_logger().error(f"Service exception: {str(e)}")
 71            return False
 72        return resp is not None and resp.response.header.code == 0
 73
 74    def register_input_source(self):
 75        self.get_logger().info("Registering input source...")
 76
 77        timeout_sec = 8.0
 78        start = self.get_clock().now().nanoseconds / 1e9
 79
 80        while not self.client.wait_for_service(timeout_sec=2.0):
 81            now = self.get_clock().now().nanoseconds / 1e9
 82            if now - start > timeout_sec:
 83                self.get_logger().error("Waiting for service timed out")
 84                return False
 85            self.get_logger().info("Waiting for input source service...")
 86
 87        # Try ADD first; if already registered, fall back to ENABLE
 88        if self._call_input_source(McInputAction.INPUTACTION_ADD):  # ADD (1001)
 89            self.get_logger().info("✅ Input source registered (ADD).")
 90            return True
 91        self.get_logger().info(
 92            "ADD failed (likely already registered), trying ENABLE...")
 93        if self._call_input_source(McInputAction.INPUTACTION_ENABLE):  # ENABLE (2001)
 94            self.get_logger().info("✅ Input source enabled.")
 95            return True
 96        self.get_logger().error(
 97            "❌ Failed to register input source via ADD or ENABLE.")
 98        return False
 99
100    def check_key_and_publish(self):
101        try:
102            ch = self.stdscr.getch()
103        except Exception:
104            ch = -1
105
106        if ch != -1:
107            if ch == ord(' '):
108                self.forward_velocity = 0.0
109                self.lateral_velocity = 0.0
110                self.angular_velocity = 0.0
111            elif ch == ord('w'):
112                self.forward_velocity = min(
113                    self.forward_velocity + self.step, 1.0)
114            elif ch == ord('s'):
115                self.forward_velocity = max(
116                    self.forward_velocity - self.step, -1.0)
117            elif ch == ord('a'):
118                self.lateral_velocity = min(
119                    self.lateral_velocity + self.step, 1.0)
120            elif ch == ord('d'):
121                self.lateral_velocity = max(
122                    self.lateral_velocity - self.step, -1.0)
123            elif ch == ord('q'):
124                self.angular_velocity = min(
125                    self.angular_velocity + self.angular_step, 1.0)
126            elif ch == ord('e'):
127                self.angular_velocity = max(
128                    self.angular_velocity - self.angular_step, -1.0)
129            elif ch == 27:  # ESC
130                self.get_logger().info("Exiting control")
131                self.timer.cancel()
132                return
133
134        msg = McLocomotionVelocity()
135        msg.header = MessageHeader()
136        msg.header.stamp = self.get_clock().now().to_msg()
137        msg.source = "node.keyboard"
138        msg.forward_velocity = self.forward_velocity
139        msg.lateral_velocity = self.lateral_velocity
140        msg.angular_velocity = self.angular_velocity
141
142        self.publisher.publish(msg)
143
144        # Update UI
145        self.stdscr.clear()
146        self.stdscr.addstr(
147            0, 0, "W/S: Forward/Backward | A/D: Strafe | Q/E: Turn | Space: Stop | ESC: Exit")
148        self.stdscr.addstr(2, 0,
149                           f"Speed Status: Forward: {self.forward_velocity:.2f} m/s | "
150                           f"Lateral: {self.lateral_velocity:.2f} m/s | "
151                           f"Angular: {self.angular_velocity:.2f} rad/s")
152        self.stdscr.refresh()
153
154
155def curses_main(stdscr):
156    rclpy.init()
157    try:
158        node = KeyboardVelocityController(stdscr)
159        while not node.timer.is_canceled():
160            rclpy.spin_once(node, timeout_sec=0.1)
161    except (Exception, KeyboardInterrupt) as e:
162        rclpy.logging.get_logger("main").fatal(
163            f"Program exited with exception: {e}")
164    finally:
165        curses.endwin()
166        if rclpy.ok():
167            rclpy.shutdown()
168
169
170def main():
171    curses.wrapper(curses_main)
172
173
174if __name__ == '__main__':
175    main()

6.1.13 拍照

该示例中用到了take_photo,在运行节点程序前,修改需要拍照的相机话题,启动节点程序后,会创建/images/目录,将当前帧图像保存在这个目录下。

 1#!/usr/bin/env python3
 2import threading
 3import time
 4from pathlib import Path
 5
 6import rclpy
 7import rclpy.task
 8from rclpy.node import Node
 9from rclpy.qos import qos_profile_sensor_data
10from sensor_msgs.msg import Image
11from cv_bridge import CvBridge
12import cv2
13
14
15class SaveOneRawPy(Node):
16    def __init__(self):
17        super().__init__('save_one_image')
18
19        # parameter: image topic
20        self.declare_parameter(
21            'image_topic', '/aima/hal/sensor/stereo_head_front_left/rgb_image'
22        )
23        self.topic = self.get_parameter(
24            'image_topic').get_parameter_value().string_value
25
26        # save directory
27        self.save_dir = Path('images').resolve()
28        self.save_dir.mkdir(parents=True, exist_ok=True)
29
30        self._bridge = CvBridge()
31
32        self._future = rclpy.task.Future()
33        self._lock = threading.Lock()
34
35        # subscriber (sensor QoS)
36        self.sub = self.create_subscription(
37            Image,
38            self.topic,
39            self.image_cb,
40            qos_profile_sensor_data
41        )
42        self.get_logger().info(f'Subscribing to raw image: {self.topic}')
43        self.get_logger().info(f'Images will be saved to: {self.save_dir}')
44
45    def image_cb(self, msg: Image):
46        with self._lock:
47            if self._future.done():
48                return
49
50        try:
51            enc = msg.encoding.lower()
52            self.get_logger().info(f'Received image with encoding: {enc}')
53
54            img = self._bridge.imgmsg_to_cv2(
55                msg, desired_encoding='passthrough')
56
57            if enc == 'rgb8':
58                img = cv2.cvtColor(img, cv2.COLOR_RGB2BGR)
59            elif enc == 'mono8':
60                img = cv2.cvtColor(img, cv2.COLOR_GRAY2BGR)
61
62            ts_ms = int(time.time() * 1000)
63            out_path = self.save_dir / f'frame_{ts_ms}.png'
64
65            ok = cv2.imwrite(str(out_path), img)
66            if ok:
67                self.get_logger().info(
68                    f'Saved image: {out_path}  ({img.shape[1]}x{img.shape[0]})'
69                )
70                with self._lock:
71                    if not self._future.done():
72                        self._future.set_result(str(out_path))
73            else:
74                self.get_logger().error(f'cv2.imwrite failed: {out_path}')
75        except Exception as e:
76            self.get_logger().error(f'Failed to decode / save image: {e}')
77            with self._lock:
78                if not self._future.done():
79                    self._future.set_exception(e)
80
81
82def main():
83    rclpy.init()
84    node = SaveOneRawPy()
85
86    try:
87        rclpy.spin_until_future_complete(node, node._future)
88    except KeyboardInterrupt:
89        pass
90    finally:
91        node.destroy_node()
92        if rclpy.ok():
93            rclpy.shutdown()
94
95
96if __name__ == '__main__':
97    main()

6.1.14 相机推流示例集

该示例集提供了多种相机数据订阅和处理功能,支持深度相机、双目相机和单目相机的数据流订阅。
这些相机数据订阅 example 并没有实际的业务用途, 仅提供相机数据基础信息的打印;若您比较熟悉 ros2 使用,会发现 ros2 topic echo + ros2 topic hz 也能够实现 example 提供的功能。 您可以选择快速查阅 SDK 接口手册中话题列表直接快速进入自己模块的开发,也可以使用相机 example 作为脚手架加入自己的业务逻辑。 我们发布的传感器数据均为未经预处理(去畸变等)的原始数据,如果您需要查询传感器的详细信息(如分辨率、焦距等),请关注相应的内参(camera_info)话题。

深度相机数据订阅

该示例中用到了echo_camera_rgbd,通过订阅/aima/hal/sensor/rgbd_head_front/话题来接收机器人的深度相机数据,支持深度图、RGB图、压缩RGB图和相机内参等多种数据类型。

功能特点:

  • 支持多种数据类型订阅(深度图、RGB图、压缩图、相机内参)

  • 实时FPS统计和数据显示

  • 支持RGB图视频录制功能

  • 可配置的topic类型选择

支持的数据类型:

  • depth_image: 深度图像 (sensor_msgs/Image)

  • rgb_image: RGB图像 (sensor_msgs/Image)

  • rgb_image_compressed: 压缩RGB图像 (sensor_msgs/CompressedImage)

  • camera_info: 相机内参 (sensor_msgs/CameraInfo)

  1#!/usr/bin/env python3
  2"""
  3Head depth camera multi-topic subscription example
  4
  5Supports selecting the topic type to subscribe via startup parameter --ros-args -p topic_type:=<type>:
  6  - depth_image: Depth image (sensor_msgs/Image)
  7  - depth_camera_info: Camera intrinsic parameters (sensor_msgs/CameraInfo)
  8  - rgb_image: RGB image (sensor_msgs/Image)
  9  - rgb_image_compressed: RGB compressed image (sensor_msgs/CompressedImage)
 10  - rgb_camera_info: Camera intrinsic parameters (sensor_msgs/CameraInfo)
 11
 12Example:
 13  ros2 run py_examples echo_camera_rgbd --ros-args -p topic_type:=rgb_image
 14  ros2 run py_examples echo_camera_rgbd --ros-args -p topic_type:=rgb_camera_info
 15
 16Default topic_type is rgb_image
 17"""
 18
 19import rclpy
 20from rclpy.node import Node
 21from rclpy.qos import QoSProfile, QoSReliabilityPolicy, QoSHistoryPolicy
 22from sensor_msgs.msg import Image, CompressedImage, CameraInfo
 23from collections import deque
 24import cv2
 25import numpy as np
 26
 27
 28class CameraTopicEcho(Node):
 29    def __init__(self):
 30        super().__init__('camera_topic_echo')
 31
 32        # Select the topic type to subscribe
 33        self.declare_parameter('topic_type', 'rgb_image')
 34        self.declare_parameter('dump_video_path', '')
 35        self.declare_parameter('dump_video_fps', 30.0)
 36
 37        self.topic_type = self.get_parameter('topic_type').value
 38        self.dump_video_path = self.get_parameter('dump_video_path').value
 39        self.dump_video_fps = self.get_parameter('dump_video_fps').value
 40        self.video_writer = None
 41        self.frame_count = 0
 42
 43        # SensorDataQoS: BEST_EFFORT + VOLATILE
 44        qos = QoSProfile(
 45            reliability=QoSReliabilityPolicy.BEST_EFFORT,
 46            history=QoSHistoryPolicy.KEEP_LAST,
 47            depth=5
 48        )
 49
 50        # Create different subscribers based on topic_type
 51        if self.topic_type == "depth_image":
 52            self.topic_name = "/aima/hal/sensor/rgbd_head_front/depth_image"
 53            self.sub_image = self.create_subscription(
 54                Image, self.topic_name, self.cb_image, qos)
 55            self.get_logger().info(
 56                f"✅ Subscribing Depth Image: {self.topic_name}")
 57
 58        elif self.topic_type == "rgb_image":
 59            self.topic_name = "/aima/hal/sensor/rgbd_head_front/rgb_image"
 60            self.sub_image = self.create_subscription(
 61                Image, self.topic_name, self.cb_image, qos)
 62            self.get_logger().info(
 63                f"✅ Subscribing RGB Image: {self.topic_name}")
 64            if self.dump_video_path:
 65                self.get_logger().info(
 66                    f"📝 Will dump received images to video: {self.dump_video_path}")
 67
 68        elif self.topic_type == "rgb_image_compressed":
 69            self.topic_name = "/aima/hal/sensor/rgbd_head_front/rgb_image/compressed"
 70            self.sub_compressed = self.create_subscription(
 71                CompressedImage, self.topic_name, self.cb_compressed, qos)
 72            self.get_logger().info(
 73                f"✅ Subscribing CompressedImage: {self.topic_name}")
 74
 75        elif self.topic_type == "rgb_camera_info":
 76            self.topic_name = "/aima/hal/sensor/rgbd_head_front/rgb_camera_info"
 77            # RGB-D CameraInfo subscriptions are different from other cameras. The best_effort + volatile QoS is sufficient for 30Hz rgb_camera_info
 78            self.sub_camerainfo = self.create_subscription(
 79                CameraInfo, self.topic_name, self.cb_camerainfo, qos)
 80            self.get_logger().info(
 81                f"✅ Subscribing RGB CameraInfo: {self.topic_name}")
 82
 83        elif self.topic_type == "depth_camera_info":
 84            self.topic_name = "/aima/hal/sensor/rgbd_head_front/depth_camera_info"
 85            # RGB-D CameraInfo subscriptions are different from other cameras. The best_effort + volatile QoS is sufficient for 30Hz depth_camera_info
 86            self.sub_camerainfo = self.create_subscription(
 87                CameraInfo, self.topic_name, self.cb_camerainfo, qos)
 88            self.get_logger().info(
 89                f"✅ Subscribing Depth CameraInfo: {self.topic_name}")
 90
 91        else:
 92            self.get_logger().error(f"Unknown topic_type: {self.topic_type}")
 93            raise ValueError("Unknown topic_type")
 94
 95        # Internal state
 96        self.last_print = self.get_clock().now()
 97        self.print_allowed = False
 98        self.arrivals = deque()
 99
100    def update_arrivals(self):
101        """Calculate received FPS"""
102        now = self.get_clock().now()
103        self.arrivals.append(now)
104        while self.arrivals and (now - self.arrivals[0]).nanoseconds * 1e-9 > 1.0:
105            self.arrivals.popleft()
106
107    def get_fps(self):
108        """Get FPS"""
109        return len(self.arrivals)
110
111    def should_print(self, master=True):
112        """Control print frequency"""
113        if not master:
114            return self.print_allowed
115        now = self.get_clock().now()
116        if (now - self.last_print).nanoseconds * 1e-9 >= 1.0:
117            self.last_print = now
118            self.print_allowed = True
119        else:
120            self.print_allowed = False
121        return self.print_allowed
122
123    def cb_image(self, msg: Image):
124        """Image callback (Depth/RGB image)"""
125        self.update_arrivals()
126
127        if self.should_print():
128            stamp_sec = msg.header.stamp.sec + msg.header.stamp.nanosec * 1e-9
129            self.get_logger().info(
130                f"📸 {self.topic_type} received\n"
131                f"  • frame_id:        {msg.header.frame_id}\n"
132                f"  • stamp (sec):     {stamp_sec:.6f}\n"
133                f"  • encoding:        {msg.encoding}\n"
134                f"  • size (WxH):      {msg.width} x {msg.height}\n"
135                f"  • step (bytes/row):{msg.step}\n"
136                f"  • is_bigendian:    {msg.is_bigendian}\n"
137                f"  • recv FPS (1s):   {self.get_fps():.1f}"
138            )
139
140        # Only RGB image supports video dump
141        if self.topic_type == "rgb_image" and self.dump_video_path:
142            self.dump_image_to_video(msg)
143
144    def cb_compressed(self, msg: CompressedImage):
145        """CompressedImage callback"""
146        self.update_arrivals()
147
148        if self.should_print():
149            stamp_sec = msg.header.stamp.sec + msg.header.stamp.nanosec * 1e-9
150            self.get_logger().info(
151                f"🗜️  CompressedImage received\n"
152                f"  • frame_id:        {msg.header.frame_id}\n"
153                f"  • stamp (sec):     {stamp_sec:.6f}\n"
154                f"  • format:          {msg.format}\n"
155                f"  • data size:       {len(msg.data)}\n"
156                f"  • recv FPS (1s):   {self.get_fps():.1f}"
157            )
158
159    def cb_camerainfo(self, msg: CameraInfo):
160        """CameraInfo callback (camera intrinsic parameters)"""
161        # Camera info will only receive one frame, print it directly
162        stamp_sec = msg.header.stamp.sec + msg.header.stamp.nanosec * 1e-9
163
164        # Format D array
165        d_str = ", ".join([f"{d:.6f}" for d in msg.d])
166
167        # Format K matrix
168        k_str = ", ".join([f"{k:.6f}" for k in msg.k])
169
170        # Format P matrix
171        p_str = ", ".join([f"{p:.6f}" for p in msg.p])
172
173        self.get_logger().info(
174            f"📷 {self.topic_type} received\n"
175            f"  • frame_id:        {msg.header.frame_id}\n"
176            f"  • stamp (sec):     {stamp_sec:.6f}\n"
177            f"  • width x height:  {msg.width} x {msg.height}\n"
178            f"  • distortion_model:{msg.distortion_model}\n"
179            f"  • D: [{d_str}]\n"
180            f"  • K: [{k_str}]\n"
181            f"  • P: [{p_str}]\n"
182            f"  • binning_x: {msg.binning_x}\n"
183            f"  • binning_y: {msg.binning_y}\n"
184            f"  • roi: {{ x_offset: {msg.roi.x_offset}, y_offset: {msg.roi.y_offset}, height: {msg.roi.height}, width: {msg.roi.width}, do_rectify: {msg.roi.do_rectify} }}"
185        )
186
187    def dump_image_to_video(self, msg: Image):
188        """Dump RGB image to video file using mp4v encoding"""
189        if msg.encoding != "rgb8":
190            self.get_logger().warn(
191                f"image encoding not expected: {msg.encoding}")
192            return
193
194        np_array = np.frombuffer(msg.data, dtype=np.uint8).reshape(
195            msg.height, msg.width, 3)
196        cv_image = cv2.cvtColor(np_array, cv2.COLOR_RGB2BGR)
197
198        if self.video_writer is None:
199            fourcc = cv2.VideoWriter_fourcc(*'mp4v')
200            height, width = cv_image.shape[:2]
201            self.video_writer = cv2.VideoWriter(
202                self.dump_video_path, fourcc, self.dump_video_fps, (width, height))
203            if not self.video_writer.isOpened():
204                self.get_logger().error(
205                    f"Failed to open video writer: {self.dump_video_path}")
206                self.dump_video_path = ''
207                return
208            self.get_logger().info(
209                f"📹 Video writer initialized: {width}x{height} @ {self.dump_video_fps}fps")
210
211        self.video_writer.write(cv_image)
212        self.frame_count += 1
213
214        if self.frame_count % 30 == 0:
215            self.get_logger().info(
216                f"📹 Written {self.frame_count} frames to video")
217
218    def destroy_node(self):
219        if self.video_writer is not None:
220            self.video_writer.release()
221            self.get_logger().info(
222                f"📹 Video saved: {self.dump_video_path} ({self.frame_count} frames)")
223        super().destroy_node()
224
225
226def main(args=None):
227    rclpy.init(args=args)
228    try:
229        node = CameraTopicEcho()
230        rclpy.spin(node)
231    except KeyboardInterrupt:
232        pass
233    except Exception as e:
234        print(f"Error: {e}")
235    finally:
236        if 'node' in locals():
237            node.destroy_node()
238        if rclpy.ok():
239            rclpy.shutdown()
240
241
242if __name__ == '__main__':
243    main()

使用说明:

  1. 订阅RGB图像数据:

    ros2 run py_examples echo_camera_rgbd --ros-args -p topic_type:=rgb_image
    
  2. 订阅相机内参:

    ros2 run py_examples echo_camera_rgbd --ros-args -p topic_type:=rgb_camera_info
    ros2 run py_examples echo_camera_rgbd --ros-args -p topic_type:=depth_camera_info
    
  3. 录制RGB视频:

    # dump_video_path的值可改为其他路径, 注意提前创建该文件所在目录才能保存
    ros2 run py_examples echo_camera_rgbd --ros-args -p topic_type:=rgb_image -p dump_video_path:=$PWD/output.mp4
    

双目相机数据订阅

该示例中用到了echo_camera_stereo,通过订阅/aima/hal/sensor/stereo_head_front_*/话题来接收机器人的双目相机数据,支持左右相机的RGB图、压缩图和相机内参数据。

功能特点:

  • 支持左右相机独立数据订阅

  • 实时FPS统计和数据显示

  • 支持RGB图视频录制功能

  • 可配置的相机选择(左/右)

支持的数据类型:

  • left_rgb_image: 左相机RGB图像 (sensor_msgs/Image)

  • left_rgb_image_compressed: 左相机压缩RGB图像 (sensor_msgs/CompressedImage)

  • left_camera_info: 左相机内参 (sensor_msgs/CameraInfo)

  • right_rgb_image: 右相机RGB图像 (sensor_msgs/Image)

  • right_rgb_image_compressed: 右相机压缩RGB图像 (sensor_msgs/CompressedImage)

  • right_camera_info: 右相机内参 (sensor_msgs/CameraInfo)

  1#!/usr/bin/env python3
  2"""
  3Head stereo camera multi-topic subscription example
  4
  5Supports selecting the topic type to subscribe via startup parameter --ros-args -p topic_type:=<type>:
  6  - left_rgb_image: Left camera RGB image (sensor_msgs/Image)
  7  - left_rgb_image_compressed: Left camera RGB compressed image (sensor_msgs/CompressedImage)
  8  - left_camera_info: Left camera intrinsic parameters (sensor_msgs/CameraInfo)
  9  - right_rgb_image: Right camera RGB image (sensor_msgs/Image)
 10  - right_rgb_image_compressed: Right camera RGB compressed image (sensor_msgs/CompressedImage)
 11  - right_camera_info: Right camera intrinsic parameters (sensor_msgs/CameraInfo)
 12
 13Example:
 14  ros2 run py_examples echo_camera_stereo --ros-args -p topic_type:=left_rgb_image
 15  ros2 run py_examples echo_camera_stereo --ros-args -p topic_type:=right_rgb_image
 16  ros2 run py_examples echo_camera_stereo --ros-args -p topic_type:=left_camera_info
 17
 18Default topic_type is left_rgb_image
 19"""
 20
 21import rclpy
 22from rclpy.node import Node
 23from rclpy.qos import QoSProfile, QoSReliabilityPolicy, QoSDurabilityPolicy, QoSHistoryPolicy
 24from sensor_msgs.msg import Image, CompressedImage, CameraInfo
 25from collections import deque
 26import cv2
 27import numpy as np
 28
 29
 30class StereoCameraTopicEcho(Node):
 31    def __init__(self):
 32        super().__init__('stereo_camera_topic_echo')
 33
 34        # Select the topic type to subscribe
 35        self.declare_parameter('topic_type', 'left_rgb_image')
 36        self.declare_parameter('dump_video_path', '')
 37        self.declare_parameter('dump_video_fps', 10.0)
 38
 39        self.topic_type = self.get_parameter('topic_type').value
 40        self.dump_video_path = self.get_parameter('dump_video_path').value
 41        self.dump_video_fps = self.get_parameter('dump_video_fps').value
 42        self.video_writer = None
 43        self.frame_count = 0
 44
 45        # Set QoS parameters - use sensor data QoS
 46        qos = QoSProfile(
 47            reliability=QoSReliabilityPolicy.BEST_EFFORT,
 48            history=QoSHistoryPolicy.KEEP_LAST,
 49            depth=5,
 50            durability=QoSDurabilityPolicy.VOLATILE
 51        )
 52
 53        # Create different subscribers based on topic_type
 54        if self.topic_type == "left_rgb_image":
 55            self.topic_name = "/aima/hal/sensor/stereo_head_front_left/rgb_image"
 56            self.sub_image = self.create_subscription(
 57                Image, self.topic_name, self.cb_image, qos)
 58            self.get_logger().info(
 59                f"✅ Subscribing Left RGB Image: {self.topic_name}")
 60            if self.dump_video_path:
 61                self.get_logger().info(
 62                    f"📝 Will dump received images to video: {self.dump_video_path}")
 63
 64        elif self.topic_type == "left_rgb_image_compressed":
 65            self.topic_name = "/aima/hal/sensor/stereo_head_front_left/rgb_image/compressed"
 66            self.sub_compressed = self.create_subscription(
 67                CompressedImage, self.topic_name, self.cb_compressed, qos)
 68            self.get_logger().info(
 69                f"✅ Subscribing Left CompressedImage: {self.topic_name}")
 70
 71        elif self.topic_type == "left_camera_info":
 72            self.topic_name = "/aima/hal/sensor/stereo_head_front_left/camera_info"
 73            # CameraInfo subscription must use reliable + transient_local QoS to receive historical messages (even if only one frame is published)
 74            camera_qos = QoSProfile(
 75                reliability=QoSReliabilityPolicy.RELIABLE,
 76                history=QoSHistoryPolicy.KEEP_LAST,
 77                depth=1,
 78                durability=QoSDurabilityPolicy.TRANSIENT_LOCAL
 79            )
 80            self.sub_camerainfo = self.create_subscription(
 81                CameraInfo, self.topic_name, self.cb_camerainfo, camera_qos)
 82            self.get_logger().info(
 83                f"✅ Subscribing Left CameraInfo (with transient_local): {self.topic_name}")
 84
 85        elif self.topic_type == "right_rgb_image":
 86            self.topic_name = "/aima/hal/sensor/stereo_head_front_right/rgb_image"
 87            self.sub_image = self.create_subscription(
 88                Image, self.topic_name, self.cb_image, qos)
 89            self.get_logger().info(
 90                f"✅ Subscribing Right RGB Image: {self.topic_name}")
 91            if self.dump_video_path:
 92                self.get_logger().info(
 93                    f"📝 Will dump received images to video: {self.dump_video_path}")
 94
 95        elif self.topic_type == "right_rgb_image_compressed":
 96            self.topic_name = "/aima/hal/sensor/stereo_head_front_right/rgb_image/compressed"
 97            self.sub_compressed = self.create_subscription(
 98                CompressedImage, self.topic_name, self.cb_compressed, qos)
 99            self.get_logger().info(
100                f"✅ Subscribing Right CompressedImage: {self.topic_name}")
101
102        elif self.topic_type == "right_camera_info":
103            self.topic_name = "/aima/hal/sensor/stereo_head_front_right/camera_info"
104            # CameraInfo subscription must use reliable + transient_local QoS to receive historical messages (even if only one frame is published)
105            camera_qos = QoSProfile(
106                reliability=QoSReliabilityPolicy.RELIABLE,
107                history=QoSHistoryPolicy.KEEP_LAST,
108                depth=1,
109                durability=QoSDurabilityPolicy.TRANSIENT_LOCAL
110            )
111            self.sub_camerainfo = self.create_subscription(
112                CameraInfo, self.topic_name, self.cb_camerainfo, camera_qos)
113            self.get_logger().info(
114                f"✅ Subscribing Right CameraInfo (with transient_local): {self.topic_name}")
115
116        else:
117            self.get_logger().error(f"Unknown topic_type: {self.topic_type}")
118            raise ValueError("Unknown topic_type")
119
120        # Internal state
121        self.last_print = self.get_clock().now()
122        self.print_allowed = False
123        self.arrivals = deque()
124
125    def update_arrivals(self):
126        """Calculate received FPS"""
127        now = self.get_clock().now()
128        self.arrivals.append(now)
129        while self.arrivals and (now - self.arrivals[0]).nanoseconds * 1e-9 > 1.0:
130            self.arrivals.popleft()
131
132    def get_fps(self):
133        """Get FPS"""
134        return len(self.arrivals)
135
136    def should_print(self, master=True):
137        """Control print frequency"""
138        if not master:
139            return self.print_allowed
140        now = self.get_clock().now()
141        if (now - self.last_print).nanoseconds * 1e-9 >= 1.0:
142            self.last_print = now
143            self.print_allowed = True
144        else:
145            self.print_allowed = False
146        return self.print_allowed
147
148    def cb_image(self, msg: Image):
149        """Image callback (Left/Right camera RGB image)"""
150        self.update_arrivals()
151
152        if self.should_print():
153            stamp_sec = msg.header.stamp.sec + msg.header.stamp.nanosec * 1e-9
154            self.get_logger().info(
155                f"📸 {self.topic_type} received\n"
156                f"  • frame_id:        {msg.header.frame_id}\n"
157                f"  • stamp (sec):     {stamp_sec:.6f}\n"
158                f"  • encoding:        {msg.encoding}\n"
159                f"  • size (WxH):      {msg.width} x {msg.height}\n"
160                f"  • step (bytes/row):{msg.step}\n"
161                f"  • is_bigendian:    {msg.is_bigendian}\n"
162                f"  • recv FPS (1s):   {self.get_fps():.1f}"
163            )
164
165        # Only RGB images support video dump
166        if (self.topic_type in ["left_rgb_image", "right_rgb_image"]) and self.dump_video_path:
167            self.dump_image_to_video(msg)
168
169    def cb_compressed(self, msg: CompressedImage):
170        """CompressedImage callback (Left/Right camera RGB compressed image)"""
171        self.update_arrivals()
172
173        if self.should_print():
174            stamp_sec = msg.header.stamp.sec + msg.header.stamp.nanosec * 1e-9
175            self.get_logger().info(
176                f"🗜️  {self.topic_type} received\n"
177                f"  • frame_id:        {msg.header.frame_id}\n"
178                f"  • stamp (sec):     {stamp_sec:.6f}\n"
179                f"  • format:          {msg.format}\n"
180                f"  • data size:       {len(msg.data)}\n"
181                f"  • recv FPS (1s):   {self.get_fps():.1f}"
182            )
183
184    def cb_camerainfo(self, msg: CameraInfo):
185        """CameraInfo callback (Left/Right camera intrinsic parameters)"""
186        # Camera info will only receive one frame, print it directly
187        stamp_sec = msg.header.stamp.sec + msg.header.stamp.nanosec * 1e-9
188
189        # Format D array
190        d_str = ", ".join([f"{d:.6f}" for d in msg.d])
191
192        # Format K matrix
193        k_str = ", ".join([f"{k:.6f}" for k in msg.k])
194
195        # Format P matrix
196        p_str = ", ".join([f"{p:.6f}" for p in msg.p])
197
198        self.get_logger().info(
199            f"📷 {self.topic_type} received\n"
200            f"  • frame_id:        {msg.header.frame_id}\n"
201            f"  • stamp (sec):     {stamp_sec:.6f}\n"
202            f"  • width x height:  {msg.width} x {msg.height}\n"
203            f"  • distortion_model:{msg.distortion_model}\n"
204            f"  • D: [{d_str}]\n"
205            f"  • K: [{k_str}]\n"
206            f"  • P: [{p_str}]\n"
207            f"  • binning_x: {msg.binning_x}\n"
208            f"  • binning_y: {msg.binning_y}\n"
209            f"  • roi: {{ x_offset: {msg.roi.x_offset}, y_offset: {msg.roi.y_offset}, height: {msg.roi.height}, width: {msg.roi.width}, do_rectify: {msg.roi.do_rectify} }}"
210        )
211
212    def dump_image_to_video(self, msg: Image):
213        """Dump RGB image to video file using mp4v encoding"""
214        if msg.encoding != "rgb8":
215            self.get_logger().warn(
216                f"image encoding not expected: {msg.encoding}")
217            return
218
219        np_array = np.frombuffer(msg.data, dtype=np.uint8).reshape(
220            msg.height, msg.width, 3)
221        cv_image = cv2.cvtColor(np_array, cv2.COLOR_RGB2BGR)
222
223        if self.video_writer is None:
224            fourcc = cv2.VideoWriter_fourcc(*'mp4v')
225            height, width = cv_image.shape[:2]
226            self.video_writer = cv2.VideoWriter(
227                self.dump_video_path, fourcc, self.dump_video_fps, (width, height))
228            if not self.video_writer.isOpened():
229                self.get_logger().error(
230                    f"Failed to open video writer: {self.dump_video_path}")
231                self.dump_video_path = ''
232                return
233            self.get_logger().info(
234                f"📹 Video writer initialized: {width}x{height} @ {self.dump_video_fps}fps")
235
236        self.video_writer.write(cv_image)
237        self.frame_count += 1
238
239        if self.frame_count % 30 == 0:
240            self.get_logger().info(
241                f"📹 Written {self.frame_count} frames to video")
242
243    def destroy_node(self):
244        if self.video_writer is not None:
245            self.video_writer.release()
246            self.get_logger().info(
247                f"📹 Video saved: {self.dump_video_path} ({self.frame_count} frames)")
248        super().destroy_node()
249
250
251def main(args=None):
252    rclpy.init(args=args)
253    try:
254        node = StereoCameraTopicEcho()
255        rclpy.spin(node)
256    except KeyboardInterrupt:
257        pass
258    except Exception as e:
259        print(f"Error: {e}")
260    finally:
261        if 'node' in locals():
262            node.destroy_node()
263        if rclpy.ok():
264            rclpy.shutdown()
265
266
267if __name__ == '__main__':
268    main()

使用说明:

  1. 订阅左相机RGB图像:

    ros2 run py_examples echo_camera_stereo --ros-args -p topic_type:=left_rgb_image
    
  2. 订阅右相机RGB图像:

    ros2 run py_examples echo_camera_stereo --ros-args -p topic_type:=right_rgb_image
    
  3. 订阅左相机内参:

    ros2 run py_examples echo_camera_stereo --ros-args -p topic_type:=left_camera_info
    
  4. 录制左相机视频:

    # dump_video_path的值可改为其他路径, 注意提前创建该文件所在目录才能保存
    ros2 run py_examples echo_camera_stereo --ros-args -p topic_type:=left_rgb_image -p dump_video_path:=$PWD/left_camera.mp4
    

头部后置单目相机数据订阅

该示例中用到了echo_camera_head_rear,通过订阅/aima/hal/sensor/rgb_head_rear/话题来接收机器人的头部后置单目相机数据,支持RGB图(及其mask处理)、压缩图和相机内参数据。

功能特点:

  • 支持头部后置相机数据订阅

  • 实时FPS统计和数据显示

  • 支持RGB图视频录制功能及把手遮挡区域mask处理

  • 可配置的topic类型选择

  • 基于软件标记识别相机安装方向,对图像旋转补偿

支持的数据类型:

  • rgb_image: RGB图像 (sensor_msgs/Image)

  • rgb_image_compressed: 压缩RGB图像 (sensor_msgs/CompressedImage)

  • camera_info: 相机内参 (sensor_msgs/CameraInfo)

  1#!/usr/bin/env python3
  2"""
  3Head rear monocular camera multi-topic subscription example
  4
  5Supports selecting the topic type to subscribe via startup parameter --ros-args -p topic_type:=<type>:
  6  - rgb_image: RGB image (sensor_msgs/Image)
  7  - rgb_image_compressed: RGB compressed image (sensor_msgs/CompressedImage)
  8  - camera_info: Camera intrinsic parameters (sensor_msgs/CameraInfo)
  9
 10Example:
 11  ros2 run py_examples echo_camera_head_rear --ros-args -p topic_type:=rgb_image
 12  ros2 run py_examples echo_camera_head_rear --ros-args -p topic_type:=rgb_image_compressed
 13  ros2 run py_examples echo_camera_head_rear --ros-args -p topic_type:=camera_info
 14
 15Default topic_type is rgb_image
 16
 17Note: Some rear cameras are rotated by 90 degrees. This example automatically
 18detects them and applies rotation compensation when needed.
 19"""
 20
 21import rclpy
 22from rclpy.node import Node
 23from rclpy.qos import QoSProfile, QoSReliabilityPolicy, QoSDurabilityPolicy, QoSHistoryPolicy
 24from sensor_msgs.msg import Image, CompressedImage, CameraInfo
 25from collections import deque
 26import os
 27import cv2
 28import numpy as np
 29import re
 30
 31REAR_ROTATION_PATTERN = re.compile(r't3[d.]5', re.IGNORECASE)
 32
 33
 34class HeadRearCameraTopicEcho(Node):
 35    def __init__(self):
 36        super().__init__('head_rear_camera_topic_echo')
 37
 38        self.declare_parameter('topic_type', 'rgb_image')
 39        self.declare_parameter('dump_video_path', '')
 40        self.declare_parameter('dump_video_fps', 10.0)
 41        self.declare_parameter('with_mask', False)
 42        self.declare_parameter('auto_rotate', True)
 43
 44        self.topic_type = self.get_parameter('topic_type').value
 45        self.dump_video_path = self.get_parameter('dump_video_path').value
 46        self.dump_video_fps = self.get_parameter('dump_video_fps').value
 47        self.with_mask = self.get_parameter('with_mask').value
 48        self.auto_rotate = self.get_parameter('auto_rotate').value
 49        self.mask_image = None
 50        self.need_rotation = False
 51        self.hw_version = None
 52        self.video_writer = None
 53        self.frame_count = 0
 54
 55        qos = QoSProfile(
 56            reliability=QoSReliabilityPolicy.BEST_EFFORT,
 57            history=QoSHistoryPolicy.KEEP_LAST,
 58            depth=5,
 59            durability=QoSDurabilityPolicy.VOLATILE
 60        )
 61
 62        if self.auto_rotate:
 63            self._detect_rotate_from_file()
 64
 65        if self.with_mask and self.dump_video_path:
 66            mask_file = 'rgb_head_rear_mask_t35.png' if self.need_rotation else 'rgb_head_rear_mask.png'
 67            mask_path = os.path.join(
 68                os.path.dirname(__file__), 'data', mask_file)
 69            self.mask_image = cv2.imread(mask_path, cv2.IMREAD_GRAYSCALE)
 70            if self.mask_image is None:
 71                self.get_logger().error(
 72                    f"Failed to load mask file: {mask_path}")
 73                raise ValueError("Failed to load mask file")
 74            self.get_logger().info(f"🎭 Loaded mask: {mask_file}")
 75
 76        if self.topic_type == "rgb_image":
 77            self.topic_name = "/aima/hal/sensor/rgb_head_rear/rgb_image"
 78            self.sub_image = self.create_subscription(
 79                Image, self.topic_name, self.cb_image, qos)
 80            self.get_logger().info(
 81                f"✅ Subscribing RGB Image: {self.topic_name}")
 82            if self.dump_video_path:
 83                mask_state = "with mask" if self.with_mask else "without mask"
 84                rotation_state = "with rotation compensation" if self.need_rotation else "without rotation"
 85                self.get_logger().info(
 86                    f"📝 Will dump received images {mask_state} {rotation_state} to video: {self.dump_video_path}")
 87
 88        elif self.topic_type == "rgb_image_compressed":
 89            self.topic_name = "/aima/hal/sensor/rgb_head_rear/rgb_image/compressed"
 90            self.sub_compressed = self.create_subscription(
 91                CompressedImage, self.topic_name, self.cb_compressed, qos)
 92            self.get_logger().info(
 93                f"✅ Subscribing CompressedImage: {self.topic_name}")
 94
 95        elif self.topic_type == "camera_info":
 96            self.topic_name = "/aima/hal/sensor/rgb_head_rear/camera_info"
 97            camera_qos = QoSProfile(
 98                reliability=QoSReliabilityPolicy.RELIABLE,
 99                history=QoSHistoryPolicy.KEEP_LAST,
100                depth=1,
101                durability=QoSDurabilityPolicy.TRANSIENT_LOCAL
102            )
103            self.sub_camerainfo = self.create_subscription(
104                CameraInfo, self.topic_name, self.cb_camerainfo, camera_qos)
105            self.get_logger().info(
106                f"✅ Subscribing CameraInfo (with transient_local): {self.topic_name}")
107
108        else:
109            self.get_logger().error(f"Unknown topic_type: {self.topic_type}")
110            raise ValueError("Unknown topic_type")
111
112        self.last_print = self.get_clock().now()
113        self.print_allowed = False
114        self.arrivals = deque()
115
116    def _detect_rotate_from_file(self):
117        try:
118            hw_file = '/agibot/data/info/hardware_version'
119            if os.path.exists(hw_file):
120                with open(hw_file, 'r') as f:
121                    self.hw_version = f.read().strip()
122                self.get_logger().info(
123                    f"📋 Hardware version from file: '{self.hw_version}'")
124                self._check_rotation(self.hw_version)
125            else:
126                self.get_logger().warn(
127                    f"⚠️  Hardware version file not found: {hw_file}")
128        except Exception as e:
129            self.get_logger().warn(
130                f"⚠️  Failed to read hardware version from file: {e}")
131
132    def _check_rotation(self, version: str):
133        if REAR_ROTATION_PATTERN.search(version):
134            self.need_rotation = True
135            self.get_logger().info(
136                "🔄 Rotation detected: Rear camera rotation compensation enabled (90° counter-clockwise)"
137            )
138        else:
139            self.get_logger().info(
140                f"✅ No rotation detected ({version}): No rotation compensation needed"
141            )
142
143    def _rotate_image_90_ccw(self, image: np.ndarray) -> np.ndarray:
144        return cv2.rotate(image, cv2.ROTATE_90_COUNTERCLOCKWISE)
145
146    def update_arrivals(self):
147        now = self.get_clock().now()
148        self.arrivals.append(now)
149        while self.arrivals and (now - self.arrivals[0]).nanoseconds * 1e-9 > 1.0:
150            self.arrivals.popleft()
151
152    def get_fps(self):
153        return len(self.arrivals)
154
155    def should_print(self, master=True):
156        if not master:
157            return self.print_allowed
158        now = self.get_clock().now()
159        if (now - self.last_print).nanoseconds * 1e-9 >= 1.0:
160            self.last_print = now
161            self.print_allowed = True
162        else:
163            self.print_allowed = False
164        return self.print_allowed
165
166    def cb_image(self, msg: Image):
167        self.update_arrivals()
168
169        if self.should_print():
170            stamp_sec = msg.header.stamp.sec + msg.header.stamp.nanosec * 1e-9
171            rotation_note = " (rotated 90° CCW)" if self.need_rotation else ""
172            self.get_logger().info(
173                f"📸 {self.topic_type} received{rotation_note}\n"
174                f"  • frame_id:        {msg.header.frame_id}\n"
175                f"  • stamp (sec):     {stamp_sec:.6f}\n"
176                f"  • encoding:        {msg.encoding}\n"
177                f"  • size (WxH):      {msg.width} x {msg.height}\n"
178                f"  • step (bytes/row):{msg.step}\n"
179                f"  • is_bigendian:    {msg.is_bigendian}\n"
180                f"  • recv FPS (1s):   {self.get_fps():.1f}"
181            )
182
183        if self.topic_type == "rgb_image" and self.dump_video_path:
184            self.dump_image_to_video(msg)
185
186    def cb_compressed(self, msg: CompressedImage):
187        self.update_arrivals()
188
189        if self.should_print():
190            stamp_sec = msg.header.stamp.sec + msg.header.stamp.nanosec * 1e-9
191            self.get_logger().info(
192                f"🗜️  {self.topic_type} received\n"
193                f"  • frame_id:        {msg.header.frame_id}\n"
194                f"  • stamp (sec):     {stamp_sec:.6f}\n"
195                f"  • format:          {msg.format}\n"
196                f"  • data size:       {len(msg.data)}\n"
197                f"  • recv FPS (1s):   {self.get_fps():.1f}"
198            )
199
200    def cb_camerainfo(self, msg: CameraInfo):
201        stamp_sec = msg.header.stamp.sec + msg.header.stamp.nanosec * 1e-9
202
203        d_str = ", ".join([f"{d:.6f}" for d in msg.d])
204        k_str = ", ".join([f"{k:.6f}" for k in msg.k])
205        p_str = ", ".join([f"{p:.6f}" for p in msg.p])
206
207        self.get_logger().info(
208            f"📷 {self.topic_type} received\n"
209            f"  • frame_id:        {msg.header.frame_id}\n"
210            f"  • stamp (sec):     {stamp_sec:.6f}\n"
211            f"  • width x height:  {msg.width} x {msg.height}\n"
212            f"  • distortion_model:{msg.distortion_model}\n"
213            f"  • D: [{d_str}]\n"
214            f"  • K: [{k_str}]\n"
215            f"  • P: [{p_str}]\n"
216            f"  • binning_x: {msg.binning_x}\n"
217            f"  • binning_y: {msg.binning_y}\n"
218            f"  • roi: {{ x_offset: {msg.roi.x_offset}, y_offset: {msg.roi.y_offset}, height: {msg.roi.height}, width: {msg.roi.width}, do_rectify: {msg.roi.do_rectify} }}"
219        )
220
221    def dump_image_to_video(self, msg: Image):
222        """Dump RGB image to video file using mp4v encoding"""
223        try:
224            if msg.encoding != "rgb8":
225                self.get_logger().warn(
226                    f"image encoding not expected: {msg.encoding}")
227                return
228
229            np_array = np.frombuffer(msg.data, dtype=np.uint8).reshape(
230                msg.height, msg.width, 3)
231            cv_image = cv2.cvtColor(np_array, cv2.COLOR_RGB2BGR)
232
233            if self.need_rotation:
234                cv_image = self._rotate_image_90_ccw(cv_image)
235
236            if self.mask_image is not None:
237                mask_resized = cv2.resize(
238                    self.mask_image, (cv_image.shape[1], cv_image.shape[0]))
239                cv_image[mask_resized == 0] = 0
240
241            if self.video_writer is None:
242                fourcc = cv2.VideoWriter_fourcc(*'mp4v')
243                height, width = cv_image.shape[:2]
244                self.video_writer = cv2.VideoWriter(
245                    self.dump_video_path, fourcc, self.dump_video_fps, (width, height))
246                if not self.video_writer.isOpened():
247                    self.get_logger().error(
248                        f"Failed to open video writer: {self.dump_video_path}")
249                    self.dump_video_path = ''
250                    return
251                self.get_logger().info(
252                    f"📹 Video writer initialized: {width}x{height} @ {self.dump_video_fps}fps")
253
254            self.video_writer.write(cv_image)
255            self.frame_count += 1
256
257            if self.frame_count % 30 == 0:
258                self.get_logger().info(
259                    f"📹 Written {self.frame_count} frames to video")
260
261        except Exception as e:
262            self.get_logger().error(f"Failed to process image for video: {e}")
263
264    def destroy_node(self):
265        if self.video_writer is not None:
266            self.video_writer.release()
267            self.get_logger().info(
268                f"� Video saved: {self.dump_video_path} ({self.frame_count} frames)")
269        super().destroy_node()
270
271
272def main(args=None):
273    rclpy.init(args=args)
274    node = None
275    try:
276        node = HeadRearCameraTopicEcho()
277        rclpy.spin(node)
278    except KeyboardInterrupt:
279        pass
280    except Exception as e:
281        print(f"Error: {e}")
282    finally:
283        if node is not None:
284            node.destroy_node()
285        if rclpy.ok():
286            rclpy.shutdown()
287
288
289if __name__ == '__main__':
290    main()

使用说明:

  1. 订阅RGB图像数据:

    ros2 run py_examples echo_camera_head_rear --ros-args -p topic_type:=rgb_image
    
  2. 订阅压缩图像数据:

    ros2 run py_examples echo_camera_head_rear --ros-args -p topic_type:=rgb_image_compressed
    
  3. 订阅相机内参:

    ros2 run py_examples echo_camera_head_rear --ros-args -p topic_type:=camera_info
    
  4. 录制视频:

    # dump_video_path的值可改为其他路径, 注意提前创建该文件所在目录才能保存
    ros2 run py_examples echo_camera_head_rear --ros-args -p topic_type:=rgb_image -p dump_video_path:=$PWD/rear_camera.mp4
    
  5. 录制视频并对把手遮挡区域mask处理:

    # dump_video_path的值可改为其他路径, 注意提前创建该文件所在目录才能保存
    ros2 run py_examples echo_camera_head_rear --ros-args -p topic_type:=rgb_image -p with_mask:=true -p dump_video_path:=$PWD/rear_camera.mp4
    
  6. 禁用自动旋转补偿(获取原始图像):

    ros2 run py_examples echo_camera_head_rear --ros-args -p topic_type:=rgb_image -p auto_rotate:=false
    

应用场景:

  • 人脸识别和追踪

  • 目标检测和识别

  • 视觉SLAM

  • 图像处理和计算机视觉算法开发

  • 机器人视觉导航

6.1.15 头部触摸传感器数据订阅

该示例中用到了 echo_head_touch_sensor,通过订阅/aima/hal/sensor/touch_head话题来接收机器人的头部触摸传感器的反馈数据。

功能特点:

  • 订阅了头部传感器的反馈数据,当头部被触摸时候,输出会从 IDLE->TOUCH

 1#!/usr/bin/env python3
 2"""
 3Head touch state subscription example
 4"""
 5
 6import rclpy
 7from rclpy.node import Node
 8from aimdk_msgs.msg import TouchState
 9
10
11class TouchStateSubscriber(Node):
12    def __init__(self):
13        super().__init__('touch_state_subscriber')
14
15        # touch event types
16        self.event_type_map = {
17            TouchState.UNKNOWN: "UNKNOWN",
18            TouchState.IDLE: "IDLE",
19            TouchState.TOUCH: "TOUCH",
20        }
21
22        # create subscriber
23        self.subscription = self.create_subscription(
24            TouchState,
25            '/aima/hal/sensor/touch_head',
26            self.touch_callback,
27            10
28        )
29
30        self.get_logger().info(
31            'TouchState subscriber started, listening to /aima/hal/sensor/touch_head')
32
33    def touch_callback(self, msg):
34        event_str = self.event_type_map.get(
35            msg.event_type, "INVALID")
36
37        self.get_logger().info(f'Timestamp: {msg.header.stamp.sec}.{msg.header.stamp.nanosec:09d}, '
38                               f'Event: {event_str} ({msg.event_type})')
39
40
41def main(args=None):
42    rclpy.init(args=args)
43    node = TouchStateSubscriber()
44    try:
45        rclpy.spin(node)
46    except KeyboardInterrupt:
47        pass
48    finally:
49        node.destroy_node()
50        if rclpy.ok():
51            rclpy.shutdown()
52
53
54if __name__ == '__main__':
55    main()

使用说明:

ros2 run py_examples echo_head_touch_sensor

输出示例:

...
[INFO] [1779174766.897555028] [touch_state_subscriber]: Timestamp: 1779174766.892997567, Event: IDLE (1)
[INFO] [1779174766.905498343] [touch_state_subscriber]: Timestamp: 1779174766.902994523, Event: TOUCH (2)

6.1.16 激光雷达数据订阅

该示例中用到了echo_lidar_data,通过订阅/aima/hal/sensor/lidar_chest_front/话题来接收机器人的激光雷达数据,支持点云数据和IMU数据两种数据类型。

功能特点:

  • 支持激光雷达点云数据订阅

  • 支持激光雷达IMU数据订阅

  • 实时FPS统计和数据显示

  • 可配置的topic类型选择

  • 详细的数据字段信息输出

支持的数据类型:

  • PointCloud2: 激光雷达点云数据 (sensor_msgs/PointCloud2)

  • Imu: 激光雷达IMU数据 (sensor_msgs/Imu)

技术实现:

  • 使用SensorDataQoS配置(BEST_EFFORT + VOLATILE)

  • 支持点云字段信息解析和显示

  • 支持IMU四元数、角速度和线性加速度数据

  • 提供详细的调试日志输出

应用场景:

  • 激光雷达数据采集和分析

  • 点云数据处理和可视化

  • 机器人导航和定位

  • SLAM算法开发

  • 环境感知和建图

  1#!/usr/bin/env python3
  2"""
  3Chest LiDAR data subscription example
  4
  5Supports subscribing to the following topics:
  6  1. /aima/hal/sensor/lidar_chest_front/lidar_pointcloud
  7     - Data type: sensor_msgs/PointCloud2
  8     - frame_id: lidar_chest_front
  9     - child_frame_id: /
 10     - Content: LiDAR point cloud data
 11  2. /aima/hal/sensor/lidar_chest_front/imu
 12     - Data type: sensor_msgs/Imu
 13     - frame_id: lidar_imu_chest_front
 14     - Content: LiDAR IMU data
 15
 16You can select the topic type to subscribe via startup parameter --ros-args -p topic_type:=<type>:
 17  - pointcloud: subscribe to LiDAR point cloud
 18  - imu: subscribe to LiDAR IMU
 19Default topic_type is pointcloud
 20
 21Examples:
 22  ros2 run py_examples echo_lidar_data --ros-args -p topic_type:=pointcloud
 23  ros2 run py_examples echo_lidar_data --ros-args -p topic_type:=imu
 24"""
 25
 26import rclpy
 27from rclpy.node import Node
 28from rclpy.qos import QoSProfile, QoSReliabilityPolicy, QoSHistoryPolicy
 29from sensor_msgs.msg import PointCloud2, Imu
 30from collections import deque
 31
 32
 33class LidarChestEcho(Node):
 34    def __init__(self):
 35        super().__init__('lidar_chest_echo')
 36
 37        # Select the topic type to subscribe
 38        self.declare_parameter('topic_type', 'pointcloud')
 39        self.topic_type = self.get_parameter('topic_type').value
 40
 41        # SensorDataQoS: BEST_EFFORT + VOLATILE
 42        qos = QoSProfile(
 43            reliability=QoSReliabilityPolicy.BEST_EFFORT,
 44            history=QoSHistoryPolicy.KEEP_LAST,
 45            depth=5
 46        )
 47
 48        # Create different subscribers based on topic_type
 49        if self.topic_type == "pointcloud":
 50            self.topic_name = "/aima/hal/sensor/lidar_chest_front/lidar_pointcloud"
 51            self.sub_pointcloud = self.create_subscription(
 52                PointCloud2, self.topic_name, self.cb_pointcloud, qos)
 53            self.get_logger().info(
 54                f"✅ Subscribing LIDAR PointCloud2: {self.topic_name}")
 55
 56        elif self.topic_type == "imu":
 57            self.topic_name = "/aima/hal/sensor/lidar_chest_front/imu"
 58            self.sub_imu = self.create_subscription(
 59                Imu, self.topic_name, self.cb_imu, qos)
 60            self.get_logger().info(
 61                f"✅ Subscribing LIDAR IMU: {self.topic_name}")
 62
 63        else:
 64            self.get_logger().error(f"Unknown topic_type: {self.topic_type}")
 65            raise ValueError("Unknown topic_type")
 66
 67        # Internal state
 68        self.last_print = self.get_clock().now()
 69        self.arrivals = deque()
 70
 71    def update_arrivals(self):
 72        """Calculate received FPS"""
 73        now = self.get_clock().now()
 74        self.arrivals.append(now)
 75        while self.arrivals and (now - self.arrivals[0]).nanoseconds * 1e-9 > 1.0:
 76            self.arrivals.popleft()
 77
 78    def get_fps(self):
 79        """Get FPS"""
 80        return len(self.arrivals)
 81
 82    def should_print(self):
 83        """Control print frequency"""
 84        now = self.get_clock().now()
 85        if (now - self.last_print).nanoseconds * 1e-9 >= 1.0:
 86            self.last_print = now
 87            return True
 88        return False
 89
 90    def cb_pointcloud(self, msg: PointCloud2):
 91        """PointCloud2 callback (LiDAR point cloud)"""
 92        self.update_arrivals()
 93
 94        if self.should_print():
 95            stamp_sec = f"{msg.header.stamp.sec}.{msg.header.stamp.nanosec:09d}"
 96
 97            # Format fields info
 98            fields_str = " ".join(
 99                [f"{f.name}({int(f.datatype)})" for f in msg.fields])
100
101            self.get_logger().info(
102                f"🟢 LIDAR PointCloud2 received\n"
103                f"  • frame_id:        {msg.header.frame_id}\n"
104                f"  • stamp (sec):     {stamp_sec}\n"
105                f"  • width x height:  {msg.width} x {msg.height}\n"
106                f"  • point_step:      {msg.point_step}\n"
107                f"  • row_step:        {msg.row_step}\n"
108                f"  • fields:          {fields_str}\n"
109                f"  • is_bigendian:    {msg.is_bigendian}\n"
110                f"  • is_dense:        {msg.is_dense}\n"
111                f"  • data size:       {len(msg.data)}\n"
112                f"  • recv FPS (1s):   {self.get_fps():.1f}"
113            )
114
115    def cb_imu(self, msg: Imu):
116        """IMU callback (LiDAR IMU)"""
117        self.update_arrivals()
118
119        if self.should_print():
120            stamp_sec = f"{msg.header.stamp.sec}.{msg.header.stamp.nanosec:09d}"
121
122            self.get_logger().info(
123                f"🟢 LIDAR IMU received\n"
124                f"  • frame_id:        {msg.header.frame_id}\n"
125                f"  • stamp (sec):     {stamp_sec}\n"
126                f"  • orientation:     [{msg.orientation.x:.6f}, {msg.orientation.y:.6f}, {msg.orientation.z:.6f}, {msg.orientation.w:.6f}]\n"
127                f"  • angular_velocity:[{msg.angular_velocity.x:.6f}, {msg.angular_velocity.y:.6f}, {msg.angular_velocity.z:.6f}]\n"
128                f"  • linear_accel:    [{msg.linear_acceleration.x:.6f}, {msg.linear_acceleration.y:.6f}, {msg.linear_acceleration.z:.6f}]\n"
129                f"  • recv FPS (1s):   {self.get_fps():.1f}"
130            )
131
132
133def main(args=None):
134    rclpy.init(args=args)
135    try:
136        node = LidarChestEcho()
137        rclpy.spin(node)
138    except KeyboardInterrupt:
139        pass
140    except Exception as e:
141        print(f"Error: {e}")
142    finally:
143        if 'node' in locals():
144            node.destroy_node()
145        if rclpy.ok():
146            rclpy.shutdown()
147
148
149if __name__ == '__main__':
150    main()

使用说明:

# 订阅激光雷达点云数据
ros2 run py_examples echo_lidar_data --ros-args -p topic_type:=pointcloud

# 订阅激光雷达IMU数据
ros2 run py_examples echo_lidar_data --ros-args -p topic_type:=imu

输出示例:

...
[INFO] [1779175640.420801950] [lidar_chest_echo]: 🟢 LIDAR PointCloud2 received
  • frame_id:        lidar_chest_front
  • stamp (sec):     1779175640.315327000
  • width x height:  24779 x 1
  • point_step:      32
  • row_step:        792928
  • fields:          x(7) y(7) z(7) intensity(7) ring(4) timestamp(8)
  • is_bigendian:    False
  • is_dense:        True
  • data size:       792928
  • recv FPS (1s):   11.0

6.1.17 IMU数据订阅

该示例中用到了echo_imu_data,通过订阅IMU话题接收机器人的姿态、角速度与线性加速度数据,并打印各字段的协方差对角线信息。默认订阅胸部IMU,可通过参数切换到胯部IMU、Lidar IMU或深度相机IMU。

功能特点:

  • 订阅 sensor_msgs/Imu 数据,输出姿态四元数、角速度、线性加速度

  • 打印姿态/角速度/加速度的协方差对角线(-1 表示未知)

  • 统计接收间隔(recv_dt),便于观察实际到达频率

  • 通过 imu_topic 参数可切换不同 IMU 源

支持的话题:

  • /aima/hal/imu/chest/state:胸部IMU(默认)

  • /aima/hal/imu/torso/state:胯部IMU

  • /aima/hal/sensor/lidar_chest_front/imu:Lidar IMU

  • /aima/hal/sensor/rgbd_head_front/imu:深度相机 IMU

技术实现:

  • 使用 qos_profile_sensor_data(BEST_EFFORT + VOLATILE),与传感器数据默认 QoS 一致

 1#!/usr/bin/env python3
 2import time
 3import rclpy
 4from rclpy.node import Node
 5from rclpy.qos import qos_profile_sensor_data
 6from rclpy.time import Time
 7from sensor_msgs.msg import Imu
 8
 9
10class ImuPrinter(Node):
11    def __init__(self):
12        super().__init__('imu_printer')
13        # Allow overriding the topic name via a parameter
14        self.declare_parameter('imu_topic', '/aima/hal/imu/chest/state')
15        # Can be replaced with other IMU topic names.
16        # Waist IMU: /aima/hal/imu/torso/state
17        # LiDAR IMU: /aima/hal/sensor/lidar_chest_front/imu
18        self.topic = self.get_parameter(
19            'imu_topic').get_parameter_value().string_value
20
21        self._last_recv = None
22        # qos_profile_sensor_data = BestEffort / Volatile
23        self.sub = self.create_subscription(
24            Imu, self.topic, self.cb, qos_profile_sensor_data
25        )
26        self.get_logger().info(f'Subscribing IMU: {self.topic}')
27
28    def cb(self, msg: Imu):
29        # Compute receive interval (based on wall time, for observation only)
30        now = time.perf_counter()
31        dt_ms = 0.0 if self._last_recv is None else (
32            now - self._last_recv) * 1000.0
33        self._last_recv = now
34
35        t_sec = Time.from_msg(msg.header.stamp).nanoseconds / 1e9
36        q = msg.orientation
37        w = msg.angular_velocity
38        a = msg.linear_acceleration
39        oc = msg.orientation_covariance
40        wc = msg.angular_velocity_covariance
41        ac = msg.linear_acceleration_covariance
42
43        # Optional: print diagonal covariances (-1 means unknown)
44        self.get_logger().info(
45            (
46                f"stamp={t_sec:.6f}s  frame={msg.header.frame_id}  recv_dt={dt_ms:.3f}ms\n"
47                f"  orientation (x,y,z,w): [{q.x:.6f}, {q.y:.6f}, {q.z:.6f}, {q.w:.6f}]\n"
48                f"  angular_velocity (rad/s): [{w.x:.6f}, {w.y:.6f}, {w.z:.6f}]\n"
49                f"  linear_accel (m/s^2): [{a.x:.6f}, {a.y:.6f}, {a.z:.6f}]\n"
50                f"  cov_diag(ori, ang, acc): "
51                f"[{oc[0]:.6f},{oc[4]:.6f},{oc[8]:.6f} | "
52                f"{wc[0]:.6f},{wc[4]:.6f},{wc[8]:.6f} | "
53                f"{ac[0]:.6f},{ac[4]:.6f},{ac[8]:.6f}]"
54            )
55        )
56
57
58def main():
59    rclpy.init()
60    node = ImuPrinter()
61    try:
62        rclpy.spin(node)
63    except KeyboardInterrupt:
64        pass
65    finally:
66        node.destroy_node()
67        if rclpy.ok():
68            rclpy.shutdown()
69
70
71if __name__ == '__main__':
72    main()

使用说明:

# 订阅胸部IMU(默认)
ros2 run py_examples echo_imu_data

# 订阅胯部IMU
ros2 run py_examples echo_imu_data --ros-args -p imu_topic:=/aima/hal/imu/torso/state

# 订阅Lidar IMU
ros2 run py_examples echo_imu_data --ros-args -p imu_topic:=/aima/hal/sensor/lidar_chest_front/imu

输出示例:

[INFO] [1781764206.320174965] [imu_printer]: Subscribing IMU: /aima/hal/imu/chest/state
[INFO] [1781764206.318604000] [imu_printer]: stamp=1781764206.318604s  frame=base_link  recv_dt=2.039ms
  orientation (x,y,z,w): [0.087036, -0.264735, -0.019103, -0.960195]
  angular_velocity (rad/s): [-0.006531, 0.002874, 0.002391]
  linear_accel (m/s^2): [-5.013936, -1.548364, 8.280822]
  cov_diag(ori, ang, acc): [0.000000,0.000000,0.000000 | 0.000000,0.000000,0.000000 | 0.000000,0.000000,0.000000]

6.1.18 播放视频

该示例中用到了play_video,在运行节点程序前,需要先将视频上传到机器人的**交互计算单元(PC3)**上(用户可在其上创建一个用来存储视频的目录,如/var/tmp/videos/),然后将节点程序中的video_path改为需要播放视频的路径。

注意

⚠️ 请注意!交互计算单元(PC3)独立于二开程序所在的开发计算单元(PC2), 音视频文件务必存入交互计算单元(IP: 10.0.1.42)。
⚠️ 音视频文件夹及该文件夹所有父目录应当为所有用户可访问读取(建议在/var/tmp/下创建子目录存放)

注意

使用本功能前,请在运控计算单元(PC1)上执行以下命令关闭 task_manager 模块,避免其与二开程序争抢显示屏控制权:

aima em stop-app task_manager

功能说明 通过调用PlayVideo服务,可以让机器人在屏幕上播放指定路径的视频文件。请确保视频文件已上传到交互计算单元,否则播放会失败。

💡 提示:视频文件的准备方式

您可以使用 C++ echo_camera_rgbd 示例 将机器人的深度相机 RGB 图像录制为 .mp4 视频文件,再将其作为 play_video 的输入:

第一步:在机器人开发计算单元(PC2)上录制视频(使用 C++ 示例)

ros2 run examples echo_camera_rgbd --ros-args -p topic_type:=rgb_image -p dump_video_path:=$PWD/output.mp4
# 录制完成后按 Ctrl+C 停止,视频保存为 output.mp4

第二步:将视频传输到交互计算单元(PC3,IP: 10.0.1.42)

scp output.mp4 user@10.0.1.42:/var/tmp/videos/output.mp4

第三步:运行 play_video 播放

ros2 run py_examples play_video  # 修改代码中 video_path 为 /var/tmp/videos/output.mp4
 1#!/usr/bin/env python3
 2
 3import rclpy
 4import rclpy.logging
 5from rclpy.node import Node
 6
 7from aimdk_msgs.srv import PlayVideo
 8
 9
10class PlayVideoClient(Node):
11    def __init__(self):
12        super().__init__('play_video_client')
13        self.client = self.create_client(
14            PlayVideo, '/aimdk_5Fmsgs/srv/PlayVideo')
15        self.get_logger().info('✅ PlayVideo client node created.')
16
17        # Wait for the service to become available
18        while not self.client.wait_for_service(timeout_sec=2.0):
19            self.get_logger().info('⏳ Service unavailable, waiting...')
20
21        self.get_logger().info('🟢 Service available, ready to send request.')
22
23    def send_request(self, video_path, mode, priority):
24        req = PlayVideo.Request()
25
26        req.video_path = video_path
27        req.mode = mode
28        req.priority = priority
29
30        # async call
31        self.get_logger().info(
32            f'📨 Sending request to play video: mode={mode} video={video_path}')
33        for i in range(8):
34            req.header.header.stamp = self.get_clock().now().to_msg()
35            future = self.client.call_async(req)
36            rclpy.spin_until_future_complete(self, future, timeout_sec=0.25)
37
38            if future.done():
39                break
40
41            # retry as remote peer is NOT handled well by ROS
42            self.get_logger().info(f'trying ... [{i}]')
43
44        resp = future.result()
45        if resp is None:
46            self.get_logger().error('❌ Service call not completed or timed out.')
47            return False
48
49        if resp.success:
50            self.get_logger().info(
51                f'✅ Request to play video recorded successfully: {resp.message}')
52            return True
53        else:
54            self.get_logger().error(
55                f'❌ Failed to record play-video request: {resp.message}')
56            return False
57
58
59def main(args=None):
60    rclpy.init(args=args)
61    node = None
62
63    try:
64        # video path and priority can be customized
65        default_video_path = "/agibot/software/face_ui/bin/app/aarch64-generic/assets/mp4/emoji_x2_sleepy.mp4"
66        video_path = input(
67            f"Enter video path (default: {default_video_path}): ").strip()
68        if not video_path:
69            video_path = default_video_path
70        priority = 5
71        # input play mode
72        mode = int(input("Enter video play mode (1: play once, 2: loop): "))
73        if mode not in (1, 2):
74            raise ValueError(f'invalid mode {mode}')
75
76        node = PlayVideoClient()
77        node.send_request(video_path, mode, priority)
78    except KeyboardInterrupt:
79        pass
80    except Exception as e:
81        rclpy.logging.get_logger('main').error(
82            f'Program exited with exception: {e}')
83
84    if node:
85        node.destroy_node()
86    if rclpy.ok():
87        rclpy.shutdown()
88
89
90if __name__ == '__main__':
91    main()

6.1.19 音频文件播放

该示例中用到了play_audio,通过该节点可实现播放指定的音频文件,支持WAV(PCM编码)、RAW PCM格式的音频文件播放,不支持MP3等其他格式。

功能特点:

  • 支持WAV(PCM编码)、RAW PCM格式播放,不支持MP3等其他格式

  • 支持优先级控制,可设置播放优先级

  • 支持自定义文件路径和播放参数

  • 提供完整的错误处理和状态反馈

技术实现:

  • 使用PlayAudioFile服务进行音频文件播放

  • 支持优先级设置(priority: 1~10,priority_weight: 1~100)

  • 提供详细的播放状态反馈

应用场景:

  • 音频文件播放和媒体控制

  • 语音提示和音效播放

  • 多媒体应用开发

  • 机器人交互音频反馈

注意

⚠️ 请注意!交互计算单元(PC3)独立于二开程序所在的开发计算单元(PC2), 音视频文件务必存入交互计算单元(IP: 10.0.1.42)。
⚠️ 音视频文件夹及该文件夹所有父目录应当为所有用户可访问读取(建议在/var/tmp/下创建子目录存放)

  1#!/usr/bin/env python3
  2
  3import os
  4import sys
  5import rclpy
  6import rclpy.logging
  7from rclpy.node import Node
  8
  9from aimdk_msgs.srv import PlayAudioFile
 10from aimdk_msgs.msg import CommonState
 11
 12
 13class PlayAudioClient(Node):
 14    def __init__(self):
 15        super().__init__('play_audio_client')
 16        self.client = self.create_client(
 17            PlayAudioFile, '/aimdk_5Fmsgs/srv/PlayAudioFile')
 18        self.get_logger().info('✅ PlayAudio client node created.')
 19
 20        # Wait for the service to become available
 21        while not self.client.wait_for_service(timeout_sec=2.0):
 22            self.get_logger().info('⏳ Service unavailable, waiting...')
 23
 24        self.get_logger().info('🟢 Service available, ready to send request.')
 25
 26    def send_request(self, audio_path):
 27        req = PlayAudioFile.Request()
 28
 29        req.file.pkg_name = 'demo_client'
 30        req.file.file_name = os.path.basename(audio_path)
 31        req.file.file_path = os.path.dirname(audio_path) + '/'
 32        req.file.priority = 8
 33        req.file.priority_weight = 0
 34
 35        self.get_logger().info(
 36            f'📨 Sending request to play audio: {audio_path}')
 37        for i in range(5):
 38            req.request.header.stamp = self.get_clock().now().to_msg()
 39            future = self.client.call_async(req)
 40            rclpy.spin_until_future_complete(self, future, timeout_sec=1.0)
 41
 42            if future.done():
 43                break
 44
 45            if not rclpy.ok():
 46                self.get_logger().warn('Interrupted while waiting')
 47                return False
 48
 49            # retry as remote peer is NOT handled well by ROS
 50            self.get_logger().info(f'trying ... [{i}]')
 51
 52        resp = future.result()
 53        if resp is None:
 54            self.get_logger().error('❌ Service call not completed or timed out.')
 55            return False
 56
 57        if resp.reponse.status.value == CommonState.SUCCESS:  # SUCCESS (1)
 58            self.get_logger().info('✅ Request to play audio file recorded successfully.')
 59            return True
 60        else:
 61            # FAILURE(2): focus-apply failed; INVALID(5): invalid params;
 62            # other: see CommonState for details.
 63            reasons = {CommonState.FAILURE: 'focus apply failed',  # FAILURE (2)
 64                       CommonState.INVALID: 'invalid params'}      # INVALID (5)
 65            reason = reasons.get(resp.reponse.status.value, 'unknown')
 66            self.get_logger().error(
 67                f'❌ Failed to record play-audio request '
 68                f'(status={resp.reponse.status.value}, {reason}): '
 69                f'{resp.reponse.message}')
 70            return False
 71
 72
 73def main(args=None):
 74    rclpy.init(args=args)
 75    node = None
 76
 77    default_audio = '/agibot/data/var/hal_audio/file/bingo.wav'
 78    try:
 79        if len(sys.argv) > 1:
 80            audio_path = sys.argv[1]
 81        else:
 82            audio_path = input(
 83                f'Enter audio file path to play (default: {default_audio}): ').strip()
 84            if not audio_path:
 85                audio_path = default_audio
 86
 87        node = PlayAudioClient()
 88        node.send_request(audio_path)
 89    except KeyboardInterrupt:
 90        pass
 91    except Exception as e:
 92        rclpy.logging.get_logger('main').error(
 93            f'Program exited with exception: {e}')
 94
 95    if node:
 96        node.destroy_node()
 97    if rclpy.ok():
 98        rclpy.shutdown()
 99
100
101if __name__ == '__main__':
102    main()

使用说明:

# 播放默认音频文件
ros2 run py_examples play_audio

# 播放指定音频文件
# 注意替换/path/to/your/audio_file.wav为交互板上实际文件路径
ros2 run py_examples play_audio /path/to/your/audio_file.wav

# 播放TTS缓存文件
ros2 run py_examples play_audio /agibot/data/var/hal_audio/file/bingo.wav

输出示例:

[INFO] [1779183967.998445447] [play_audio_client]: ✅ PlayAudio client node created.
[INFO] [1779183968.005492518] [play_audio_client]: 🟢 Service available, ready to send request.
[INFO] [1779183968.007782727] [play_audio_client]: 📨 Sending request to play audio: /agibot/data/var/hal_audio/file/bingo.wav
[INFO] [1779183968.029756237] [play_audio_client]: ✅ Request to play audio file recorded successfully.

注意事项:

  • 确保音频文件路径正确且文件存在

  • 支持的文件格式:WAV(PCM编码)、RAW PCM,不支持MP3等其他格式

  • 优先级设置影响播放队列顺序

6.1.20 TTS (文字转语音)

该示例中用到了play_tts,通过该节点可实现语音播放输入的文字,用户可根据不同的场景输入相应的文本。

功能特点:

  • 支持命令行参数和交互式输入

  • 完整的服务可用性检查和错误处理

  • 支持优先级控制和打断机制

  • 提供详细的播放状态反馈

核心代码

 1#!/usr/bin/env python3
 2
 3import sys
 4import rclpy
 5import rclpy.logging
 6from rclpy.node import Node
 7
 8from aimdk_msgs.srv import PlayTts
 9from aimdk_msgs.msg import TtsPriorityLevel
10
11
12class PlayTTSClient(Node):
13    def __init__(self):
14        super().__init__('play_tts_client')
15
16        # fill in the actual service name
17        self.client = self.create_client(PlayTts, '/aimdk_5Fmsgs/srv/PlayTts')
18        self.get_logger().info('✅ PlayTts client node created.')
19
20        # Wait for the service to become available
21        while not self.client.wait_for_service(timeout_sec=2.0):
22            self.get_logger().info('⏳ Service unavailable, waiting...')
23
24        self.get_logger().info('🟢 Service available, ready to send request.')
25
26    def send_request(self, text):
27        req = PlayTts.Request()
28
29        req.tts_req.text = text
30        req.tts_req.domain = 'demo_client'   # required: caller domain
31        req.tts_req.trace_id = 'demo'        # optional: request id
32        req.tts_req.is_interrupted = True    # required: interrupt same-priority
33        req.tts_req.priority_weight = 0
34        # L6 (6)
35        req.tts_req.priority_level.value = TtsPriorityLevel.INTERACTION_L6
36
37        self.get_logger().info(f'📨 Sending request to play tts: text={text}')
38        for i in range(8):
39            req.header.header.stamp = self.get_clock().now().to_msg()
40            future = self.client.call_async(req)
41            rclpy.spin_until_future_complete(self, future, timeout_sec=0.25)
42
43            if future.done():
44                break
45
46            # retry as remote peer is NOT handled well by ROS
47            self.get_logger().info(f'trying ... [{i}]')
48
49        resp = future.result()
50        if resp is None:
51            self.get_logger().error('❌ Service call not completed or timed out.')
52            return False
53
54        if resp.tts_resp.is_success:
55            self.get_logger().info('✅ TTS sent successfully.')
56            return True
57        else:
58            self.get_logger().error(
59                f'❌ Failed to send TTS: error_code={resp.tts_resp.error_code}, '
60                f'error_message={resp.tts_resp.error_message}')
61            return False
62
63
64def main(args=None):
65    rclpy.init(args=args)
66    node = None
67
68    try:
69        # get text to speak
70        if len(sys.argv) > 1:
71            text = sys.argv[1]
72        else:
73            text = input('Enter text to speak: ')
74            if not text:
75                text = 'Hello, I am AgiBot X2.'
76
77        node = PlayTTSClient()
78        node.send_request(text)
79    except KeyboardInterrupt:
80        pass
81    except Exception as e:
82        rclpy.logging.get_logger('main').error(
83            f'Program exited with exception: {e}')
84
85    if node:
86        node.destroy_node()
87    if rclpy.ok():
88        rclpy.shutdown()
89
90
91if __name__ == '__main__':
92    main()

使用说明

# 使用命令行参数播报文本(推荐)
ros2 run py_examples play_tts "你好,我是灵犀X2机器人"

# 或者不带参数运行,程序会提示用户输入
ros2 run py_examples play_tts

输出示例

[INFO] [1779181480.764266243] [play_tts_client]: ✅ PlayTts client node created.
[INFO] [1779181480.765467698] [play_tts_client]: 🟢 Service available, ready to send request.
[INFO] [1779181480.766450608] [play_tts_client]: 📨 Sending request to play tts: text=Hello
[INFO] [1779181480.774077262] [play_tts_client]: ✅ TTS sent successfully.

注意事项

  • 确保TTS服务正常运行

  • 支持中文和英文文本播报

  • 优先级设置影响播放队列顺序

  • 打断功能可中断当前播放的语音

接口参考

  • 服务:/aimdk_5Fmsgs/srv/PlayTts

  • 消息:aimdk_msgs/srv/PlayTts

6.1.21 麦克风数据接收

该示例中用到了mic_receiver,通过订阅/agent/process_audio_output话题来接收机器人的降噪音频数据,支持内置麦克风和外置麦克风两种音频流,并根据VAD(语音活动检测)状态自动保存完整的语音片段为PCM文件。

功能特点:

  • 基于VAD状态自动检测语音开始、处理中、结束

  • 自动保存完整语音片段为PCM文件(单声道,采样率16kHz,格式S16LE)

  • 按时间戳和音频流分类存储

  • 支持音频时长计算和统计信息输出

应用场景:

  • 语音识别和语音处理

  • 音频数据采集和分析

  • 实时语音监控

  • 音频质量检测

  • 多麦克风阵列数据处理

  1#!/usr/bin/env python3
  2"""
  3Microphone data receiving example
  4
  5This example subscribes to the `/agent/process_audio_output` topic to receive the robot's
  6noise-suppressed audio data. It supports both the built-in microphone and the external
  7microphone audio streams, and automatically saves complete speech segments as PCM files
  8based on the VAD (Voice Activity Detection) state.
  9
 10Features:
 11- Supports receiving multiple audio streams at the same time (built-in mic stream_id=1, external mic stream_id=2)
 12- Automatically detects speech start / in-progress / end based on VAD state
 13- Automatically saves complete speech segments as PCM files
 14- Stores files categorized by timestamp and audio stream
 15- Supports audio duration calculation and logging
 16
 17VAD state description:
 18- 1: Speech start
 19- 2: Speech in progress
 20- 3: Speech end
 21"""
 22
 23import rclpy
 24from rclpy.node import Node
 25from rclpy.qos import QoSProfile, QoSReliabilityPolicy, QoSHistoryPolicy
 26from aimdk_msgs.msg import ProcessedAudioOutput, AudioVadStateType
 27import os
 28from datetime import datetime
 29from collections import defaultdict
 30from typing import Dict, List
 31
 32
 33class AudioSubscriber(Node):
 34    def __init__(self):
 35        super().__init__('audio_subscriber')
 36
 37        # Audio buffers, stored separately by stream_id
 38        # stream_id -> buffer
 39        self.audio_buffers: Dict[int, List[bytes]] = defaultdict(list)
 40        self.recording_state: Dict[int, bool] = defaultdict(bool)
 41
 42        # Create audio output directory
 43        self.audio_output_dir = "audio_recordings"
 44        os.makedirs(self.audio_output_dir, exist_ok=True)
 45
 46        # VAD state name mapping
 47        self.vad_state_names = {
 48            AudioVadStateType.AUDIO_VAD_STATE_BEGIN: "Speech start",
 49            AudioVadStateType.AUDIO_VAD_STATE_PROCESSING: "Speech in progress",
 50            AudioVadStateType.AUDIO_VAD_STATE_END: "Speech end",
 51        }
 52
 53        # Audio stream name mapping
 54        self.stream_names = {
 55            1: "Built-in microphone",
 56            2: "External microphone"
 57        }
 58
 59        # QoS settings
 60        # Note: deep queue to avoid missing data in a burst at start of VAD.
 61        qos = QoSProfile(
 62            history=QoSHistoryPolicy.KEEP_LAST,
 63            depth=500,
 64            reliability=QoSReliabilityPolicy.BEST_EFFORT
 65        )
 66
 67        # Create subscriber
 68        self.subscription = self.create_subscription(
 69            ProcessedAudioOutput,
 70            '/agent/process_audio_output',
 71            self.audio_callback,
 72            qos
 73        )
 74
 75        self.get_logger().info("Start subscribing to noise-suppressed audio data...")
 76
 77    def audio_callback(self, msg: ProcessedAudioOutput):
 78        """Audio data callback"""
 79        try:
 80            stream_id = msg.stream_id
 81            vad_state = msg.audio_vad_state.value
 82            audio_data = bytes(msg.audio_data)
 83
 84            self.get_logger().info(
 85                f"Received audio data: stream_id={stream_id}, "
 86                f"vad_state={vad_state}({self.vad_state_names.get(vad_state, 'Unknown state')}), "
 87                f"audio_size={len(audio_data)} bytes"
 88            )
 89
 90            self.handle_vad_state(stream_id, vad_state, audio_data)
 91
 92        except Exception as e:
 93            self.get_logger().error(
 94                f"Error while processing audio message: {str(e)}")
 95
 96    def handle_vad_state(self, stream_id: int, vad_state: int, audio_data: bytes):
 97        """Handle VAD state changes"""
 98        stream_name = self.stream_names.get(
 99            stream_id, f"Unknown stream {stream_id}")
100        vad_name = self.vad_state_names.get(
101            vad_state, f"Unknown state {vad_state}")
102
103        self.get_logger().info(
104            f"[{stream_name}] VAD state: {vad_name} audio: {len(audio_data)} bytes"
105        )
106
107        # Speech start
108        if vad_state == AudioVadStateType.AUDIO_VAD_STATE_BEGIN:  # BEGIN (1)
109            self.get_logger().info("🎤 Speech start detected")
110            if not self.recording_state[stream_id]:
111                self.audio_buffers[stream_id].clear()
112                self.recording_state[stream_id] = True
113            if audio_data:
114                self.audio_buffers[stream_id].append(audio_data)
115
116        # Speech in progress
117        # PROCESSING (2)
118        elif vad_state == AudioVadStateType.AUDIO_VAD_STATE_PROCESSING:
119            self.get_logger().info("🔄 Speech in progress...")
120            if self.recording_state[stream_id] and audio_data:
121                self.audio_buffers[stream_id].append(audio_data)
122
123        # Speech end
124        elif vad_state == AudioVadStateType.AUDIO_VAD_STATE_END:  # END (3)
125            self.get_logger().info("✅ Speech end")
126            if self.recording_state[stream_id] and audio_data:
127                self.audio_buffers[stream_id].append(audio_data)
128
129            if self.recording_state[stream_id] and self.audio_buffers[stream_id]:
130                self.save_audio_segment(stream_id)
131            self.recording_state[stream_id] = False
132
133        # Unknown speech state
134        else:
135            if self.recording_state[stream_id]:
136                self.get_logger().info("⏹️ Reset recording state")
137                self.recording_state[stream_id] = False
138
139        # Print current buffer status
140        buffer_size = sum(len(chunk)
141                          for chunk in self.audio_buffers[stream_id])
142        recording = self.recording_state[stream_id]
143        self.get_logger().debug(
144            f"[Stream {stream_id}] Buffer size: {buffer_size} bytes, recording: {recording}"
145        )
146
147    def save_audio_segment(self, stream_id: int):
148        """Save audio segment"""
149        if not self.audio_buffers[stream_id]:
150            return
151
152        # Merge all audio data
153        audio_data = b''.join(self.audio_buffers[stream_id])
154
155        # Get current timestamp
156        now = datetime.now()
157        timestamp = now.strftime("%Y%m%d_%H%M%S_%f")[:-3]  # to milliseconds
158
159        # Create subdirectory by stream_id
160        stream_dir = os.path.join(self.audio_output_dir, f"stream_{stream_id}")
161        os.makedirs(stream_dir, exist_ok=True)
162
163        # Generate filename
164        stream_name = "internal_mic" if stream_id == 1 else "external_mic" if stream_id == 2 else f"stream_{stream_id}"
165        filename = f"{stream_name}_{timestamp}.pcm"
166        filepath = os.path.join(stream_dir, filename)
167
168        try:
169            # Save PCM file
170            with open(filepath, 'wb') as f:
171                f.write(audio_data)
172
173            self.get_logger().info(
174                f"Audio segment saved: {filepath} (size: {len(audio_data)} bytes)")
175
176            # Calculate audio duration (assuming 16 kHz, 16-bit, mono)
177            sample_rate = 16000
178            bits_per_sample = 16
179            channels = 1
180            bytes_per_sample = bits_per_sample // 8
181            total_samples = len(audio_data) // (bytes_per_sample * channels)
182            duration_seconds = total_samples / sample_rate
183
184            self.get_logger().info(
185                f"Audio duration: {duration_seconds:.2f} s ({total_samples} samples)")
186
187        except Exception as e:
188            self.get_logger().error(f"Failed to save audio file: {str(e)}")
189
190
191def main(args=None):
192    rclpy.init(args=args)
193    node = AudioSubscriber()
194
195    try:
196        node.get_logger().info(
197            "Listening for noise-suppressed audio data, press Ctrl+C to exit...")
198        rclpy.spin(node)
199    except KeyboardInterrupt:
200        node.get_logger().info("Interrupt signal received, exiting...")
201    finally:
202        node.destroy_node()
203        if rclpy.ok():
204            rclpy.shutdown()
205
206
207if __name__ == '__main__':
208    main()

使用说明:

  1. 运行程序:

    # 构建Python包
    colcon build --packages-select py_examples
    
    # 运行麦克风接收程序, 配合唤醒词触发VAD
    ros2 run py_examples mic_receiver
    

    按 Ctrl+C 安全退出程序,程序会自动处理音频流的开始和结束。

  2. 查找录制的PCM文件:

    • 运行节点后会自动创建audio_recordings目录

    • 音频文件按stream_id分类存储(暂不区分, 一律使用stream_1):

      • stream_1/: 内置麦克风音频

      • stream_2/: 外置麦克风音频

    • 文件命名格式:{stream_name}_{timestamp}.pcm

      • internal_mic_20250909_133649_738.pcm (内置麦克风)

      • external_mic_20250909_133650_123.pcm (外置麦克风)

  3. 播放保存的PCM文件:

    # 播放内置麦克风录音
    aplay -r 16000 -f S16_LE -c 1 audio_recordings/stream_1/internal_mic_20250909_133649_738.pcm
    

    参数说明:

    • -r 16000 # 采样率16kHz

    • -f S16_LE # 16位小端格式

    • -c 1 # 单声道 你也可以用其他音频播放器(如Audacity)以原始PCM格式导入并播放。

  4. 转换为WAV格式(可选):

    # 使用ffmpeg转换为WAV格式
    ffmpeg -f s16le -ar 16000 -ac 1 -i external_mic_20250909_133649_738.pcm output.wav
    

输出示例:

正常启动和运行:

[INFO] [1779196050.590953283] [audio_subscriber]: Start subscribing to noise-suppressed audio data...
[INFO] [1779196050.592515383] [audio_subscriber]: Listening for noise-suppressed audio data, press Ctrl+C to exit...

检测到语音开始:

[INFO] [1779196055.100798381] [audio_subscriber]: Received audio data: stream_id=1, vad_state=1(Speech start), audio_size=2560 bytes
[INFO] [1779196055.101560663] [audio_subscriber]: [Built-in microphone] VAD state: Speech start audio: 2560 bytes
[INFO] [1779196055.102147334] [audio_subscriber]: 🎤 Speech start detected

语音处理过程:

[INFO] [1779196055.103814262] [audio_subscriber]: Received audio data: stream_id=1, vad_state=2(Speech in progress), audio_size=19200 bytes
[INFO] [1779196055.104458211] [audio_subscriber]: [Built-in microphone] VAD state: Speech in progress audio: 19200 bytes
[INFO] [1779196055.105151599] [audio_subscriber]: 🔄 Speech in progress...
[INFO] [1779196055.164856185] [audio_subscriber]: Received audio data: stream_id=1, vad_state=2(Speech in progress), audio_size=3200 bytes
[INFO] [1779196055.165511654] [audio_subscriber]: [Built-in microphone] VAD state: Speech in progress audio: 3200 bytes
[INFO] [1779196055.166184626] [audio_subscriber]: 🔄 Speech in progress...

语音结束和保存:

[INFO] [1779196055.867450360] [audio_subscriber]: Received audio data: stream_id=1, vad_state=3(Speech end), audio_size=2560 bytes
[INFO] [1779196055.868118086] [audio_subscriber]: [Built-in microphone] VAD state: Speech end audio: 2560 bytes
[INFO] [1779196055.868834482] [audio_subscriber]: ✅ Speech end
[INFO] [1779196055.870320425] [audio_subscriber]: Audio segment saved: audio_recordings/stream_1/internal_mic_20260519_210735_868.pcm (size: 49920 bytes)
[INFO] [1779196055.871051508] [audio_subscriber]: Audio duration: 1.56 s (24960 samples)

6.1.22 麦克风原始流式数据接收

该示例中用到了mic_raw_receiver,通过订阅/aima/hal/audio/capture话题来接收机器人的原始音频数据,识别麦克风/回采,自动按声道保存为PCM文件。

功能特点:

  • 按时间戳和声道分类存储

  • 自动保存声道音频数据为PCM文件(单声道,采样率16kHz,格式S16LE)

  • 支持麦克风切换检测

  1#!/usr/bin/env python3
  2"""
  3Microphone raw data receiving example
  4
  5This example subscribes to the `/aima/hal/audio/capture` topic to receive the robot's
  6raw audio data. It supports both the built-in microphone and the external
  7microphone audio streams, and automatically saves complete speech segments as PCM files.
  8
  9Features:
 10- Automatically saves raw audio as PCM files
 11- Stores files categorized by timestamp and mic source
 12
 13"""
 14
 15import rclpy
 16from rclpy.node import Node
 17from rclpy.qos import QoSProfile, QoSReliabilityPolicy, QoSHistoryPolicy
 18from aimdk_msgs.msg import AudioCapture
 19import os
 20import time
 21from datetime import datetime
 22from collections import defaultdict
 23from typing import Dict, List
 24
 25
 26class RawAudioSubscriber(Node):
 27    def __init__(self):
 28        super().__init__('raw_audio_subscriber')
 29
 30        # Audio buffers, stored separately by channels
 31        self.audio_buffers: Dict[int, List[bytes]] = defaultdict(list)
 32        self.mic_channels = 0
 33        self.ref_channels = 0
 34        self.inited = 0
 35        self.dump_timestamp = 0
 36
 37        # Create audio output directory
 38        now = datetime.now()
 39        timestamp = now.strftime("%Y%m%d_%H%M%S_%f")[:-3]  # to milliseconds
 40
 41        self.audio_output_dir = os.path.join("audio_recordings", timestamp)
 42        os.makedirs(self.audio_output_dir, exist_ok=True)
 43
 44        # QoS settings
 45        # Note: deep queue to avoid missing data
 46        qos = QoSProfile(
 47            history=QoSHistoryPolicy.KEEP_LAST,
 48            depth=500,
 49            reliability=QoSReliabilityPolicy.BEST_EFFORT
 50        )
 51
 52        # Create subscriber
 53        self.subscription = self.create_subscription(
 54            AudioCapture,
 55            '/aima/hal/audio/capture',
 56            self.audio_callback,
 57            qos
 58        )
 59
 60        self.get_logger().info("Start subscribing to raw audio data...")
 61
 62    def audio_callback(self, msg: AudioCapture):
 63        """Audio data callback"""
 64        if not self.inited:
 65            self.mic_channels = msg.mic_channels
 66            self.ref_channels = msg.ref_channels
 67            self.dump_timestamp = time.time()
 68            self.inited = 1
 69        elif self.mic_channels != msg.mic_channels or self.ref_channels != msg.ref_channels:
 70            # quit as MIC switched
 71            self.get_logger().error(
 72                f"MIC channels info changed (mic:{self.mic_channels} ref:{self.ref_channels}) -> (mic:{msg.mic_channels} ref:{msg.ref_channels})")
 73            if rclpy.ok():
 74                rclpy.shutdown()
 75            return
 76        audio_data = bytes(msg.data.data)
 77
 78        self.handle_audio_data(audio_data)
 79
 80    def handle_audio_data(self, audio_data: bytes):
 81        """Handle raw audio data"""
 82
 83        # Split S16LE data into channels
 84        channels = self.ref_channels + self.mic_channels
 85        bytes_per_channel = 2
 86        unit_size = channels * bytes_per_channel
 87        base = 0
 88        for k in range(len(audio_data) // unit_size):
 89            for i in range(channels):
 90                tail = base + bytes_per_channel
 91                self.audio_buffers[i].append(audio_data[base:tail])
 92                base = tail
 93
 94    def save_audio_segments(self):
 95        self.get_logger().info("🔄 Flushing cached audio data...")
 96        for i in range(self.mic_channels + self.ref_channels):
 97            self.save_audio_segment(i, i >= self.mic_channels)
 98
 99    def save_audio_segment(self, channel: int, is_ref: bool):
100        """Save audio segment"""
101        if not self.audio_buffers[channel]:
102            return
103
104        # Merge all audio data
105        audio_data = b''.join(self.audio_buffers[channel])
106        self.audio_buffers[channel].clear()
107
108        channel_type = 'mic'
109        if is_ref:
110            channel_type = 'ref'
111
112        # Generate filename
113        filepath = os.path.join(self.audio_output_dir,
114                                f"channel_{channel}_{channel_type}.pcm")
115
116        try:
117            # Save PCM file
118            with open(filepath, 'ab') as f:
119                f.write(audio_data)
120
121            self.get_logger().info(
122                f"Audio segment saved: {filepath} appended: {len(audio_data)} bytes")
123
124        except Exception as e:
125            self.get_logger().error(f"Failed to save audio file: {str(e)}")
126
127    def run(self):
128        self.timer = self.create_timer(1.0, self.save_audio_segments)
129        rclpy.spin(self)
130
131
132def main(args=None):
133    rclpy.init(args=args)
134    node = None
135
136    try:
137        node = RawAudioSubscriber()
138        node.get_logger().info("Listening to raw audio data, press Ctrl+C to exit...")
139        node.run()
140    except KeyboardInterrupt:
141        rclpy.logging.get_logger('main').info(
142            "Interrupt signal received, exiting...")
143
144    if node:
145        node.destroy_node()
146    if rclpy.ok():
147        rclpy.shutdown()
148
149
150if __name__ == '__main__':
151    main()

使用说明:

  1. 运行程序:

    # 构建Python包
    colcon build --packages-select py_examples
    
    # 运行麦克风接收程序
    ros2 run py_examples mic_raw_receiver
    
  2. 查找录制的PCM文件:

    • 运行节点后会自动创建audio_recordings目录

    • 音频文件按录制程序本次启动时间归集存储至如下格式的目录:

      • 20250909_133650_123

    • 文件命名格式:channel_{index}_{type}.pcm,如

      • channel_0_mic.pcm(麦克风)

      • channel_4_ref.pcm(回采)

  3. 播放保存的PCM文件:

    • 转到计算机上播放:

      aplay -r 16000 -f S16_LE -c 1 audio_recordings/20250909_133650_123/channel_0_mic.pcm
      
    • 机器人内播放:

      # 使用原始流式音频播放例程
      ros2 run py_examples play_audio_stream --ros-args -p raw_audio_path:=$PWD/audio_recordings/20250909_133650_123/channel_0_mic.pcm
      # 或使用媒体文件播放例程, 注意提前将文件转存到交互板内, 并使用其路径替换下方pcm路径
      ros2 run py_examples play_audio please/use/path/on/PC3.pcm
      

6.1.23 获取当前麦克风

 1#!/usr/bin/env python3
 2
 3import rclpy
 4import rclpy.logging
 5from rclpy.node import Node
 6
 7from aimdk_msgs.srv import GetMicSourceRequest
 8from aimdk_msgs.msg import CommonRequest, CommonState
 9
10
11class GetMicSourceRequestClient(Node):
12    def __init__(self):
13        super().__init__('get_mic_source_request_client')
14        self.client = self.create_client(
15            GetMicSourceRequest, '/aimdk_5Fmsgs/srv/GetMicSourceRequest'
16        )
17        self.get_logger().info('✅ GetMicSourceRequest client node created.')
18
19        # Wait for the service to become available
20        while not self.client.wait_for_service(timeout_sec=2.0):
21            self.get_logger().info('⏳ Service unavailable, waiting...')
22
23        self.get_logger().info('🟢 Service available, ready to send request.')
24
25    def send_request(self):
26        req = GetMicSourceRequest.Request()
27        req.header = CommonRequest()
28
29        self.get_logger().info(
30            '📨 Sending request to get MIC source')
31        for i in range(8):
32            req.header.header.stamp = self.get_clock().now().to_msg()
33            future = self.client.call_async(req)
34            rclpy.spin_until_future_complete(self, future, timeout_sec=0.25)
35
36            if future.done():
37                break
38
39            # retry as remote peer is NOT handled well by ROS
40            self.get_logger().info(f'trying ... [{i}]')
41
42        response = future.result()
43        if response is None:
44            self.get_logger().error('❌ Service call failed or timed out.')
45            return
46
47        if response.header.status.value == CommonState.SUCCESS:  # SUCCESS (1)
48            self.get_logger().info('✅ MIC source get successfully.')
49            self.get_logger().info(f'MIC id: {response.mic_source}')
50        else:
51            self.get_logger().error(
52                f'❌ Failed to get MIC source: {response.header.message}'
53            )
54
55
56def main(args=None):
57    rclpy.init(args=args)
58    node = None
59    try:
60        node = GetMicSourceRequestClient()
61        node.send_request()
62    except KeyboardInterrupt:
63        pass
64    except Exception as e:
65        rclpy.logging.get_logger('main').error(
66            f'Program exited with exception: {e}')
67
68    if node:
69        node.destroy_node()
70    if rclpy.ok():
71        rclpy.shutdown()
72
73
74if __name__ == '__main__':
75    main()

6.1.24 切换麦克风

  1#!/usr/bin/env python3
  2
  3import sys
  4import rclpy
  5import rclpy.logging
  6from rclpy.node import Node
  7
  8from aimdk_msgs.srv import SetMicSourceRequest
  9from aimdk_msgs.msg import CommonRequest, CommonState
 10
 11
 12class SetMicSourceRequestClient(Node):
 13    def __init__(self):
 14        super().__init__('set_mic_source_request_client')
 15        self.client = self.create_client(
 16            SetMicSourceRequest, '/aimdk_5Fmsgs/srv/SetMicSourceRequest'
 17        )
 18        self.get_logger().info('✅ SetMicSourceRequest client node created.')
 19
 20        # Wait for the service to become available
 21        while not self.client.wait_for_service(timeout_sec=2.0):
 22            self.get_logger().info('⏳ Service unavailable, waiting...')
 23
 24        self.get_logger().info('🟢 Service available, ready to send request.')
 25
 26    def send_request(self, mic_source: int):
 27        req = SetMicSourceRequest.Request()
 28        req.header = CommonRequest()
 29
 30        req.mic_source = mic_source
 31
 32        self.get_logger().info(
 33            f'📨 Sending request to set mic source: {mic_source}')
 34        for i in range(8):
 35            req.header.header.stamp = self.get_clock().now().to_msg()
 36            future = self.client.call_async(req)
 37            rclpy.spin_until_future_complete(self, future, timeout_sec=0.25)
 38
 39            if future.done():
 40                break
 41
 42            # retry as remote peer is NOT handled well by ROS
 43            self.get_logger().info(f'trying ... [{i}]')
 44
 45        response = future.result()
 46        if response is None:
 47            self.get_logger().error('❌ Service call failed or timed out.')
 48            return
 49
 50        if response.header.status.value == CommonState.SUCCESS:  # SUCCESS (1)
 51            self.get_logger().info('✅ MIC source set successfully.')
 52        else:
 53            self.get_logger().error(
 54                f'❌ Failed to set MIC source: {response.header.message}'
 55            )
 56
 57
 58def main(args=None):
 59    mic_info = {
 60        0: ('int', 'internal MIC'),
 61        1: ('ext', 'external MIC'),
 62    }
 63
 64    choices = {}
 65    for k, v in mic_info.items():
 66        choices[v[0]] = k
 67
 68    rclpy.init(args=args)
 69    node = None
 70    try:
 71        # Prefer command-line argument, otherwise prompt for input
 72        if len(sys.argv) > 1:
 73            mic = sys.argv[1]
 74        else:
 75            print('{:<4} - {:<5} : {}'.format('abbr',
 76                  'mic_id', 'description'))
 77            for k, v in mic_info.items():
 78                print(f'{v[0]:<4} - {k:<5} : {v[1]}')
 79            mic = input('Enter abbr of MIC source:')
 80
 81        mic_id = choices.get(mic)
 82        if mic_id is None:
 83            raise ValueError(f'Invalid abbr of MIC source: {mic}')
 84
 85        node = SetMicSourceRequestClient()
 86        node.send_request(mic_id)
 87    except KeyboardInterrupt:
 88        pass
 89    except Exception as e:
 90        rclpy.logging.get_logger('main').error(
 91            f'Program exited with exception: {e}')
 92
 93    if node:
 94        node.destroy_node()
 95    if rclpy.ok():
 96        rclpy.shutdown()
 97
 98
 99if __name__ == '__main__':
100    main()

使用说明

# 构建
colcon build --packages-select py_examples

# 交互式运行
ros2 run py_examples set_mic_source

# 命令行参数运行
ros2 run py_examples set_mic_source ext  # ext - 外置麦, int - 内置麦

6.1.25 原始流式音频播放

该示例中用到了play_audio_stream,先申请音频焦点,再将PCM文件以实时速率发布到/aima/hal/audio/playback话题进行播放;焦点被抢时自动退出。

  1#!/usr/bin/env python3
  2"""
  3Raw audio stream playback example
  4
  5This example would get audio focus first and then publish raw audio data
  6at fixed rate to `/aima/hal/audio/playback` topic. And it would stop playing
  7when audio focus is lost.
  8
  9"""
 10
 11import rclpy
 12from rclpy.node import Node
 13from rclpy.qos import QoSProfile, QoSReliabilityPolicy, QoSDurabilityPolicy, QoSHistoryPolicy
 14from aimdk_msgs.msg import AudioPlayback, AudioInfo, AudioData, FocusResponse, FocusRequester, CommonState
 15from aimdk_msgs.srv import RequestAudioFocus, AbandonAudioFocus
 16import os
 17import signal
 18import time
 19import sys
 20
 21global_node = None
 22
 23
 24def signal_handler(sig, frame):
 25    global global_node
 26    if global_node is not None:
 27        if global_node.is_holding_focus():
 28            global_node.send_request(False)
 29        global_node.get_logger().info(
 30            f"Received signal {sig}, abandon audio focus and shutting down")
 31    if rclpy.ok():
 32        rclpy.shutdown()
 33    sys.exit(0)
 34
 35
 36class FakeMicDevice():
 37    def __init__(self, raw_audio_file):
 38        self.audio_data = []
 39        self.channels = 1
 40        self.sample_rate = 16000
 41        self.bytes_per_sample = 2  # S16LE
 42        self.audio_length = 0
 43        with open(raw_audio_file, 'rb') as f:
 44            self.audio_data = list(f.read())
 45            self.audio_length = len(self.audio_data)
 46
 47        self.index = 0
 48        self.start_time = 0
 49
 50    def get_channel_count(self):
 51        return self.channels
 52
 53    def get_sample_rate(self):
 54        return self.sample_rate
 55
 56    def get_cached_data(self):
 57        now = time.monotonic()
 58        if not self.start_time:
 59            # fake init: assume audio data started 1s ago
 60            self.start_time = now - 1
 61
 62        # cached data range since last call
 63        tail = int((now - self.start_time) * self.sample_rate) * \
 64            self.channels * self.bytes_per_sample
 65        data = self.audio_data[self.index:tail]
 66        if tail < self.audio_length:
 67            self.index = tail
 68        else:
 69            # loopback
 70            self.index = 0
 71            self.start_time = now
 72        return data
 73
 74
 75class AudioStreamPlayer(Node):
 76    def __init__(self):
 77        super().__init__('audio_stream_player')
 78
 79        self.declare_parameter('raw_audio_path', '')
 80        self.raw_audio_path = self.get_parameter('raw_audio_path').value
 81
 82        self.mic_device = FakeMicDevice(self.raw_audio_path)
 83
 84        self.pkg_name = 'audio_stream_player{}'.format(os.getpid())
 85        self.focus = False
 86        self.focus_force = True
 87        self.kAudioPriority = 8
 88
 89        self.get_logger().info(f'local pkg name: {self.pkg_name}')
 90        # Create focus request/release clients
 91        self.request_client = self.create_client(
 92            RequestAudioFocus,
 93            '/aimdk_5Fmsgs/srv/RequestAudioFocus')
 94        self.release_client = self.create_client(
 95            AbandonAudioFocus,
 96            '/aimdk_5Fmsgs/srv/AbandonAudioFocus')
 97
 98        # Wait for the service to become available
 99        while not self.request_client.wait_for_service(timeout_sec=2.0):
100            self.get_logger().info('⏳ Service unavailable, waiting...')
101        self.get_logger().info('🟢 Service available, ready to send request.')
102
103        # Create focus event subscriber and raw audio publisher
104        focus_qos = QoSProfile(
105            reliability=QoSReliabilityPolicy.RELIABLE,
106            history=QoSHistoryPolicy.KEEP_LAST,
107            depth=10,
108            durability=QoSDurabilityPolicy.VOLATILE
109        )
110        self.sub = self.create_subscription(
111            FocusResponse,
112            '/aima/hal/audio/focus_response',
113            self.focus_event_callback, focus_qos
114        )
115        self.pub = self.create_publisher(
116            AudioPlayback,
117            '/aima/hal/audio/playback', 10
118        )
119
120    def send_request(self, focus: bool):
121        if focus:
122            self.send_focus_request()
123        else:
124            self.send_focus_release()
125
126    def send_focus_request(self):
127        req = RequestAudioFocus.Request()
128
129        requester = FocusRequester()
130        requester.pkg_name = self.pkg_name
131        requester.priority = self.kAudioPriority
132
133        req.focus_requester = requester
134
135        self.get_logger().info('📨 Sending RequestAudioFocus request')
136        for i in range(8):
137            future = self.request_client.call_async(req)
138            rclpy.spin_until_future_complete(self, future, timeout_sec=0.25)
139
140            if future.done():
141                break
142
143            # retry as remote peer is NOT handled well by ROS
144            self.get_logger().info(f'trying ... [{i}]')
145
146        response = future.result()
147        if response is None:
148            self.get_logger().error('❌ Service call failed or timed out.')
149            return False
150
151        if response.reponse.status.value == CommonState.SUCCESS:  # SUCCESS (1)
152            focus = response.focus_response.focus_gain
153            self.focus = focus
154            self.get_logger().info(f'✅ RequestAudioFocus done: focus {focus}')
155            return True
156        else:
157            self.get_logger().error(
158                f'❌ Failed in response of RequestAudioFocus: {response.reponse.message}'
159            )
160            return False
161
162    def send_focus_release(self):
163        req = AbandonAudioFocus.Request()
164
165        requester = FocusRequester()
166        requester.pkg_name = self.pkg_name
167        requester.priority = self.kAudioPriority  # must match RequestAudioFocus
168
169        req.focus_requester = requester
170
171        self.get_logger().info('📨 Sending AbandonAudioFocus request')
172        for i in range(8):
173            future = self.release_client.call_async(req)
174            rclpy.spin_until_future_complete(self, future, timeout_sec=0.25)
175
176            if future.done():
177                break
178
179            # retry as remote peer is NOT handled well by ROS
180            self.get_logger().info(f'trying ... [{i}]')
181
182        response = future.result()
183        if response is None:
184            self.get_logger().error('❌ Service call failed or timed out.')
185            return False
186
187        if response.reponse.status.value == CommonState.SUCCESS:  # SUCCESS (1)
188            focus = response.focus_response.focus_gain
189            # always focus False
190            self.focus = focus
191            self.get_logger().info(f'✅ AbandonAudioFocus done: focus {focus}')
192            return True
193        else:
194            self.get_logger().error(
195                f'❌ Failed in response of AbandonAudioFocus: {response.reponse.message}'
196            )
197            return False
198
199    def focus_event_callback(self, msg: FocusResponse):
200        pkg_name = msg.pkg_name
201        focus = msg.focus_gain
202        if msg.pkg_name == self.pkg_name:
203            self.get_logger().info(
204                f'receive focus out event: focus state: {focus}')
205            self.focus_force = msg.focus_gain
206
207    def is_holding_focus(self):
208        return self.focus and self.focus_force
209
210    def spin_until_focus(self):
211        while rclpy.ok():
212            self.focus_force = True
213            self.send_request(True)
214            if self.is_holding_focus():
215                return
216            self.get_logger().info("need retry to get focus")
217            t1 = t0 = time.monotonic()
218            while rclpy.ok() and t1 - t0 < 1.0:
219                rclpy.spin_once(self, timeout_sec=0.1)
220                t1 = time.monotonic()
221        return
222
223    def run_once(self):
224        """ flush raw audio data cached since last call """
225
226        raw_audio_msg = AudioPlayback()
227
228        raw_audio_msg.pkg_name = self.pkg_name
229        raw_audio_msg.token_id = self.pkg_name
230        # fill AudioInfo
231        audio_info = AudioInfo()
232        audio_info.channels = self.mic_device.get_channel_count()
233        audio_info.sample_rate = self.mic_device.get_sample_rate()
234        # fill raw audio data
235        audio_data = AudioData()
236        cached_data = self.mic_device.get_cached_data()
237        audio_data.data = cached_data
238        raw_audio_msg.info = audio_info
239        raw_audio_msg.data = audio_data
240        # publish
241        self.pub.publish(raw_audio_msg)
242
243    def run(self):
244        self.get_logger().info('🟢 publishing audio data ...')
245        while rclpy.ok() and self.is_holding_focus():
246            self.run_once()
247            rclpy.spin_once(self, timeout_sec=0.05)
248            time.sleep(0.05)
249
250        if not self.is_holding_focus():
251            self.get_logger().info("focus out, exiting...")
252
253
254def main(args=None):
255    global global_node
256    rclpy.init(args=args)
257    node = None
258
259    try:
260        node = AudioStreamPlayer()
261
262        global_node = node
263        signal.signal(signal.SIGINT, signal_handler)
264        signal.signal(signal.SIGTERM, signal_handler)
265
266        node.spin_until_focus()
267        node.run()
268
269    except KeyboardInterrupt:
270        rclpy.logging.get_logger('main').info(
271            "Interrupt signal received, exiting...")
272    except Exception as e:
273        rclpy.logging.get_logger('main').error(
274            f'Program exited with exception: {e}')
275
276    if node:
277        node.destroy_node()
278    if rclpy.ok():
279        rclpy.shutdown()
280
281
282if __name__ == '__main__':
283    main()

使用说明

# 注意替换/path/to/your/raw_audio_file.pcm为原始音频文件路径,单通道,采样率16kHz,格式S16LE
ros2 run py_examples play_audio_stream --ros-args -p raw_audio_path:=/path/to/your/raw_audio_file.pcm

6.1.26 表情控制

该示例中用到了play_emoji,通过该节点可实现表达指定的表情,用户可根据已有的表情列表来选择要变化的表情,具体表情列表可参考表情列表

 1#!/usr/bin/env python3
 2
 3import rclpy
 4import rclpy.logging
 5from rclpy.node import Node
 6
 7from aimdk_msgs.srv import PlayEmoji
 8
 9
10class PlayEmojiClient(Node):
11    def __init__(self):
12        super().__init__('play_emoji_client')
13        self.client = self.create_client(
14            PlayEmoji, '/aimdk_5Fmsgs/srv/PlayEmoji')
15        self.get_logger().info('✅ PlayEmoji client node created.')
16
17        # Wait for the service to become available
18        while not self.client.wait_for_service(timeout_sec=2.0):
19            self.get_logger().info('⏳ Service unavailable, waiting...')
20
21        self.get_logger().info('🟢 Service available, ready to send request.')
22
23    def send_request(self, emoji: int, mode: int, priority: int):
24        req = PlayEmoji.Request()
25
26        req.emotion_id = int(emoji)
27        req.mode = int(mode)
28        req.priority = int(priority)
29
30        self.get_logger().info(
31            f'📨 Sending request to play emoji: id={emoji}, mode={mode}, priority={priority}')
32        for i in range(8):
33            req.header.header.stamp = self.get_clock().now().to_msg()
34            future = self.client.call_async(req)
35            rclpy.spin_until_future_complete(self, future, timeout_sec=0.25)
36
37            if future.done():
38                break
39
40            # retry as remote peer is NOT handled well by ROS
41            self.get_logger().info(f'trying ... [{i}]')
42
43        resp = future.result()
44        if resp is None:
45            self.get_logger().error('❌ Service call not completed or timed out.')
46            return False
47
48        if resp.success:
49            self.get_logger().info(
50                f'✅ Emoji played successfully: {resp.message}')
51            return True
52        else:
53            self.get_logger().error(f'❌ Failed to play emoji: {resp.message}')
54            return False
55
56
57def main(args=None):
58    rclpy.init(args=args)
59    node = None
60
61    # Interactive input, same as the original C++ version
62    try:
63        emotion = int(
64            input("Enter emoji ID: 1-blink, 60-bored, 70-abnormal, 80-sleeping, 90-happy ... 190-double angry, 200-adore: "))
65        mode = int(input("Enter play mode (1: play once, 2: loop): "))
66        if mode not in (1, 2):
67            raise ValueError("invalid mode")
68        priority = 10  # default priority
69
70        node = PlayEmojiClient()
71        node.send_request(emotion, mode, priority)
72    except KeyboardInterrupt:
73        pass
74    except Exception as e:
75        rclpy.logging.get_logger('main').error(
76            f'Program exited with exception: {e}')
77
78    if node:
79        node.destroy_node()
80    if rclpy.ok():
81        rclpy.shutdown()
82
83
84if __name__ == '__main__':
85    main()

6.1.27 地图构建

该示例中用到了slam,通过该节点给建图服务发送消息,实现建图功能

 1import rclpy
 2from rclpy.node import Node
 3from rclpy.qos import QoSProfile, QoSDurabilityPolicy, QoSReliabilityPolicy
 4from std_msgs.msg import String
 5
 6
 7class SlamCommandPublisher(Node):
 8    def __init__(self):
 9        super().__init__('slam_command_publisher')
10        # Match SLAM subscriber QoS: TRANSIENT_LOCAL + RELIABLE
11        qos = QoSProfile(
12            depth=10,
13            durability=QoSDurabilityPolicy.TRANSIENT_LOCAL,
14            reliability=QoSReliabilityPolicy.RELIABLE,
15        )
16        self.publisher_ = self.create_publisher(
17            String, '/integrated_command', qos)
18
19    def publish_start_mapping(self):
20        msg = String()
21        msg.data = 'start_mapping'
22        self.get_logger().info(f'Publishing: [{msg.data}]')
23        self.publisher_.publish(msg)
24
25    def publish_stop_mapping(self, map_name):
26        msg = String()
27        msg.data = f'stop_mapping:{map_name}'
28        self.get_logger().info(f'Publishing: [{msg.data}]')
29        self.publisher_.publish(msg)
30
31
32def main(args=None):
33    rclpy.init(args=args)
34    slam_command_publisher = SlamCommandPublisher()
35
36    try:
37        while rclpy.ok():
38            input_value = input(
39                "Enter 1 to start mapping, 2 to stop mapping: ")
40
41            if input_value == '1':
42                slam_command_publisher.publish_start_mapping()
43            elif input_value == '2':
44                map_name = input("Enter map name: ")
45                slam_command_publisher.publish_stop_mapping(map_name)
46            else:
47                print("Invalid input. Please enter 1 or 2.")
48    except KeyboardInterrupt:
49        pass
50    finally:
51        slam_command_publisher.destroy_node()
52        if rclpy.ok():
53            rclpy.shutdown()
54
55
56if __name__ == '__main__':
57    main()

使用说明:

ros2 run py_examples slam
Enter 1 to start mapping, 2 to stop mapping: 1
[INFO] [1772521830.713768092] [slam_command_publisher]: Publishing: [start_mapping]
Enter 1 to start mapping, 2 to stop mapping: 2
Enter map name: testmap
[INFO] [1772521837.760016698] [slam_command_publisher]: Publishing: [stop_mapping:testmap]

开始建图之后,推动机器,或者控制机器人移动,完成建图

6.1.28 使用地图进行重定位

该示例中用到地图构建步骤完成构建的地图进行定位

  1#!/usr/bin/env python3
  2
  3import rclpy
  4from rclpy.node import Node
  5from std_msgs.msg import String
  6from geometry_msgs.msg import Pose
  7from nav_msgs.msg import Odometry
  8from rclpy.qos import QoSProfile, QoSReliabilityPolicy, QoSDurabilityPolicy
  9
 10
 11class RelocalizationNode(Node):
 12    def __init__(self, map_id, init_x, init_y):
 13        super().__init__('relocalization_node')
 14
 15        self.map_id = map_id
 16        self.init_x = init_x
 17        self.init_y = init_y
 18
 19        # /integrated_command subscriber requires TRANSIENT_LOCAL durability
 20        command_qos = QoSProfile(
 21            depth=10,
 22            durability=QoSDurabilityPolicy.TRANSIENT_LOCAL,
 23        )
 24
 25        # Create publishers
 26        self.integrated_command_pub = self.create_publisher(
 27            String, '/integrated_command', command_qos)
 28        self.relocalization_pose_pub = self.create_publisher(
 29            Pose, '/relocalization_pose', 10)
 30
 31        # Create subscriber with BEST_EFFORT QoS
 32        lidar_loc_qos = QoSProfile(
 33            depth=10,
 34            reliability=QoSReliabilityPolicy.BEST_EFFORT
 35        )
 36
 37        self.odometry_sub = self.create_subscription(
 38            Odometry,
 39            '/slam/lidar_odom',
 40            self.odometry_callback,
 41            qos_profile=lidar_loc_qos
 42        )
 43
 44        # Setup timer to publish messages in sequence
 45        self.success_received = False
 46        self.timeout_timer = None
 47
 48        # Wait for subscribers before publishing
 49        self.publish_timer = self.create_timer(0.2, self.wait_and_publish)
 50
 51    def wait_and_publish(self):
 52        if (self.integrated_command_pub.get_subscription_count() == 0 or
 53                self.relocalization_pose_pub.get_subscription_count() == 0):
 54            self.get_logger().info(
 55                f'Waiting for subscribers... '
 56                f'(command: {self.integrated_command_pub.get_subscription_count()}, '
 57                f'pose: {self.relocalization_pose_pub.get_subscription_count()})',
 58                throttle_duration_sec=2.0)
 59            return
 60        self.publish_timer.cancel()
 61        self.publish_sequence()
 62
 63    def publish_sequence(self):
 64        # Publish integrated_command
 65        integrated_command_msg = String()
 66        integrated_command_msg.data = f'start_relocalization:{self.map_id}'
 67        self.integrated_command_pub.publish(integrated_command_msg)
 68        self.get_logger().info('Published integrated_command')
 69
 70        # Schedule relocalization_pose publication after delay
 71        self.relocalization_pose_timer = self.create_timer(
 72            1.0,
 73            self.publish_relocalization_pose
 74        )
 75
 76    def publish_relocalization_pose(self):
 77        # Cancel this timer immediately so it only fires once
 78        self.relocalization_pose_timer.cancel()
 79
 80        relocalization_pose_msg = Pose()
 81        relocalization_pose_msg.position.x = self.init_x
 82        relocalization_pose_msg.position.y = self.init_y
 83        relocalization_pose_msg.position.z = 0.0
 84        relocalization_pose_msg.orientation.x = 0.0
 85        relocalization_pose_msg.orientation.y = 0.0
 86        relocalization_pose_msg.orientation.z = 0.0
 87        relocalization_pose_msg.orientation.w = 1.0
 88
 89        self.relocalization_pose_pub.publish(relocalization_pose_msg)
 90        self.get_logger().info('Published relocalization_pose')
 91
 92        # Start timeout timer (30 seconds)
 93        self.timeout_timer = self.create_timer(
 94            30.0,
 95            self.timeout_callback
 96        )
 97
 98        self.get_logger().info('Waiting for robot pose data (timeout: 30s)...')
 99
100    def odometry_callback(self, msg):
101        if not self.success_received:
102            self.success_received = True
103            if self.timeout_timer is not None:
104                self.timeout_timer.cancel()
105            self.get_logger().info('Received odometry data - Relocalization successful!')
106            if rclpy.ok():
107                rclpy.shutdown()
108
109    def timeout_callback(self):
110        if not self.success_received:
111            self.get_logger().error('Timeout reached - Relocalization failed!')
112            if rclpy.ok():
113                rclpy.shutdown()
114
115
116def main(args=None):
117    rclpy.init(args=args)
118
119    # Inputs needed for relocalization. To find them, see:
120    #   map_id        -> sqlite3 /agibot/data/var/MapManagerModule/map.db
121    #                    "SELECT * FROM map;"
122    #   init_x/init_y -> origin (u, v) in
123    #                    /agibot/data/var/MapManagerModule/<map_id>/grid_map_info.txt
124    #                    (line 2; pixel coordinates)
125    try:
126        map_id = int(input("Enter map_id: ").strip())
127        init_x = float(input("Enter initial position.x (pixel u): ").strip())
128        init_y = float(input("Enter initial position.y (pixel v): ").strip())
129    except (ValueError, EOFError):
130        print("Invalid input, aborted.")
131        if rclpy.ok():
132            rclpy.shutdown()
133        return
134
135    node = RelocalizationNode(map_id, init_x, init_y)
136    try:
137        rclpy.spin(node)
138    except KeyboardInterrupt:
139        pass
140    finally:
141        if rclpy.ok():
142            rclpy.shutdown()
143
144
145if __name__ == '__main__':
146    main()

使用说明:

ros2 run py_examples relocate
Enter map_id: 1778133306741
Enter initial position.x (pixel u): 1041
Enter initial position.y (pixel v): 630
[INFO] [1772526167.342953490] [relocalization_node]: Published integrated_command
[INFO] [1772526168.346034723] [relocalization_node]: Published relocalization_pose
[INFO] [1772526168.346915278] [relocalization_node]: Waiting for robot pose data (timeout: 30s)...
[INFO] [1772526173.347455156] [relocalization_node]: Received odometry data - Relocalization successful!

需要将机器人移动到地图构建的起始点,然后运行重定位程序

参数获取方式:map_id 与起点像素坐标(position.x / position.y)的获取见 地图标识与坐标的获取。

6.1.29 获取已存储的地图

该示例中用到了get_map,按地图名称获取机器人上已存储的地图数据,包括地图路径、分辨率、宽高及地图ID等信息。

 1#!/usr/bin/env python3
 2
 3import rclpy
 4from rclpy.node import Node
 5from rclpy.executors import SingleThreadedExecutor
 6from aimdk_msgs.srv import GetStoredMapByName
 7from std_msgs.msg import Header
 8
 9
10class MapClient(Node):
11    def __init__(self):
12        super().__init__('get_map_client')
13        self.client = self.create_client(
14            GetStoredMapByName,
15            '/aimdk_5Fmsgs/srv/GetStoredMapByName'
16        )
17
18    def send_request(self, map_name):
19        while not self.client.wait_for_service(timeout_sec=1.0):
20            if not rclpy.ok():
21                self.get_logger().error("Interrupted while waiting for service.")
22                return
23            self.get_logger().info("Service not available, waiting again...")
24
25        request = GetStoredMapByName.Request()
26
27        request.header = Header()
28        request.header.stamp = self.get_clock().now().to_msg()
29        request.header.frame_id = ""
30
31        request.map_name = map_name
32
33        future = self.client.call_async(request)
34
35        executor = SingleThreadedExecutor()
36        executor.add_node(self)
37        executor.spin_until_future_complete(future, timeout_sec=60.0)
38        executor.remove_node(self)
39
40        if future.result() is None:
41            self.get_logger().error("Service call failed or timed out.")
42            return
43
44        result = future.result()
45
46        if result.code == 0:
47            self.get_logger().info("Service call succeeded")
48            self.get_logger().info(f"Map path: {result.map_path}")
49            self.get_logger().info(
50                f"Map resolution: {result.map_info.resolution}")
51            self.get_logger().info(f"Map width: {result.map_info.width}")
52            self.get_logger().info(f"Map height: {result.map_info.height}")
53            self.get_logger().info(f"Map id: {result.map_id}")
54        else:
55            self.get_logger().error(
56                f"Service call failed with code: {result.code}")
57
58
59def main(args=None):
60    rclpy.init(args=args)
61
62    client = MapClient()
63    try:
64        # Map name (NOT map_id) is required. To list stored maps, run:
65        #   sqlite3 /agibot/data/var/MapManagerModule/map.db "SELECT * FROM map;"
66        map_name = input("Enter map name: ").strip()
67        if not map_name:
68            client.get_logger().error("Map name is empty, aborted.")
69            return
70        client.send_request(map_name)
71    except KeyboardInterrupt:
72        pass
73    finally:
74        if rclpy.ok():
75            rclpy.shutdown()
76
77
78if __name__ == '__main__':
79    main()

使用说明:

ros2 run py_examples get_map
Enter map name: test_map
[INFO] [get_map_client]: Service call succeeded
[INFO] [get_map_client]: Map path: /path/to/test_map.png
[INFO] [get_map_client]: Map resolution: 0.05
[INFO] [get_map_client]: Map width: 200
[INFO] [get_map_client]: Map height: 200
[INFO] [get_map_client]: Map id: 1778133306741

参数获取方式:输入的是地图名称(map_name),不是 map_id;map_name 的获取见 地图标识与坐标的获取。

6.1.30 导航

该示例提供了交互式终端菜单,支持导航任务的开始、暂停、恢复和停止操作。

  1#!/usr/bin/env python3
  2"""
  3Navigation example: publishes navigation task requests (start/pause/resume/stop)
  4via an interactive terminal menu.
  5"""
  6
  7import time
  8
  9import numpy as np
 10import rclpy
 11from rclpy.node import Node
 12
 13from aimdk_msgs.msg import MessageHeader, PncTaskRequest
 14from geometry_msgs.msg import PoseStamped
 15
 16STARTUP_DELAY = 5.0
 17
 18# task_request enum
 19TASK_REQUEST_START = 1
 20TASK_REQUEST_STOP = 2
 21TASK_REQUEST_PAUSE = 3
 22TASK_REQUEST_RESUME = 4
 23
 24
 25class NavigationExample(Node):
 26
 27    def __init__(self):
 28        super().__init__('navigation_example')
 29        self.publisher = self.create_publisher(
 30            PncTaskRequest,
 31            '/aima/te/pnc_task_request',
 32            10,
 33        )
 34        self.task_id = int(time.time() * 1000)
 35        self.map_id = 0
 36        self.get_logger().info(
 37            f'Navigation example node started, task_id={self.task_id}')
 38
 39    def _make_header(self) -> MessageHeader:
 40        header = MessageHeader()
 41        now = self.get_clock().now().to_msg()
 42        header.stamp = now
 43        header.meas_stamp = now
 44        header.frame_id = 'map'
 45        header.sequence = 0
 46        return header
 47
 48    def send_start(self, map_id: int, x: float, y: float, z: float,
 49                   ox: float, oy: float, oz: float, ow: float,
 50                   radius: float, pnc_mode: int, max_speed: float):
 51        """Start navigation."""
 52        self.map_id = map_id
 53        msg = PncTaskRequest()
 54        msg.header = self._make_header()
 55        msg.task_type = 2
 56        msg.task_request = TASK_REQUEST_START
 57        msg.pnc_mode = pnc_mode
 58        msg.max_forward_speed = max_speed
 59        msg.task_id = self.task_id
 60        msg.map_id = map_id
 61        msg.target_pose_radius = radius
 62        msg.reserve_info = np.zeros(64, dtype=np.uint8)
 63
 64        target = PoseStamped()
 65        target.header.stamp = self.get_clock().now().to_msg()
 66        target.header.frame_id = 'map'
 67        target.pose.position.x = x
 68        target.pose.position.y = y
 69        target.pose.position.z = z
 70        target.pose.orientation.x = ox
 71        target.pose.orientation.y = oy
 72        target.pose.orientation.z = oz
 73        target.pose.orientation.w = ow
 74        msg.target_pose = target
 75
 76        self.publisher.publish(msg)
 77        self.get_logger().info(
 78            f'[Start] task_id={self.task_id}, map_id={map_id}, '
 79            f'target=({x}, {y}, {z}), radius={radius}'
 80        )
 81
 82    def send_pause(self):
 83        """Pause navigation."""
 84        msg = PncTaskRequest()
 85        msg.header = self._make_header()
 86        msg.task_type = 2
 87        msg.task_request = TASK_REQUEST_PAUSE
 88        msg.pnc_mode = 0
 89        msg.max_forward_speed = 0.5
 90        msg.task_id = self.task_id
 91        msg.map_id = 0
 92        msg.target_pose_radius = 0.0
 93        msg.reserve_info = np.zeros(64, dtype=np.uint8)
 94
 95        target = PoseStamped()
 96        target.pose.orientation.w = 1.0
 97        msg.target_pose = target
 98
 99        self.publisher.publish(msg)
100        self.get_logger().info(f'[Pause] task_id={self.task_id}')
101
102    def send_resume(self):
103        """Resume navigation."""
104        msg = PncTaskRequest()
105        msg.header = self._make_header()
106        msg.task_type = 2
107        msg.task_request = TASK_REQUEST_RESUME
108        msg.pnc_mode = 0
109        msg.max_forward_speed = 0.5
110        msg.task_id = self.task_id
111        msg.map_id = self.map_id
112        msg.target_pose_radius = 0.0
113        msg.reserve_info = np.zeros(64, dtype=np.uint8)
114
115        target = PoseStamped()
116        target.pose.orientation.w = 1.0
117        msg.target_pose = target
118
119        self.publisher.publish(msg)
120        self.get_logger().info(
121            f'[Resume] task_id={self.task_id}, map_id={self.map_id}')
122
123    def send_stop(self):
124        """Stop navigation."""
125        msg = PncTaskRequest()
126        msg.header = self._make_header()
127        msg.task_type = 2
128        msg.task_request = TASK_REQUEST_STOP
129        msg.pnc_mode = 0
130        msg.max_forward_speed = 0.5
131        msg.task_id = self.task_id
132        msg.map_id = self.map_id
133        msg.target_pose_radius = 0.0
134        msg.reserve_info = np.zeros(64, dtype=np.uint8)
135
136        target = PoseStamped()
137        target.pose.orientation.w = 1.0
138        msg.target_pose = target
139
140        self.publisher.publish(msg)
141        self.get_logger().info(
142            f'[Stop] task_id={self.task_id}, map_id={self.map_id}')
143
144
145def _input_float(prompt: str, default: float) -> float:
146    raw = input(f'{prompt} [{default}]: ').strip()
147    if not raw:
148        return default
149    return float(raw)
150
151
152def _input_int(prompt: str, default: int) -> int:
153    raw = input(f'{prompt} [{default}]: ').strip()
154    if not raw:
155        return default
156    return int(raw)
157
158
159def print_menu(task_id: int):
160    print('\n' + '=' * 40)
161    print(f'  Navigation Control  (task_id={task_id})')
162    print('=' * 40)
163    print('  1. Start navigation')
164    print('  2. Pause navigation')
165    print('  3. Resume navigation')
166    print('  4. Stop navigation')
167    print('  q. Quit')
168    print('=' * 40)
169
170
171def main():
172    rclpy.init()
173    nav = NavigationExample()
174
175    nav.get_logger().info(
176        f'Waiting for node communication ready ({STARTUP_DELAY}s)...')
177    time.sleep(STARTUP_DELAY)
178
179    try:
180        while True:
181            print_menu(nav.task_id)
182            choice = input('Select an option: ').strip()
183
184            if choice == '1':
185                print('\n--- Start Navigation Parameters ---')
186                map_id = _input_int('Map ID (map_id)', 1773113429735)
187                x = _input_float('Target x', 1.0)
188                y = _input_float('Target y', 2.0)
189                z = _input_float('Target z', 0.0)
190                ox = _input_float('Orientation x', 0.0)
191                oy = _input_float('Orientation y', 0.0)
192                oz = _input_float('Orientation z', 0.0)
193                ow = _input_float('Orientation w', 1.0)
194                radius = _input_float('Target pose radius', 0.5)
195                pnc_mode = _input_int('pnc_mode', 0)
196                max_speed = _input_float('Max forward speed', 0.5)
197                nav.send_start(map_id, x, y, z, ox, oy, oz,
198                               ow, radius, pnc_mode, max_speed)
199
200            elif choice == '2':
201                nav.send_pause()
202
203            elif choice == '3':
204                nav.send_resume()
205
206            elif choice == '4':
207                nav.send_stop()
208
209            elif choice.lower() == 'q':
210                print('Exiting navigation control.')
211                break
212
213            else:
214                print('Invalid input, please try again.')
215
216    except KeyboardInterrupt:
217        nav.get_logger().info('Interrupted by user (Ctrl+C)')
218    finally:
219        nav.destroy_node()
220        rclpy.shutdown()
221
222
223if __name__ == '__main__':
224    main()

使用说明:

ros2 run py_examples navigation

输出示例:

[INFO] [navigation_example]: Navigation example node started, task_id=1709123456789
[INFO] [navigation_example]: Waiting for node communication ready (5.0s)...

========================================
  Navigation Control  (task_id=1709123456789)
========================================
  1. Start navigation
  2. Pause navigation
  3. Resume navigation
  4. Stop navigation
  q. Quit
========================================
Select an option: 1

--- Start Navigation Parameters ---
Map ID (map_id) [1773113429735]:
Target x [1.0]:
Target y [2.0]:
...
[INFO] [navigation_example]: [Start] task_id=1709123456789, map_id=1773113429735, target=(1.0, 2.0, 0.0), radius=0.5

开始导航时需输入地图ID、目标位姿、到点精度等参数,每项都有默认值,直接回车即可使用默认值。

参数获取方式:map_id 的获取见 地图标识与坐标的获取;target_x / target_y 为目标点米制坐标(map 坐标系),与重定位用的像素坐标不同,根据实际任务直接指定

6.1.31 播放灵创动作

该示例中用到了play_linkcraft,列出机器人上的灵创动作,并且选择一条进行播放

  1#!/usr/bin/env python3
  2
  3import rclpy
  4from rclpy.node import Node
  5from aimdk_msgs.srv import GetRobotResources, ExecuteActionResource
  6
  7
  8class RobotResourceClient(Node):
  9    def __init__(self):
 10        super().__init__('robot_resource_client')
 11
 12        self.get_resource_client = self.create_client(
 13            GetRobotResources,
 14            '/aimdk_5Fmsgs/srv/GetRobotResources'
 15        )
 16        self.play_resource_client = self.create_client(
 17            ExecuteActionResource,
 18            '/aimdk_5Fmsgs/srv/ExecuteActionResource'
 19        )
 20
 21        self.resources = []
 22
 23    def get_robot_resources(self):
 24        while not self.get_resource_client.wait_for_service(timeout_sec=1.0):
 25            if not rclpy.ok():
 26                self.get_logger().error("Interrupted while waiting for the service. Exiting.")
 27                return False
 28            self.get_logger().info("Service not available, waiting again...")
 29
 30        request = GetRobotResources.Request()
 31        future = self.get_resource_client.call_async(request)
 32        rclpy.spin_until_future_complete(self, future)
 33
 34        if future.result() is None:
 35            self.get_logger().error("Failed to call GetRobotResources")
 36            return False
 37
 38        response = future.result()
 39        self.resources = list(response.robot_resources)
 40        self.get_logger().info(f"Found {len(self.resources)} resources")
 41        return True
 42
 43    def print_resource_menu(self):
 44        if not self.resources:
 45            print("(no resources)")
 46            return
 47
 48        print("\n=== Available Robot Resources ===")
 49        for idx, resource in enumerate(self.resources):
 50            ver = resource.current_version
 51            print(f"[{idx}] {ver.name or '<no name>'}")
 52            print(f"     key:     {resource.resource_key}")
 53            print(f"     version: {ver.version}")
 54        print("=================================\n")
 55
 56    def pick_resource(self):
 57        if not self.resources:
 58            return None
 59
 60        while True:
 61            try:
 62                raw = input(
 63                    f"Select resource index [0-{len(self.resources) - 1}] (q to quit): ").strip()
 64            except EOFError:
 65                return None
 66            if raw.lower() in ("q", "quit", "exit"):
 67                return None
 68            if not raw.isdigit():
 69                print("Please enter a number.")
 70                continue
 71            idx = int(raw)
 72            if 0 <= idx < len(self.resources):
 73                return self.resources[idx]
 74            print(f"Index out of range (0-{len(self.resources) - 1}).")
 75
 76    def play_resource(self, resource):
 77        while not self.play_resource_client.wait_for_service(timeout_sec=1.0):
 78            if not rclpy.ok():
 79                self.get_logger().error("Interrupted while waiting for service.")
 80                return
 81            self.get_logger().info("Service not available, waiting...")
 82
 83        request = ExecuteActionResource.Request()
 84        request.resource_key = resource.resource_key
 85        request.resource_version = resource.current_version.version
 86
 87        if "onnx" in request.resource_key:
 88            request.meta = '{"resource_type": "BODY_MONTION"}'
 89        else:
 90            request.meta = '{"resource_type": "ARM_MONTION"}'
 91
 92        self.get_logger().info("Sending request:")
 93        self.get_logger().info(f"  resource_key: {request.resource_key}")
 94        self.get_logger().info(
 95            f"  resource_version: {request.resource_version}")
 96        self.get_logger().info(f"  meta: {request.meta}")
 97
 98        future = self.play_resource_client.call_async(request)
 99        rclpy.spin_until_future_complete(self, future)
100
101        if future.result() is not None:
102            self.handle_response(future.result())
103        else:
104            self.get_logger().error("Failed to call ExecuteActionResource")
105
106    def handle_response(self, response):
107        self.get_logger().info("Service response received:")
108        self.get_logger().info("Response Header:")
109        self.get_logger().info(
110            f"  stamp: {response.header.header.stamp.sec}.{response.header.header.stamp.nanosec:09}")
111        self.get_logger().info(f"  code: {response.header.header.code}")
112        self.get_logger().info("Status:")
113        self.get_logger().info(f"  value: {response.header.status.value}")
114        self.get_logger().info(f"  message: {response.header.message}")
115
116
117def main(args=None):
118    rclpy.init(args=args)
119    client_node = None
120    try:
121        client_node = RobotResourceClient()
122        if not client_node.get_robot_resources():
123            return
124        client_node.print_resource_menu()
125        chosen = client_node.pick_resource()
126        if chosen is None:
127            client_node.get_logger().info("No resource selected, exiting.")
128            return
129        client_node.play_resource(chosen)
130    except KeyboardInterrupt:
131        pass
132    finally:
133        if client_node:
134            client_node.destroy_node()
135        if rclpy.ok():
136            rclpy.shutdown()
137
138
139if __name__ == '__main__':
140    main()

使用说明:

ros2 run py_examples play_linkcraft
[INFO] [1772526701.308025739] [robot_resource_client]: Found 1 resources

=== Available Robot Resources ===
[0] <no name>
     key:     linkcraft_resource_onnx_01KBM2BHNFM93Z0DK6ES9F6DJA
     version: 0.0.1
=================================

[INFO] [1772526701.308537895] [robot_resource_client]: Sending request:
[INFO] [1772526701.308692872] [robot_resource_client]:   resource_key: linkcraft_resource_onnx_01KBM2BHNFM93Z0DK6ES9F6DJA
[INFO] [1772526701.308853424] [robot_resource_client]:   resource_version: 0.0.1
[INFO] [1772526701.309014801] [robot_resource_client]:   meta: {"resource_type": "BODY_MONTION"}
[INFO] [1772526701.338307933] [robot_resource_client]: Service response received:
[INFO] [1772526701.338480169] [robot_resource_client]: Response Header:
[INFO] [1772526701.338647069] [robot_resource_client]:   stamp: 0.000000000
[INFO] [1772526701.338800125] [robot_resource_client]:   code: 0
[INFO] [1772526701.338951109] [robot_resource_client]: Status:
[INFO] [1772526701.339106459] [robot_resource_client]:   value: 1
[INFO] [1772526701.339239979] [robot_resource_client]:   message: 1/1 task(s) failed. Details: localhost: failed (status code: 400) - Failed with status code: 400

6.1.32 群控播放灵创动作

该示例中用到了 multi_play_linkcraft,在多台机器人之间进行时间同步,并群控播放灵创动作。

  1#!/usr/bin/env python3
  2
  3import rclpy
  4from rclpy.node import Node
  5from aimdk_msgs.msg import SlaveDevice
  6from aimdk_msgs.srv import GetRobotResources, ExecuteActionResource, GroupControlTimeSync
  7
  8
  9class RobotResourceClient(Node):
 10    def __init__(self):
 11        super().__init__('robot_resource_client')
 12
 13        self.get_resource_client = self.create_client(
 14            GetRobotResources,
 15            '/aimdk_5Fmsgs/srv/GetRobotResources'
 16        )
 17        self.play_resource_client = self.create_client(
 18            ExecuteActionResource,
 19            '/aimdk_5Fmsgs/srv/ExecuteActionResource'
 20        )
 21        self.time_sync_client = self.create_client(
 22            GroupControlTimeSync,
 23            '/aimdk_5Fmsgs/srv/GroupControlTimeSync'
 24        )
 25
 26        self.resources = []
 27        self.slave_ips = []
 28
 29    def get_robot_resources(self):
 30        while not self.get_resource_client.wait_for_service(timeout_sec=1.0):
 31            if not rclpy.ok():
 32                self.get_logger().error("Interrupted while waiting for the service. Exiting.")
 33                return False
 34            self.get_logger().info("Service not available, waiting again...")
 35
 36        request = GetRobotResources.Request()
 37        future = self.get_resource_client.call_async(request)
 38        rclpy.spin_until_future_complete(self, future)
 39
 40        if future.result() is None:
 41            self.get_logger().error("Failed to call GetRobotResources")
 42            return False
 43
 44        response = future.result()
 45        self.resources = list(response.robot_resources)
 46        self.get_logger().info(f"Found {len(self.resources)} resources")
 47        return True
 48
 49    def print_resource_menu(self):
 50        if not self.resources:
 51            print("(no resources)")
 52            return
 53
 54        print("\n=== Available Robot Resources ===")
 55        for idx, resource in enumerate(self.resources):
 56            ver = resource.current_version
 57            print(f"[{idx}] {ver.name or '<no name>'}")
 58            print(f"     key:     {resource.resource_key}")
 59            print(f"     version: {ver.version}")
 60        print("=================================\n")
 61
 62    def pick_resource(self):
 63        if not self.resources:
 64            return None
 65
 66        while True:
 67            try:
 68                raw = input(
 69                    f"Select resource index [0-{len(self.resources) - 1}] (q to quit): ").strip()
 70            except EOFError:
 71                return None
 72            if raw.lower() in ("q", "quit", "exit"):
 73                return None
 74            if not raw.isdigit():
 75                print("Please enter a number.")
 76                continue
 77            idx = int(raw)
 78            if 0 <= idx < len(self.resources):
 79                return self.resources[idx]
 80            print(f"Index out of range (0-{len(self.resources) - 1}).")
 81
 82    def play_resource(self, resource):
 83        while not self.play_resource_client.wait_for_service(timeout_sec=1.0):
 84            if not rclpy.ok():
 85                self.get_logger().error("Interrupted while waiting for service.")
 86                return
 87            self.get_logger().info("Service not available, waiting...")
 88
 89        request = ExecuteActionResource.Request()
 90        request.resource_key = resource.resource_key
 91        request.resource_version = resource.current_version.version
 92
 93        for ip in self.slave_ips:
 94            slave = SlaveDevice()
 95            slave.ip = ip
 96            request.slaves.append(slave)
 97
 98        if "onnx" in request.resource_key:
 99            request.meta = '{"control_type": "GROUP_CONTROL","resource_type": "BODY_MONTION"}'
100        else:
101            request.meta = '{"control_type": "GROUP_CONTROL","resource_type": "ARM_MONTION"}'
102
103        self.get_logger().info("Sending request:")
104        self.get_logger().info(f"  resource_key: {request.resource_key}")
105        self.get_logger().info(
106            f"  resource_version: {request.resource_version}")
107        self.get_logger().info(f"  meta: {request.meta}")
108        for i, slave in enumerate(request.slaves):
109            self.get_logger().info(f"  slaves[{i}] ip: {slave.ip}")
110
111        future = self.play_resource_client.call_async(request)
112        rclpy.spin_until_future_complete(self, future)
113
114        if future.result() is not None:
115            self.handle_response(future.result())
116        else:
117            self.get_logger().error("Failed to call ExecuteActionResource")
118
119    def sync_time(self, ips):
120        self.slave_ips = ips
121
122        while not self.time_sync_client.wait_for_service(timeout_sec=1.0):
123            if not rclpy.ok():
124                self.get_logger().error("Interrupted while waiting for time sync service.")
125                return False
126            self.get_logger().info("TimeSync service not available, waiting again...")
127
128        request = GroupControlTimeSync.Request()
129        for ip in ips:
130            slave = SlaveDevice()
131            slave.ip = ip
132            request.slaves.append(slave)
133
134        self.get_logger().info("Sending GroupControlTimeSync request:")
135        for slave in request.slaves:
136            self.get_logger().info(f"  slave ip: {slave.ip}")
137
138        future = self.time_sync_client.call_async(request)
139        rclpy.spin_until_future_complete(self, future)
140
141        if future.result() is None:
142            self.get_logger().error("Failed to call GroupControlTimeSync")
143            return False
144
145        response = future.result()
146        self.get_logger().info("GroupControlTimeSync response:")
147        self.get_logger().info(f"  error_code: {response.error_code}")
148        self.get_logger().info(f"  error_msg: {response.error_msg}")
149        return response.error_code == 0
150
151    def handle_response(self, response):
152        self.get_logger().info("Service response received:")
153        self.get_logger().info("Response Header:")
154        self.get_logger().info(
155            f"  stamp: {response.header.header.stamp.sec}.{response.header.header.stamp.nanosec:09}")
156        self.get_logger().info(f"  code: {response.header.header.code}")
157        self.get_logger().info("Status:")
158        self.get_logger().info(f"  value: {response.header.status.value}")
159        self.get_logger().info(f"  message: {response.header.message}")
160
161
162def main(args=None):
163    rclpy.init(args=args)
164    client_node = None
165    try:
166        client_node = RobotResourceClient()
167        if not client_node.get_robot_resources():
168            return
169
170        # [USER_CONFIG] Replace the IPs below with your slave robot IPs.
171        slave_ips = ["172.22.133.178", "172.22.135.45"]
172        if not client_node.sync_time(slave_ips):
173            client_node.get_logger().warn("Time sync failed or returned error.")
174
175        client_node.print_resource_menu()
176        chosen = client_node.pick_resource()
177        if chosen is None:
178            client_node.get_logger().info("No resource selected, exiting.")
179            return
180        client_node.play_resource(chosen)
181    except KeyboardInterrupt:
182        pass
183    finally:
184        if client_node:
185            client_node.destroy_node()
186        if rclpy.ok():
187            rclpy.shutdown()
188
189
190if __name__ == '__main__':
191    main()

使用说明:

ros2 run py_examples multi_play_linkcraft
[INFO] [1772526701.308025739] [robot_resource_client]: Found 1 resources
[INFO] [1772526701.308537895] [robot_resource_client]: Sending GroupControlTimeSync request:
[INFO] [1772526701.308692872] [robot_resource_client]:   slave ip: 172.22.133.178
[INFO] [1772526701.308853424] [robot_resource_client]:   slave ip: 172.22.135.45
[INFO] [1772526701.309014801] [robot_resource_client]: GroupControlTimeSync response:
[INFO] [1772526701.309114801] [robot_resource_client]:   error_code: 0
[INFO] [1772526701.309214801] [robot_resource_client]:   error_msg: ok
[INFO] [1772526701.308537895] [robot_resource_client]: Sending request:
[INFO] [1772526701.308692872] [robot_resource_client]:   resource_key: linkcraft_resource_onnx_01KBM2BHNFM93Z0DK6ES9F6DJA
[INFO] [1772526701.308853424] [robot_resource_client]:   resource_version: 0.0.1
[INFO] [1772526701.309014801] [robot_resource_client]:   meta: {"control_type": "GROUP_CONTROL","resource_type": "BODY_MONTION"}
[INFO] [1772526701.309114801] [robot_resource_client]:   slaves[0] ip: 172.22.133.178
[INFO] [1772526701.309214801] [robot_resource_client]:   slaves[1] ip: 172.22.135.45
[INFO] [1772526701.338307933] [robot_resource_client]: Service response received:
[INFO] [1772526701.338480169] [robot_resource_client]: Response Header:
[INFO] [1772526701.338647069] [robot_resource_client]:   stamp: 0.000000000
[INFO] [1772526701.338800125] [robot_resource_client]:   code: 0
[INFO] [1772526701.338951109] [robot_resource_client]: Status:
[INFO] [1772526701.339106459] [robot_resource_client]:   value: 1
[INFO] [1772526701.339239979] [robot_resource_client]:   message: 1/1 task(s) failed. Details: localhost: failed (status code: 400) - Failed with status code: 400

6.1.33 LED灯带控制

功能说明:演示如何控制机器人的LED灯带,支持多种显示模式和自定义颜色。

注意

使用本功能前,请在运控计算单元(PC1)上执行以下命令关闭 task_manager 模块,避免其与二开程序争抢灯带控制权:

aima em stop-app task_manager

核心代码:

  1#!/usr/bin/env python3
  2
  3import sys
  4import rclpy
  5import rclpy.logging
  6from rclpy.node import Node
  7
  8from aimdk_msgs.srv import SetPmuLed
  9
 10
 11class PlayLightsClient(Node):
 12    def __init__(self):
 13        super().__init__('play_lights_client')
 14
 15        # create service client
 16        self.client = self.create_client(
 17            SetPmuLed, '/aimdk_5Fmsgs/srv/SetPmuLed')
 18
 19        self.get_logger().info('✅ PlayLights client node created.')
 20
 21        # Wait for the service to become available
 22        while not self.client.wait_for_service(timeout_sec=2.0):
 23            self.get_logger().info('⏳ Service unavailable, waiting...')
 24
 25        self.get_logger().info('🟢 Service available, ready to send request.')
 26
 27    def send_request(self, led_mode, r, g, b, priority=6, reset_priority=False):
 28        """Send LED control request"""
 29        # create request
 30        request = SetPmuLed.Request()
 31        request.led_strip_mode = led_mode
 32        request.r = r
 33        request.g = g
 34        request.b = b
 35        request.priority = priority
 36        request.reset_priority = reset_priority
 37        request.trace_id = 'play_lights'
 38
 39        # send request
 40        # Note: LED strip is slow to response (up to ~5s)
 41        self.get_logger().info(
 42            f'📨 Sending request to control led strip: mode={led_mode}, RGB=({r}, {g}, {b}), priority={priority}, reset_priority={reset_priority}')
 43        for i in range(4):
 44            request.request.header.stamp = self.get_clock().now().to_msg()
 45            future = self.client.call_async(request)
 46            rclpy.spin_until_future_complete(self, future, timeout_sec=5)
 47
 48            if future.done():
 49                break
 50
 51            # retry as remote peer is NOT handled well by ROS
 52            self.get_logger().info(f'trying ... [{i}]')
 53
 54        response = future.result()
 55        if response is None:
 56            self.get_logger().error('❌ Service call not completed or timed out.')
 57            return False
 58
 59        if response.status_code == 0:
 60            self.get_logger().info('✅ LED strip command sent successfully.')
 61            return True
 62        else:
 63            # 0x1024: priority insufficient (priority < current threshold);
 64            # use a higher priority or set reset_priority=True to reset the
 65            # threshold to 0.
 66            reason = ('priority insufficient'
 67                      if response.status_code == 0x1024
 68                      else 'failure')
 69            self.get_logger().error(
 70                f'❌ LED strip command failed with status: '
 71                f'0x{response.status_code:X} ({reason})')
 72            return False
 73
 74
 75def main(args=None):
 76    rclpy.init(args=args)
 77    node = None
 78
 79    try:
 80        # get command line args
 81        if len(sys.argv) > 6:
 82            # use CLI args
 83            led_mode = int(sys.argv[1])
 84            if led_mode not in (0, 1, 2, 3):
 85                raise ValueError("invalid mode")
 86            r = int(sys.argv[2])
 87            if r < 0 or r > 255:
 88                raise ValueError("invalid R value")
 89            g = int(sys.argv[3])
 90            if g < 0 or g > 255:
 91                raise ValueError("invalid G value")
 92            b = int(sys.argv[4])
 93            if b < 0 or b > 255:
 94                raise ValueError("invalid B value")
 95            priority = int(sys.argv[5])
 96            if priority < 0:
 97                raise ValueError("invalid priority value")
 98            reset_priority = sys.argv[6].lower() in ('true', '1', 'yes')
 99        else:
100            # interactive input
101            print("=== LED strip control example ===")
102            print("Select LED strip mode:")
103            print("0 - Steady on")
104            print("1 - Breathing (4s cycle, sine brightness)")
105            print("2 - Blinking (1s cycle, 0.5s on, 0.5s off)")
106            print("3 - Flowing (2s cycle, light up from left to right)")
107
108            led_mode = int(input("Enter mode (0-3): "))
109            if led_mode not in (0, 1, 2, 3):
110                raise ValueError("invalid mode")
111
112            print("\nSet RGB color values (0-255):")
113            r = int(input("Red (R): "))
114            if r < 0 or r > 255:
115                raise ValueError("invalid R value")
116            g = int(input("Green (G): "))
117            if g < 0 or g > 255:
118                raise ValueError("invalid G value")
119            b = int(input("Blue (B): "))
120            if b < 0 or b > 255:
121                raise ValueError("invalid B value")
122
123            print("\nSet priority (higher value = higher priority, default=6):")
124            priority = int(input("Priority: "))
125            if priority < 0:
126                raise ValueError("invalid priority value")
127
128            reset_priority = input(
129                "Reset priority after command? (y/n, default=n): ").strip().lower() in ('y', 'yes', 'true', '1')
130
131        node = PlayLightsClient()
132        node.send_request(led_mode, r, g, b, priority, reset_priority)
133    except KeyboardInterrupt:
134        pass
135    except Exception as e:
136        rclpy.logging.get_logger('main').error(
137            f'Program exited with exception: {e}')
138
139    if node:
140        node.destroy_node()
141    if rclpy.ok():
142        rclpy.shutdown()
143
144
145if __name__ == '__main__':
146    main()

使用说明:

# 构建
colcon build --packages-select py_examples

# 交互式运行
ros2 run py_examples play_lights

# 命令行参数运行: <模式> <R> <G> <B> <优先级> <重置优先级(true/false)>
ros2 run py_examples play_lights 1 255 0 0 6 false  # 模式1,红色,优先级6

输出示例:

=== LED strip control example ===
Select LED strip mode:
0 - Steady on
1 - Breathing (4s cycle, sine brightness)
2 - Blinking (1s cycle, 0.5s on, 0.5s off)
3 - Flowing (2s cycle, light up from left to right)
Enter mode (0-3): 1

Set RGB color values (0-255):
Red (R): 255
Green (G): 0
Blue (B): 0

Set priority (higher value = higher priority, default=6):
Priority: 6
Reset priority after command? (y/n, default=n): n

[INFO] [play_lights_client]: 📨 Sending request to control led strip: mode=1, RGB=(255, 0, 0), priority=6, reset_priority=False
[INFO] [play_lights_client]: ✅ LED strip command sent successfully.

技术特点:

  • 支持4种LED显示模式

  • RGB颜色自定义

  • 优先级控制(值越大优先级越高)

  • 优先级重置功能

  • 异步服务调用

  • 命令行参数支持

  • 输入参数验证

  • 友好的用户交互界面

6.1.34 GNSS定位数据订阅

该示例中用到了echo_gnss_data,通过订阅GNSS话题获取定位数据,输出经纬度、海拔和协方差信息。

功能特点:

  • 订阅NavSatFix定位数据(经纬度、海拔)

  • 实时显示定位状态和协方差

话题说明:

  • /aima/hal/sensor/gnss: 定位数据 (sensor_msgs/NavSatFix),由系统自动发布

 1#!/usr/bin/env python3
 2import time
 3import rclpy
 4from rclpy.node import Node
 5from rclpy.qos import QoSProfile, QoSReliabilityPolicy, QoSHistoryPolicy, QoSDurabilityPolicy
 6from rclpy.time import Time
 7from sensor_msgs.msg import NavSatFix, NavSatStatus
 8
 9
10STATUS_STR = {
11    NavSatStatus.STATUS_NO_FIX: 'NO_FIX',
12    NavSatStatus.STATUS_FIX: 'FIX',
13}
14
15
16class GnssPrinter(Node):
17    NAVSAT_TOPIC = '/aima/hal/sensor/gnss'
18
19    def __init__(self):
20        super().__init__('gnss_printer')
21
22        qos = QoSProfile(
23            reliability=QoSReliabilityPolicy.RELIABLE,
24            history=QoSHistoryPolicy.KEEP_LAST,
25            durability=QoSDurabilityPolicy.TRANSIENT_LOCAL,
26            depth=10,
27        )
28
29        self._last_recv = None
30        self.sub = self.create_subscription(
31            NavSatFix, self.NAVSAT_TOPIC, self.cb, qos
32        )
33        self.get_logger().info(f'Subscribing NavSatFix: {self.NAVSAT_TOPIC}')
34
35    def cb(self, msg: NavSatFix):
36        now = time.perf_counter()
37        dt_ms = 0.0 if self._last_recv is None else (
38            now - self._last_recv) * 1000.0
39        self._last_recv = now
40
41        t_sec = Time.from_msg(msg.header.stamp).nanoseconds / 1e9
42        status_str = STATUS_STR.get(msg.status.status, 'UNKNOWN')
43        cov = msg.position_covariance
44
45        self.get_logger().info(
46            "\n========== NavSatFix ==========\n"
47            f"stamp={t_sec:.6f}s\n"
48            f"frame={msg.header.frame_id}\n"
49            f"recv_dt={dt_ms:.3f}ms\n"
50            f"latitude:  {msg.latitude:.8f} deg\n"
51            f"longitude: {msg.longitude:.8f} deg\n"
52            f"altitude:  {msg.altitude:.3f} m\n"
53            f"status: {status_str}\n"
54            f"service: {msg.status.service}\n"
55            f"position_covariance: [{cov[0]:.4f}, {cov[1]:.4f}, {cov[2]:.4f}, "
56            f"{cov[3]:.4f}, {cov[4]:.4f}, {cov[5]:.4f}, "
57            f"{cov[6]:.4f}, {cov[7]:.4f}, {cov[8]:.4f}]\n"
58            f"position_covariance_type: {msg.position_covariance_type}"
59        )
60
61
62def main():
63    rclpy.init()
64    node = GnssPrinter()
65    try:
66        rclpy.spin(node)
67    except KeyboardInterrupt:
68        pass
69    finally:
70        node.destroy_node()
71        if rclpy.ok():
72            rclpy.shutdown()
73
74
75if __name__ == '__main__':
76    main()

使用说明:

# 构建
colcon build --packages-select py_examples

# 订阅GNSS数据
ros2 run py_examples echo_gnss_data

输出示例:

[INFO] [gnss_printer]: Subscribing NavSatFix: /aima/hal/sensor/gnss

========== NavSatFix ==========
stamp=1234567890.123456s
frame=gnss
recv_dt=1000.000ms
latitude:  31.84537000 deg
longitude: 117.19882000 deg
altitude:  30.000 m
status: FIX
service: 1
position_covariance: [1.0000, 0.0000, 0.0000, 0.0000, 1.0000, 0.0000, 0.0000, 0.0000, 1.0000]
position_covariance_type: 2

注意事项:

  • 定位状态说明:NO_FIX(无定位)、FIX(单点定位)

  • 室内环境下 status 通常为 NO_FIX,需到室外开阔处等待定位

6.1.35 上肢控制

备注

运行前需先切换到对应模式,详见 上肢控制。

  1#!/usr/bin/env python3
  2"""
  3Upper body control example.
  4
  5Usage:
  6  ros2 run py_examples upper_body_control <mode>
  7
  8  mode:
  9    head    — head center pose,          requires HEAD_ONLY mode
 10    claw    — claw half-open,            requires UPPERBODY_REMOTE_SPLIT mode
 11    joint   — dexterous hand half-open,  requires UPPERBODY_REMOTE_SPLIT mode
 12    gesture — dexterous hand gesture 1,  requires UPPERBODY_REMOTE_SPLIT mode
 13
 14Switch to the desired MC mode before running, e.g. for head:
 15  ros2 run py_examples set_mc_action SD     # → STAND_DEFAULT
 16  ros2 run py_examples set_mc_action HO     # → HEAD_ONLY
 17or for claw / joint / gesture:
 18  ros2 run py_examples set_mc_action URS    # → UPPERBODY_REMOTE_SPLIT
 19"""
 20
 21import sys
 22import rclpy
 23from rclpy.node import Node
 24from aimdk_msgs.msg import UpperBodyCommandArray, MessageHeader
 25
 26# hand_sub_mode, head_pos, arm_pos, hand_pos
 27MODES = {
 28    # head_yaw=0, head_pitch=0 (center)
 29    'head':    (0, [0.0, 0.0],
 30                [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0,
 31                 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0],
 32                []),
 33    # left_open=0.5, right_open=0.5
 34    'claw':    (UpperBodyCommandArray.HAND_CLAW_OPEN_CLOSE,  # claw (1)
 35                [0.0, 0.0],
 36                [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0,
 37                 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0],
 38                [0.5, 0.5]),
 39    # left 10 joints + right 10 joints, in rad, all half-open
 40    # (each joint at half its URDF range limit). Right is the mirror of left.
 41    # Joint order: [thumb_roll, thumb_abad, thumb_mcp, index_abad, index_pip,
 42    #               middle_pip, ring_abad, ring_pip, pinky_abad, pinky_pip]
 43    'joint':   (UpperBodyCommandArray.HAND_DEXTEROUS_JOINT,  # dexterous joint (2)
 44                [0.0, 0.0],
 45                [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0,
 46                 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0],
 47                [-0.6, 0.8, -0.4, 0.1, 0.7, 0.7, -0.1, 0.7, -0.1, 0.7,
 48                 0.6, -0.8, 0.4, -0.1, 0.7, 0.7, 0.1, 0.7, 0.1, 0.7]),
 49    # left_gesture=1, left_open=1.0, right_gesture=1, right_open=1.0
 50    'gesture': (UpperBodyCommandArray.HAND_DEXTEROUS_GESTURE,  # dexterous gesture (3)
 51                [0.0, 0.0],
 52                [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0,
 53                 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0],
 54                [1.0, 1.0, 1.0, 1.0]),
 55}
 56
 57
 58class UpperBodyControlNode(Node):
 59    def __init__(self, mode: str):
 60        super().__init__('upper_body_control')
 61        self.hand_sub_mode, self.head_pos, self.arm_pos, self.hand_pos = MODES[mode]
 62        self.pub = self.create_publisher(
 63            UpperBodyCommandArray, '/mc/upper_body_command', 10)
 64        self.timer = self.create_timer(0.02, self.publish)  # 50 Hz
 65        self._seq = 0
 66        self.get_logger().info(
 67            f'mode={mode}  hand_sub_mode={self.hand_sub_mode}')
 68
 69    def publish(self):
 70        msg = UpperBodyCommandArray()
 71        now = self.get_clock().now()
 72        msg.header = MessageHeader()
 73        msg.header.stamp.sec = now.nanoseconds // 1_000_000_000
 74        msg.header.stamp.nanosec = now.nanoseconds % 1_000_000_000
 75        msg.header.frame_id = 'mc_upper_body'
 76        msg.header.sequence = self._seq
 77        self._seq += 1
 78        msg.source = 'upper_body_example'
 79        msg.hand_sub_mode = self.hand_sub_mode
 80        msg.head_pos = self.head_pos
 81        msg.arm_pos = self.arm_pos
 82        msg.hand_pos = self.hand_pos
 83        self.pub.publish(msg)
 84
 85
 86def main(args=None):
 87    rclpy.init(args=args)
 88
 89    argv = sys.argv[1:]
 90    if not argv or argv[0] not in MODES:
 91        print(f'Usage: upper_body_control <{"│".join(MODES)}>')
 92        rclpy.shutdown()
 93        return
 94
 95    node = UpperBodyControlNode(argv[0])
 96    try:
 97        rclpy.spin(node)
 98    except KeyboardInterrupt:
 99        pass
100    finally:
101        node.destroy_node()
102        if rclpy.ok():
103            rclpy.shutdown()
104
105
106if __name__ == '__main__':
107    main()

使用说明:

colcon build --packages-select py_examples

# 头部控制(HEAD_ONLY 模式)
ros2 run py_examples upper_body_control head

# 夹爪控制(claw,UPPERBODY_REMOTE_SPLIT 模式)
ros2 run py_examples upper_body_control claw

# 灵巧手关节(UPPERBODY_REMOTE_SPLIT 模式)
ros2 run py_examples upper_body_control joint

# 灵巧手手势(UPPERBODY_REMOTE_SPLIT 模式)
ros2 run py_examples upper_body_control gesture

6.1.36 订阅 HDS 诊断信息 / 查询历史事件

订阅 /aima/hds/diag_code_list、/aima/hds/alert_code_list、/aima/hds/post_proc/signal 三个话题实时获取诊断码、告警码与后处理信号;同时支持调用 GetHdsEventHistory 服务查询历史事件。完整的诊断码清单见 诊断码参考表。

  1#!/usr/bin/env python3
  2"""
  3HDS diagnostics tool - two modes:
  4
  5  echo       Subscribe to diag / alert / post_proc topics in real time.
  6  history    Query historical alert / diagnostic events via service.
  7
  8See docs/cn/example/Python.md section "Subscribe HDS diagnostics /
  9query history events" for full usage and field descriptions.
 10"""
 11
 12import argparse
 13import sys
 14import time
 15
 16import rclpy
 17from rclpy.node import Node
 18from rclpy.qos import QoSProfile, ReliabilityPolicy, DurabilityPolicy
 19
 20import aimdk_msgs.msg as aimdk_msg
 21import aimdk_msgs.srv as aimdk_srv
 22
 23
 24DIAG_TYPE_NAMES = {
 25    aimdk_msg.DiagnosticType.NORMAL: "NORMAL",
 26    aimdk_msg.DiagnosticType.TRIGGER_APPEAR: "APPEAR",
 27    aimdk_msg.DiagnosticType.TRIGGER_DISAPPEAR: "DISAPPEAR",
 28}
 29
 30STATE_NAMES = {
 31    aimdk_msg.HdsEventQuery.STATE_ALL: "ALL",
 32    aimdk_msg.HdsEventQuery.STATE_ACTIVE: "ACTIVE",
 33    aimdk_msg.HdsEventQuery.STATE_RECOVERED: "RECOVERED",
 34}
 35
 36
 37# ---------- echo mode ----------
 38
 39class HdsDiagnosticsEcho(Node):
 40    """Subscribe to three HDS topics and print incoming messages."""
 41
 42    def __init__(self):
 43        super().__init__('hds_diagnostics_echo')
 44
 45        # HDS publishers use BEST_EFFORT; match it to receive messages.
 46        qos = QoSProfile(
 47            depth=10,
 48            reliability=ReliabilityPolicy.BEST_EFFORT,
 49            durability=DurabilityPolicy.VOLATILE,
 50        )
 51
 52        self.create_subscription(
 53            aimdk_msg.DiagnosticInfoArray,
 54            '/aima/hds/diag_code_list',
 55            self.on_diagnostics, qos)
 56
 57        self.create_subscription(
 58            aimdk_msg.AlertCodeArray,
 59            '/aima/hds/alert_code_list',
 60            self.on_alerts, qos)
 61
 62        self.create_subscription(
 63            aimdk_msg.PostProcSigArray,
 64            '/aima/hds/post_proc/signal',
 65            self.on_post_proc, qos)
 66
 67        self.get_logger().info(
 68            'Subscribed: diag_code_list / alert_code_list / post_proc/signal')
 69
 70    def on_diagnostics(self, msg: aimdk_msg.DiagnosticInfoArray):
 71        if not msg.diagnostics:
 72            return
 73        lines = [f'[Diagnostics] count={len(msg.diagnostics)}']
 74        for d in msg.diagnostics:
 75            type_name = DIAG_TYPE_NAMES.get(
 76                d.type.value, f'UNKNOWN({d.type.value})')
 77            lines.append(
 78                f'  diag_code=0x{d.diag_code:010X}  type={type_name}  '
 79                f'module_id={d.module_id}  ts={d.timestamp}  info="{d.info}"')
 80        self.get_logger().info('\n'.join(lines))
 81
 82    def on_alerts(self, msg: aimdk_msg.AlertCodeArray):
 83        if not msg.list:
 84            return
 85        lines = [f'[Alerts] count={len(msg.list)}']
 86        for a in msg.list:
 87            lines.append(
 88                f'  code="{a.code}"  ts={a.timestamp}  '
 89                f'first_ts={a.first_timestamp}')
 90        self.get_logger().info('\n'.join(lines))
 91
 92    def on_post_proc(self, msg: aimdk_msg.PostProcSigArray):
 93        if not msg.post_proc_sig:
 94            return
 95        lines = [f'[PostProc] count={len(msg.post_proc_sig)}']
 96        for p in msg.post_proc_sig:
 97            s = p.sig_set
 98            lines.append(
 99                f'  diag_code=0x{p.diag_code:010X}  ts={p.timestamp}  '
100                f'robot={s.robot_sig} motion={s.motion_sig} '
101                f'sound={s.sound_sig} led={s.led_sig} facial={s.facial_sig} '
102                f'funcdisable=0x{s.funcdisable_sig:016X}')
103        self.get_logger().info('\n'.join(lines))
104
105
106def run_echo(_args) -> int:
107    node = HdsDiagnosticsEcho()
108    try:
109        rclpy.spin(node)
110    except KeyboardInterrupt:
111        pass
112    finally:
113        node.destroy_node()
114    return 0
115
116
117# ---------- history mode ----------
118
119class HdsHistoryClient(Node):
120    """Call the GetHdsEventHistory service to query historical events."""
121
122    SERVICE_NAME = '/aimdk_5Fmsgs/srv/GetHdsEventHistory'
123
124    def __init__(self):
125        super().__init__('hds_history_client')
126        self.client = self.create_client(
127            aimdk_srv.GetHdsEventHistory, self.SERVICE_NAME)
128
129    def call(self, query: aimdk_msg.HdsEventQuery, timeout_sec: float = 10.0):
130        if not self.client.wait_for_service(timeout_sec=5.0):
131            self.get_logger().error(
132                f'Service {self.SERVICE_NAME} not available')
133            return None
134
135        request = aimdk_srv.GetHdsEventHistory.Request()
136        now = self.get_clock().now()
137        request.header.stamp.sec = now.nanoseconds // 1_000_000_000
138        request.header.stamp.nanosec = now.nanoseconds % 1_000_000_000
139        request.query = query
140
141        future = self.client.call_async(request)
142        start = time.time()
143        while rclpy.ok() and time.time() - start < timeout_sec:
144            rclpy.spin_once(self, timeout_sec=0.1)
145            if future.done():
146                return future.result()
147
148        self.get_logger().error('Service call timeout')
149        future.cancel()
150        return None
151
152
153def build_query(args) -> aimdk_msg.HdsEventQuery:
154    q = aimdk_msg.HdsEventQuery()
155    q.start_time = args.start_ns
156    q.end_time = args.end_ns
157    q.diag_codes = args.diag_codes
158    q.alert_codes = args.alert_codes
159    q.alert_locations = []
160    q.state = args.state
161    q.order_desc = args.desc
162    q.offset = args.offset
163    q.limit = args.limit
164    return q
165
166
167def print_records(response) -> None:
168    print(f'total_count = {response.total_count}, '
169          f'returned = {len(response.records)}, '
170          f'has_more = {response.has_more}')
171    if not response.records:
172        return
173    print('-' * 80)
174    for r in response.records:
175        recovered = 'YES' if r.recovered else 'NO'
176        duration_s = r.duration_ns / 1e9
177        print(f'id={r.id}  diag=0x{r.diag_code:010X}  '
178              f'alert="{r.alert_code}"')
179        print(f'  first_ts={r.first_timestamp}  '
180              f'last_ts={r.last_timestamp}')
181        print(f'  recovered={recovered}  '
182              f'recover_ts={r.recover_timestamp}  '
183              f'duration={duration_s:.3f}s')
184        if r.alert_location:
185            print(f'  location="{r.alert_location}"')
186        print('-' * 80)
187
188
189def run_history(args) -> int:
190    node = HdsHistoryClient()
191    try:
192        query = build_query(args)
193        node.get_logger().info(
194            f'Querying history: state={STATE_NAMES.get(query.state)}, '
195            f'offset={query.offset}, limit={query.limit}, '
196            f'desc={query.order_desc}')
197
198        response = node.call(query)
199        if response is None:
200            return 1
201        print_records(response)
202        return 0
203    finally:
204        node.destroy_node()
205
206
207# ---------- entry point ----------
208
209def parse_args():
210    parser = argparse.ArgumentParser(
211        description='HDS diagnostics tool - echo topics or query history')
212    sub = parser.add_subparsers(dest='mode', required=True)
213
214    sub.add_parser(
215        'echo', help='subscribe diag/alert/post_proc topics and print live')
216
217    h = sub.add_parser('history', help='query GetHdsEventHistory service')
218    h.add_argument('--start-ns', type=int, default=0,
219                   help='start timestamp (ns), 0 means unlimited')
220    h.add_argument('--end-ns', type=int, default=0,
221                   help='end timestamp (ns), 0 means unlimited')
222    h.add_argument('--diag-codes', type=lambda s: int(s, 0), nargs='*',
223                   default=[], help='filter diag codes (e.g. 0x6200000001)')
224    h.add_argument('--alert-codes', nargs='*', default=[],
225                   help='filter alert code strings')
226    h.add_argument('--state', type=int, default=aimdk_msg.HdsEventQuery.STATE_ALL,
227                   choices=[0, 1, 2],
228                   help='0=ALL, 1=ACTIVE, 2=RECOVERED')
229    h.add_argument('--offset', type=int, default=0)
230    h.add_argument('--limit', type=int, default=20)
231    h.add_argument('--desc', action='store_true',
232                   help='order by time descending')
233
234    return parser.parse_args()
235
236
237def main() -> int:
238    args = parse_args()
239    rclpy.init()
240    try:
241        if args.mode == 'echo':
242            return run_echo(args)
243        if args.mode == 'history':
244            return run_history(args)
245        return 1
246    finally:
247        if rclpy.ok():
248            rclpy.shutdown()
249
250
251if __name__ == '__main__':
252    sys.exit(main())

使用说明

实时订阅 (echo):

# 直接订阅三个话题,实时打印诊断码、告警码、后处理信号
ros2 run py_examples hds_diagnostics echo

话题

数据类型

/aima/hds/diag_code_list

aimdk_msgs/msg/DiagnosticInfoArray

/aima/hds/alert_code_list

aimdk_msgs/msg/AlertCodeArray

/aima/hds/post_proc/signal

aimdk_msgs/msg/PostProcSigArray

查询历史事件 (history),所有过滤条件均对应 HdsEventQuery 字段,使用示例:

# 查最近 20 条 (默认 limit=20,正序)
ros2 run py_examples hds_diagnostics history

# 按时间范围查询 (对应 start_time / end_time)
ros2 run py_examples hds_diagnostics history \
  --start-ns 1716000000000000000 \
  --end-ns   1716100000000000000

# 只看活跃中的告警 (对应 state = STATE_ACTIVE)
ros2 run py_examples hds_diagnostics history --state 1

# 按诊断码过滤 (对应 diag_codes,支持十六进制)
ros2 run py_examples hds_diagnostics history --diag-codes 0x6200000001 0x6200000002

# 按告警码过滤 (对应 alert_codes)
ros2 run py_examples hds_diagnostics history --alert-codes MOTOR_OVERHEAT COMM_LOST

# 分页 + 倒序 (对应 limit / offset / order_desc,最新的先显示)
ros2 run py_examples hds_diagnostics history --limit 50 --offset 0 --desc

# 组合使用:只看已恢复的,每次取 10 条,最新的先显示
ros2 run py_examples hds_diagnostics history --state 2 --limit 10 --desc

返回字段对应 HdsEventRecord。

echo 模式输出示例

[INFO] [hds_diagnostics_echo]: Subscribed: diag_code_list / alert_code_list / post_proc/signal
[INFO] [hds_diagnostics_echo]: [Diagnostics] count=1
  diag_code=0x6200000007  type=APPEAR  module_id=98  ts=1770125762202825297  info="获取不到机器人定位"
[INFO] [hds_diagnostics_echo]: [Alerts] count=1
  code="LOCALIZATION_LOST"  ts=1770125762202825297  first_ts=1770125762202825297
[INFO] [hds_diagnostics_echo]: [PostProc] count=1
  diag_code=0x6200000007  ts=1770125762202825297  robot=1 motion=2 sound=1 led=3 facial=0 funcdisable=0x0000000000000004

history 模式输出示例

[INFO] [hds_history_client]: Querying history: state=ALL, offset=0, limit=20, desc=True
total_count = 5, returned = 5, has_more = False
--------------------------------------------------------------------------------
id=42  diag=0x6200000007  alert="LOCALIZATION_LOST"
  first_ts=1770125762202825297  last_ts=1770125765310145000
  recovered=YES  recover_ts=1770125770000000000  duration=7.797s
--------------------------------------------------------------------------------

接口参考

  • 话题:/aima/hds/diag_code_list、/aima/hds/alert_code_list、/aima/hds/post_proc/signal

  • 服务:/aimdk_5Fmsgs/srv/GetHdsEventHistory

  • 消息:aimdk_msgs/msg/DiagnosticInfoArray、aimdk_msgs/msg/AlertCodeArray、aimdk_msgs/msg/PostProcSigArray