# qnbot_replay

ROS2 Action Server，用于回放 `.msgpack` 录制文件到 WebSocket。

## 功能特点

- 通过 ROS2 Action 接口接收回放请求
- 只支持时间轴复现模式（按录制时的时间间隔发送）
- 支持速度调节（加速/减速）
- 提供实时进度反馈
- 支持取消正在进行的回放任务

## 依赖安装

```bash
pip install msgpack websocket-client
```

## 文件放置

将 `.msgpack` 录制文件放置在 `recordings/` 目录下，或通过参数指定自定义目录。

## 使用方法

### 1. 启动 Action Server

```bash
ros2 run qnbot_replay replay_action_server
```

或使用自定义录制文件目录：

```bash
ros2 run qnbot_replay replay_action_server --ros-args -p recordings_dir:=/path/to/recordings
```

### 2. 发送回放请求

最简用法（只传文件名，使用默认速度和默认 WebSocket 地址）：

```bash
ros2 action send_goal /replay_msgpack qnbot_replay/action/ReplayMsgpack \
  "{file_name: 'data.msgpack'}"
```

指定速度（2 倍速）：

```bash
ros2 action send_goal /replay_msgpack qnbot_replay/action/ReplayMsgpack \
  "{file_name: 'data.msgpack', speed: 2.0}"
```

完整参数 + 进度反馈：

```bash
ros2 action send_goal /replay_msgpack qnbot_replay/action/ReplayMsgpack \
  "{file_name: 'data.msgpack', speed: 1.0, ws_url: 'ws://127.0.0.1:19091'}" --feedback
```

### 3. 取消回放

```bash
# 先获取 goal ID
ros2 action list

# 取消指定的 goal
ros2 action cancel /replay_msgpack
```

## Action 接口定义

### Goal（请求参数）
- `file_name`: .msgpack 文件名（不含路径）【必填】
- `speed`: 时间缩放系数（>1 加速，<1 减速，**默认 1.0**）
- `ws_url`: WebSocket 服务器地址（**默认 ws://127.0.0.1:19091**）

### Result（结果）
- `success`: 是否成功完成
- `message`: 结果描述信息
- `total_packets`: 总数据包数
- `success_count`: 成功发送的数据包数
- `failed_count`: 发送失败的数据包数
- `duration_seconds`: 实际回放耗时（秒）

### Feedback（进度反馈）
- `current_packet`: 当前已发送的数据包索引
- `total_packets`: 总数据包数
- `progress_percent`: 进度百分比
- `success_count`: 已成功发送的数据包数
- `failed_count`: 已失败的数据包数

## 示例代码（Python 客户端）

```python
import rclpy
from rclpy.action import ActionClient
from rclpy.node import Node
from qnbot_replay.action import ReplayMsgpack

class ReplayClient(Node):
    def __init__(self):
        super().__init__('replay_client')
        self._action_client = ActionClient(self, ReplayMsgpack, 'replay_msgpack')
    
    def send_goal(self, file_name: str, speed: float = 1.0, ws_url: str = 'ws://127.0.0.1:19091'):
        goal_msg = ReplayMsgpack.Goal()
        goal_msg.file_name = file_name
        goal_msg.speed = speed
        goal_msg.ws_url = ws_url
        
        self._action_client.wait_for_server()
        
        self._send_goal_future = self._action_client.send_goal_async(
            goal_msg,
            feedback_callback=self.feedback_callback
        )
        self._send_goal_future.add_done_callback(self.goal_response_callback)
    
    def feedback_callback(self, feedback_msg):
        feedback = feedback_msg.feedback
        self.get_logger().info(
            f'进度: {feedback.progress_percent:.1f}% '
            f'({feedback.current_packet}/{feedback.total_packets})'
        )
    
    def goal_response_callback(self, future):
        goal_handle = future.result()
        if not goal_handle.accepted:
            self.get_logger().info('Goal 被拒绝')
            return
        
        self.get_logger().info('Goal 已接受')
        self._get_result_future = goal_handle.get_result_async()
        self._get_result_future.add_done_callback(self.get_result_callback)
    
    def get_result_callback(self, future):
        result = future.result().result
        self.get_logger().info(f'结果: success={result.success}, message={result.message}')
        rclpy.shutdown()

def main():
    rclpy.init()
    client = ReplayClient()
    client.send_goal('recording.msgpack', speed=1.0)
    rclpy.spin(client)

if __name__ == '__main__':
    main()
```
