feat: C控制程序对接KCP
This commit is contained in:
54
AGENT.md
Normal file
54
AGENT.md
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
# 通信场景说明
|
||||||
|
|
||||||
|
## 系统拓扑
|
||||||
|
- 系统中有 `A/B/C/D` 四个终端。
|
||||||
|
- `A` 与 `C` 在同一侧,`B` 与 `D` 在同一侧。
|
||||||
|
- 实际运行时不是一条端到端 KCP,而是两条独立 KCP 链路:
|
||||||
|
- `B <-> D`
|
||||||
|
- `D <-> A`
|
||||||
|
- `C` 只负责 UDP 端口转发。
|
||||||
|
- 整体目标是让 `A` 与 `B` 双向通信:
|
||||||
|
- `A -> B` 发送控制命令
|
||||||
|
- `B -> A` 发送视频和其他反馈数据
|
||||||
|
|
||||||
|
## 网络情况
|
||||||
|
- 主要瓶颈在 `B` 侧 5G 链路,`A` 侧是有线网络,不是主要约束。
|
||||||
|
- `B <-> D` 的时延抖动较大:
|
||||||
|
- 常见约 `15-60 ms`
|
||||||
|
- 最差约 `80-90 ms`
|
||||||
|
- 带宽是明显非对称的:
|
||||||
|
- `B -> D` 约 `8 Mbps`,偏低且不稳定
|
||||||
|
- `D -> B` 约 `27-50 Mbps`,更高但也有波动
|
||||||
|
- 工程含义:
|
||||||
|
- 控制流必须优先保障
|
||||||
|
- 视频流必须限速并具备自适应能力
|
||||||
|
- 不能默认把所有流合成一个共享传输就一定更优
|
||||||
|
|
||||||
|
## 三个项目的角色
|
||||||
|
- `OmniSocketGo`
|
||||||
|
- 底层传输项目
|
||||||
|
- 使用 C 编写
|
||||||
|
- 提供 OmniSocket/KCP 传输能力,以及视频发送等原生传输程序
|
||||||
|
- `robot-command-center`
|
||||||
|
- A 侧上层应用
|
||||||
|
- 主要负责接收视频帧,并提供监控/展示能力
|
||||||
|
- 当前通过本机 `A-side OmniDaemon` 使用传输能力
|
||||||
|
- `tienkung-szu`
|
||||||
|
- 上层控制项目
|
||||||
|
- 同时包含 A 侧控制发送和 B 侧控制执行逻辑
|
||||||
|
- A 侧典型入口是键盘/Xbox sender
|
||||||
|
- B 侧典型入口是 `rl_control_node.py` / `rl_control_node_sim.py`,负责接收控制并驱动机器人行为
|
||||||
|
|
||||||
|
## 当前架构方向
|
||||||
|
- `A` 侧通过 `A-side OmniDaemon` 统一管理本机的控制发送和视频接收。
|
||||||
|
- `B` 侧通过 `B-side OmniDaemon` 统一管理本机的控制接收和视频发送。
|
||||||
|
- 核心原则:
|
||||||
|
- 控制流与视频流逻辑分离
|
||||||
|
- 每台主机通过本地 daemon 统一持有传输会话
|
||||||
|
- 后续围绕真实网络瓶颈做 telemetry、调度和速率自适应
|
||||||
|
|
||||||
|
## 代码原则
|
||||||
|
- 一定要精简,不能写过于复杂和冗余的代码,因为必须确保各个环节可控,在低延迟的要求下,需要细化各个环节的消耗
|
||||||
|
|
||||||
|
## SSH原则
|
||||||
|
- 如果你需要SSH到某个远程端执行操作,一定要确保不做删除文件操作,确保留有记录
|
||||||
54
CLAUDE.md
Normal file
54
CLAUDE.md
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
# 通信场景说明
|
||||||
|
|
||||||
|
## 系统拓扑
|
||||||
|
- 系统中有 `A/B/C/D` 四个终端。
|
||||||
|
- `A` 与 `C` 在同一侧,`B` 与 `D` 在同一侧。
|
||||||
|
- 实际运行时不是一条端到端 KCP,而是两条独立 KCP 链路:
|
||||||
|
- `B <-> D`
|
||||||
|
- `D <-> A`
|
||||||
|
- `C` 只负责 UDP 端口转发。
|
||||||
|
- 整体目标是让 `A` 与 `B` 双向通信:
|
||||||
|
- `A -> B` 发送控制命令
|
||||||
|
- `B -> A` 发送视频和其他反馈数据
|
||||||
|
|
||||||
|
## 网络情况
|
||||||
|
- 主要瓶颈在 `B` 侧 5G 链路,`A` 侧是有线网络,不是主要约束。
|
||||||
|
- `B <-> D` 的时延抖动较大:
|
||||||
|
- 常见约 `15-60 ms`
|
||||||
|
- 最差约 `80-90 ms`
|
||||||
|
- 带宽是明显非对称的:
|
||||||
|
- `B -> D` 约 `8 Mbps`,偏低且不稳定
|
||||||
|
- `D -> B` 约 `27-50 Mbps`,更高但也有波动
|
||||||
|
- 工程含义:
|
||||||
|
- 控制流必须优先保障
|
||||||
|
- 视频流必须限速并具备自适应能力
|
||||||
|
- 不能默认把所有流合成一个共享传输就一定更优
|
||||||
|
|
||||||
|
## 三个项目的角色
|
||||||
|
- `OmniSocketGo`
|
||||||
|
- 底层传输项目
|
||||||
|
- 使用 C 编写
|
||||||
|
- 提供 OmniSocket/KCP 传输能力,以及视频发送等原生传输程序
|
||||||
|
- `robot-command-center`
|
||||||
|
- A 侧上层应用
|
||||||
|
- 主要负责接收视频帧,并提供监控/展示能力
|
||||||
|
- 当前通过本机 `A-side OmniDaemon` 使用传输能力
|
||||||
|
- `tienkung-szu`
|
||||||
|
- 上层控制项目
|
||||||
|
- 同时包含 A 侧控制发送和 B 侧控制执行逻辑
|
||||||
|
- A 侧典型入口是键盘/Xbox sender
|
||||||
|
- B 侧典型入口是 `rl_control_node.py` / `rl_control_node_sim.py`,负责接收控制并驱动机器人行为
|
||||||
|
|
||||||
|
## 当前架构方向
|
||||||
|
- `A` 侧通过 `A-side OmniDaemon` 统一管理本机的控制发送和视频接收。
|
||||||
|
- `B` 侧通过 `B-side OmniDaemon` 统一管理本机的控制接收和视频发送。
|
||||||
|
- 核心原则:
|
||||||
|
- 控制流与视频流逻辑分离
|
||||||
|
- 每台主机通过本地 daemon 统一持有传输会话
|
||||||
|
- 后续围绕真实网络瓶颈做 telemetry、调度和速率自适应
|
||||||
|
|
||||||
|
## 代码原则
|
||||||
|
- 一定要精简,不能写过于复杂和冗余的代码,因为必须确保各个环节可控,在低延迟的要求下,需要细化各个环节的消耗
|
||||||
|
|
||||||
|
## SSH原则
|
||||||
|
- 如果你需要SSH到某个远程端执行操作,一定要确保不做删除文件操作,确保留有记录
|
||||||
34
ros-control-c/Makefile
Normal file
34
ros-control-c/Makefile
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
CC = gcc
|
||||||
|
CFLAGS = -std=c11 -Wall -Wextra -O2 -pthread -D_GNU_SOURCE -I../include -I../third_party/cjson -I../third_party/kcp -I./common
|
||||||
|
LDFLAGS = -pthread -lm
|
||||||
|
|
||||||
|
OMNI_SRCS = \
|
||||||
|
../src/omni_common.c \
|
||||||
|
../src/protocol.c \
|
||||||
|
../src/latencylog.c \
|
||||||
|
../src/kcp_packet_debug.c \
|
||||||
|
../src/kcp_session_stats.c \
|
||||||
|
../src/linux_timestamping.c \
|
||||||
|
../src/transport_kcp.c \
|
||||||
|
../src/peer_kcp_client.c \
|
||||||
|
../third_party/cjson/cJSON.c \
|
||||||
|
../third_party/kcp/ikcp.c
|
||||||
|
|
||||||
|
BUILDDIR = build
|
||||||
|
|
||||||
|
TARGETS = $(BUILDDIR)/keyboard_controller $(BUILDDIR)/gamepad_controller
|
||||||
|
|
||||||
|
.PHONY: all clean
|
||||||
|
|
||||||
|
all: $(TARGETS)
|
||||||
|
|
||||||
|
$(BUILDDIR)/keyboard_controller: remote/keyboard_controller.c common/protocol.h common/teleop_transport.h common/teleop_transport.c $(OMNI_SRCS)
|
||||||
|
@mkdir -p $(BUILDDIR)
|
||||||
|
$(CC) $(CFLAGS) -o $@ remote/keyboard_controller.c common/teleop_transport.c $(OMNI_SRCS) $(LDFLAGS)
|
||||||
|
|
||||||
|
$(BUILDDIR)/gamepad_controller: remote/gamepad_controller.c common/protocol.h common/teleop_transport.h common/teleop_transport.c $(OMNI_SRCS)
|
||||||
|
@mkdir -p $(BUILDDIR)
|
||||||
|
$(CC) $(CFLAGS) -o $@ remote/gamepad_controller.c common/teleop_transport.c $(OMNI_SRCS) $(LDFLAGS)
|
||||||
|
|
||||||
|
clean:
|
||||||
|
rm -rf $(BUILDDIR)
|
||||||
71
ros-control-c/README.md
Normal file
71
ros-control-c/README.md
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
# ros-control-c
|
||||||
|
|
||||||
|
`ros-control-c` keeps the original 24-byte `twist_cmd_t` control payload and now supports two runtime transports:
|
||||||
|
|
||||||
|
- `udp` (default): unchanged from the original implementation
|
||||||
|
- `kcp`: sent through OmniSocket using `MSG_TYPE_BINARY`
|
||||||
|
|
||||||
|
## Build
|
||||||
|
|
||||||
|
On Linux:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
make -C ros-control-c
|
||||||
|
```
|
||||||
|
|
||||||
|
If the robot-side Python bridge will use KCP, build and install the OmniSocket Python extension from the repo root first:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
make python-ext
|
||||||
|
make python-install
|
||||||
|
```
|
||||||
|
|
||||||
|
## UDP Mode
|
||||||
|
|
||||||
|
Sender:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./ros-control-c/build/keyboard_controller -i 192.168.1.100 -p 9870
|
||||||
|
./ros-control-c/build/gamepad_controller -i 192.168.1.100 -p 9870
|
||||||
|
```
|
||||||
|
|
||||||
|
Robot bridge:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python3 ros-control-c/robot/udp_ros_bridge.py
|
||||||
|
```
|
||||||
|
|
||||||
|
## KCP Mode
|
||||||
|
|
||||||
|
Start the existing OmniSocket KCP hub from the repo root:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./bin/kcpserver -listen :9002
|
||||||
|
```
|
||||||
|
|
||||||
|
Sender:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./ros-control-c/build/keyboard_controller -t kcp -s 192.168.1.50:9002 -I ros-keyboard-ctrl -T ros-bridge-ctrl
|
||||||
|
./ros-control-c/build/gamepad_controller -t kcp -s 192.168.1.50:9002 -I ros-gamepad-ctrl -T ros-bridge-ctrl
|
||||||
|
```
|
||||||
|
|
||||||
|
If a relay is needed, add `-r <relay_addr>` to the controller command.
|
||||||
|
|
||||||
|
Robot bridge:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python3 ros-control-c/robot/udp_ros_bridge.py --ros-args \
|
||||||
|
-p transport:=kcp \
|
||||||
|
-p kcp_server:=192.168.1.50:9002 \
|
||||||
|
-p peer_id:=ros-bridge-ctrl
|
||||||
|
```
|
||||||
|
|
||||||
|
Optional sender filtering:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python3 ros-control-c/robot/udp_ros_bridge.py --ros-args \
|
||||||
|
-p transport:=kcp \
|
||||||
|
-p peer_id:=ros-bridge-ctrl \
|
||||||
|
-p expected_sender:=ros-keyboard-ctrl
|
||||||
|
```
|
||||||
@@ -0,0 +1,221 @@
|
|||||||
|
Robot Remote Control via UDP — Implementation Plan
|
||||||
|
|
||||||
|
Context
|
||||||
|
|
||||||
|
The robot subscribes to /hric/robot/cmd_vel with geometry_msgs/msg/TwistStamped (frame_id: pelvis). Standard ROS2 teleop tools (teleop_twist_keyboard, teleop_twist_joy) publish
|
||||||
|
plain Twist, not TwistStamped, so they won't work directly. We build custom keyboard and gamepad controllers in C (zero external dependencies, Linux-only) communicating over UDP
|
||||||
|
to a robot-side ROS2 bridge.
|
||||||
|
|
||||||
|
How to Make the Robot Move
|
||||||
|
|
||||||
|
Publish TwistStamped to /hric/robot/cmd_vel continuously (~20 Hz):
|
||||||
|
|
||||||
|
┌─────────────────────┬─────────────────┐
|
||||||
|
│ Field │ Effect │
|
||||||
|
├─────────────────────┼─────────────────┤
|
||||||
|
│ twist.linear.x > 0 │ Walk forward │
|
||||||
|
├─────────────────────┼─────────────────┤
|
||||||
|
│ twist.linear.x < 0 │ Walk backward │
|
||||||
|
├─────────────────────┼─────────────────┤
|
||||||
|
│ twist.linear.y > 0 │ Strafe left │
|
||||||
|
├─────────────────────┼─────────────────┤
|
||||||
|
│ twist.linear.y < 0 │ Strafe right │
|
||||||
|
├─────────────────────┼─────────────────┤
|
||||||
|
│ twist.angular.z > 0 │ Turn left (CCW) │
|
||||||
|
├─────────────────────┼─────────────────┤
|
||||||
|
│ twist.angular.z < 0 │ Turn right (CW) │
|
||||||
|
├─────────────────────┼─────────────────┤
|
||||||
|
│ All zeros │ Stop │
|
||||||
|
└─────────────────────┴─────────────────┘
|
||||||
|
|
||||||
|
Header must have frame_id = "pelvis" and current ROS timestamp.
|
||||||
|
|
||||||
|
---
|
||||||
|
Architecture
|
||||||
|
|
||||||
|
[PC: Keyboard/Gamepad (C)] --UDP binary struct--> [Robot: Bridge (Python/rclpy)] --> /hric/robot/cmd_vel
|
||||||
|
|
||||||
|
---
|
||||||
|
Project Structure
|
||||||
|
|
||||||
|
ros-control/
|
||||||
|
├── topic_example.yaml # (existing)
|
||||||
|
├── Makefile # Build both C programs
|
||||||
|
├── common/
|
||||||
|
│ └── protocol.h # Shared UDP protocol (binary struct)
|
||||||
|
├── remote/
|
||||||
|
│ ├── keyboard_controller.c # Keyboard teleop (C, termios)
|
||||||
|
│ └── gamepad_controller.c # Gamepad teleop (C, Linux joystick API)
|
||||||
|
└── robot/
|
||||||
|
└── udp_ros_bridge.py # UDP → ROS2 TwistStamped (Python/rclpy)
|
||||||
|
|
||||||
|
---
|
||||||
|
UDP Protocol (common/protocol.h)
|
||||||
|
|
||||||
|
Binary packed struct — 24 bytes, no parsing overhead:
|
||||||
|
|
||||||
|
#pragma pack(push, 1)
|
||||||
|
typedef struct {
|
||||||
|
float lx, ly, lz; // linear velocity (m/s)
|
||||||
|
float ax, ay, az; // angular velocity (rad/s)
|
||||||
|
} twist_cmd_t;
|
||||||
|
#pragma pack(pop)
|
||||||
|
|
||||||
|
#define DEFAULT_PORT 9870
|
||||||
|
#define DEFAULT_IP "127.0.0.1"
|
||||||
|
|
||||||
|
On Python side, decode with struct.unpack('<6f', data).
|
||||||
|
|
||||||
|
---
|
||||||
|
Program 1: Keyboard Controller (remote/keyboard_controller.c)
|
||||||
|
|
||||||
|
Dependencies: None (POSIX + termios only)
|
||||||
|
|
||||||
|
Technical approach:
|
||||||
|
- termios.h: Set terminal to raw mode (~ICANON, ~ECHO, VMIN=0, VTIME=1)
|
||||||
|
- select() with 50ms timeout for non-blocking key detection
|
||||||
|
- Arrow keys: detect ESC sequence (\x1B[A/B/C/D)
|
||||||
|
- UDP send via standard socket() / sendto()
|
||||||
|
|
||||||
|
Key mapping:
|
||||||
|
|
||||||
|
┌────────┬───────────────────────────────────┐
|
||||||
|
│ Key │ Action │
|
||||||
|
├────────┼───────────────────────────────────┤
|
||||||
|
│ W / ↑ │ Forward (+linear.x) │
|
||||||
|
├────────┼───────────────────────────────────┤
|
||||||
|
│ S / ↓ │ Backward (-linear.x) │
|
||||||
|
├────────┼───────────────────────────────────┤
|
||||||
|
│ A / ← │ Turn left (+angular.z) │
|
||||||
|
├────────┼───────────────────────────────────┤
|
||||||
|
│ D / → │ Turn right (-angular.z) │
|
||||||
|
├────────┼───────────────────────────────────┤
|
||||||
|
│ Q │ Strafe left (+linear.y) │
|
||||||
|
├────────┼───────────────────────────────────┤
|
||||||
|
│ E │ Strafe right (-linear.y) │
|
||||||
|
├────────┼───────────────────────────────────┤
|
||||||
|
│ Space │ Emergency stop (all zeros) │
|
||||||
|
├────────┼───────────────────────────────────┤
|
||||||
|
│ [ / ] │ Decrease / increase linear speed │
|
||||||
|
├────────┼───────────────────────────────────┤
|
||||||
|
│ - / = │ Decrease / increase angular speed │
|
||||||
|
├────────┼───────────────────────────────────┤
|
||||||
|
│ Ctrl+C │ Quit (restore terminal) │
|
||||||
|
└────────┴───────────────────────────────────┘
|
||||||
|
|
||||||
|
Behavior:
|
||||||
|
- 20 Hz send loop in main thread
|
||||||
|
- On key press: set velocity to ±max_speed
|
||||||
|
- On no key (select timeout): gradually decay velocity to zero OR send zero immediately (configurable)
|
||||||
|
- Print current velocity and speed settings to terminal (refresh in-place with \r)
|
||||||
|
- signal(SIGINT) handler to restore terminal settings before exit
|
||||||
|
- CLI args: -i <ip>, -p <port>, -l <max_linear>, -a <max_angular>
|
||||||
|
|
||||||
|
---
|
||||||
|
Program 2: Gamepad Controller (remote/gamepad_controller.c)
|
||||||
|
|
||||||
|
Dependencies: None (Linux joystick API only: linux/joystick.h)
|
||||||
|
|
||||||
|
Technical approach:
|
||||||
|
- Open /dev/input/js0 (configurable) with O_RDONLY | O_NONBLOCK
|
||||||
|
- Read struct js_event (8 bytes: __u32 time, __s16 value, __u8 type, __u8 number)
|
||||||
|
- Event types: JS_EVENT_AXIS (0x02), JS_EVENT_BUTTON (0x01)
|
||||||
|
- select() for multiplexing joystick read + periodic UDP send
|
||||||
|
|
||||||
|
Xbox controller axis mapping (xpad driver):
|
||||||
|
|
||||||
|
┌────────┬───────────────┬───────────────────────────────────┐
|
||||||
|
│ Axis # │ Physical │ Mapping │
|
||||||
|
├────────┼───────────────┼───────────────────────────────────┤
|
||||||
|
│ 0 │ Left stick X │ linear.y (strafe) │
|
||||||
|
├────────┼───────────────┼───────────────────────────────────┤
|
||||||
|
│ 1 │ Left stick Y │ linear.x (forward/back, inverted) │
|
||||||
|
├────────┼───────────────┼───────────────────────────────────┤
|
||||||
|
│ 3 │ Right stick X │ angular.z (turn) │
|
||||||
|
└────────┴───────────────┴───────────────────────────────────┘
|
||||||
|
|
||||||
|
Button mapping:
|
||||||
|
|
||||||
|
┌──────────┬──────────┬────────────────┐
|
||||||
|
│ Button # │ Physical │ Action │
|
||||||
|
├──────────┼──────────┼────────────────┤
|
||||||
|
│ 0 │ A │ Emergency stop │
|
||||||
|
├──────────┼──────────┼────────────────┤
|
||||||
|
│ 1 │ B │ Quit │
|
||||||
|
└──────────┴──────────┴────────────────┘
|
||||||
|
|
||||||
|
Behavior:
|
||||||
|
- Axis values: raw range [-32767, 32767] → normalized to [-1.0, 1.0] → scaled by max_speed
|
||||||
|
- Deadzone: |normalized| < 0.1 → treat as 0 (configurable)
|
||||||
|
- 20 Hz UDP send loop
|
||||||
|
- Print gamepad name (via JSIOCGNAME ioctl), axes, and current velocities
|
||||||
|
- Auto-detect controller disconnect / reconnect
|
||||||
|
- CLI args: -i <ip>, -p <port>, -d <device>, -l <max_linear>, -a <max_angular>, -z <deadzone>
|
||||||
|
|
||||||
|
---
|
||||||
|
Program 3: UDP-to-ROS2 Bridge (robot/udp_ros_bridge.py)
|
||||||
|
|
||||||
|
Dependencies: rclpy, geometry_msgs (standard ROS2)
|
||||||
|
|
||||||
|
Behavior:
|
||||||
|
- ROS2 node: udp_teleop_bridge
|
||||||
|
- Bind UDP on 0.0.0.0:9870
|
||||||
|
- Receive 24-byte struct → struct.unpack('<6f', data) → build TwistStamped
|
||||||
|
- Set header.stamp = current ROS time, header.frame_id = 'pelvis'
|
||||||
|
- Publish to /hric/robot/cmd_vel at received rate
|
||||||
|
- Watchdog: if no packet for 0.5s, publish zero velocity (safety stop)
|
||||||
|
- UDP recv in separate threading.Thread, ROS2 spin() in main thread
|
||||||
|
- ROS2 parameters: udp_port (int), topic (string), frame_id (string), timeout (float)
|
||||||
|
|
||||||
|
---
|
||||||
|
Build System (Makefile)
|
||||||
|
|
||||||
|
CC = gcc
|
||||||
|
CFLAGS = -Wall -Wextra -O2 -I./common
|
||||||
|
LDFLAGS = -lm
|
||||||
|
|
||||||
|
all: build/keyboard_controller build/gamepad_controller
|
||||||
|
|
||||||
|
build/keyboard_controller: remote/keyboard_controller.c common/protocol.h
|
||||||
|
@mkdir -p build
|
||||||
|
$(CC) $(CFLAGS) -o $@ $< $(LDFLAGS)
|
||||||
|
|
||||||
|
build/gamepad_controller: remote/gamepad_controller.c common/protocol.h
|
||||||
|
@mkdir -p build
|
||||||
|
$(CC) $(CFLAGS) -o $@ $< $(LDFLAGS)
|
||||||
|
|
||||||
|
clean:
|
||||||
|
rm -rf build
|
||||||
|
|
||||||
|
---
|
||||||
|
Files to Create (5 total)
|
||||||
|
|
||||||
|
┌─────┬──────────────────────────────┬────────┬───────────────────────────────────┐
|
||||||
|
│ # │ File │ Lang │ Purpose │
|
||||||
|
├─────┼──────────────────────────────┼────────┼───────────────────────────────────┤
|
||||||
|
│ 1 │ common/protocol.h │ C │ UDP protocol: struct + constants │
|
||||||
|
├─────┼──────────────────────────────┼────────┼───────────────────────────────────┤
|
||||||
|
│ 2 │ remote/keyboard_controller.c │ C │ Keyboard → UDP (termios, select) │
|
||||||
|
├─────┼──────────────────────────────┼────────┼───────────────────────────────────┤
|
||||||
|
│ 3 │ remote/gamepad_controller.c │ C │ Gamepad → UDP (linux/joystick.h) │
|
||||||
|
├─────┼──────────────────────────────┼────────┼───────────────────────────────────┤
|
||||||
|
│ 4 │ robot/udp_ros_bridge.py │ Python │ UDP → ROS2 TwistStamped publisher │
|
||||||
|
├─────┼──────────────────────────────┼────────┼───────────────────────────────────┤
|
||||||
|
│ 5 │ Makefile │ Make │ Build system │
|
||||||
|
└─────┴──────────────────────────────┴────────┴───────────────────────────────────┘
|
||||||
|
|
||||||
|
---
|
||||||
|
Verification
|
||||||
|
|
||||||
|
1. Build: make — should compile without warnings
|
||||||
|
2. Keyboard test: Run build/keyboard_controller -i 127.0.0.1, use a simple Python UDP listener to verify packets:
|
||||||
|
import socket, struct
|
||||||
|
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
||||||
|
s.bind(('0.0.0.0', 9870))
|
||||||
|
while True:
|
||||||
|
data, _ = s.recvfrom(24)
|
||||||
|
print(struct.unpack('<6f', data))
|
||||||
|
3. Gamepad test: Connect Xbox controller, run build/gamepad_controller, verify stick input produces correct UDP packets
|
||||||
|
4. Bridge test: Run udp_ros_bridge.py, then ros2 topic echo /hric/robot/cmd_vel to verify TwistStamped messages
|
||||||
|
5. Safety: Stop controller, confirm bridge sends zero velocity after 0.5s timeout
|
||||||
|
6. End-to-end: Controller → Bridge → robot moves
|
||||||
BIN
ros-control-c/build/gamepad_controller
Normal file
BIN
ros-control-c/build/gamepad_controller
Normal file
Binary file not shown.
BIN
ros-control-c/build/keyboard_controller
Normal file
BIN
ros-control-c/build/keyboard_controller
Normal file
Binary file not shown.
26
ros-control-c/common/protocol.h
Normal file
26
ros-control-c/common/protocol.h
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
#ifndef PROTOCOL_H
|
||||||
|
#define PROTOCOL_H
|
||||||
|
|
||||||
|
#include <stdint.h>
|
||||||
|
|
||||||
|
#define DEFAULT_PORT 9870
|
||||||
|
#define DEFAULT_IP "127.0.0.1"
|
||||||
|
#define SEND_RATE_HZ 20
|
||||||
|
#define SEND_INTERVAL_US (1000000 / SEND_RATE_HZ)
|
||||||
|
|
||||||
|
#pragma pack(push, 1)
|
||||||
|
typedef struct {
|
||||||
|
float lx, ly, lz; /* linear velocity (m/s) */
|
||||||
|
float ax, ay, az; /* angular velocity (rad/s) */
|
||||||
|
} twist_cmd_t;
|
||||||
|
#pragma pack(pop)
|
||||||
|
|
||||||
|
#define TWIST_CMD_SIZE sizeof(twist_cmd_t) /* 24 bytes */
|
||||||
|
|
||||||
|
static inline void twist_cmd_zero(twist_cmd_t *cmd)
|
||||||
|
{
|
||||||
|
cmd->lx = cmd->ly = cmd->lz = 0.0f;
|
||||||
|
cmd->ax = cmd->ay = cmd->az = 0.0f;
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif /* PROTOCOL_H */
|
||||||
300
ros-control-c/common/teleop_transport.c
Normal file
300
ros-control-c/common/teleop_transport.c
Normal file
@@ -0,0 +1,300 @@
|
|||||||
|
#include "teleop_transport.h"
|
||||||
|
|
||||||
|
#include <arpa/inet.h>
|
||||||
|
#include <errno.h>
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <string.h>
|
||||||
|
#include <sys/socket.h>
|
||||||
|
#include <unistd.h>
|
||||||
|
|
||||||
|
static void teleop_transport_clear(teleop_transport_t *transport)
|
||||||
|
{
|
||||||
|
if (transport == NULL) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
memset(transport, 0, sizeof(*transport));
|
||||||
|
transport->mode = TELEOP_TRANSPORT_MODE_UDP;
|
||||||
|
transport->udp_fd = -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
int teleop_transport_parse_mode(const char *raw, teleop_transport_mode_t *out_mode)
|
||||||
|
{
|
||||||
|
if (raw == NULL || out_mode == NULL) {
|
||||||
|
errno = EINVAL;
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
if (strcmp(raw, "udp") == 0) {
|
||||||
|
*out_mode = TELEOP_TRANSPORT_MODE_UDP;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
if (strcmp(raw, "kcp") == 0) {
|
||||||
|
*out_mode = TELEOP_TRANSPORT_MODE_KCP;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
errno = EINVAL;
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
const char *teleop_transport_mode_name(teleop_transport_mode_t mode)
|
||||||
|
{
|
||||||
|
return mode == TELEOP_TRANSPORT_MODE_KCP ? "kcp" : "udp";
|
||||||
|
}
|
||||||
|
|
||||||
|
static void teleop_transport_log_incoming(const message_t *msg)
|
||||||
|
{
|
||||||
|
if (msg == NULL) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (msg->type) {
|
||||||
|
case MSG_TYPE_ERROR:
|
||||||
|
fprintf(stderr,
|
||||||
|
"teleop transport: server error from %s to %s: %.*s\n",
|
||||||
|
msg->from,
|
||||||
|
msg->to,
|
||||||
|
(int)msg->body_len,
|
||||||
|
msg->body == NULL ? "" : (const char *)msg->body);
|
||||||
|
break;
|
||||||
|
case MSG_TYPE_TEXT:
|
||||||
|
fprintf(stderr,
|
||||||
|
"teleop transport: dropped unexpected text from %s to %s: %.*s\n",
|
||||||
|
msg->from,
|
||||||
|
msg->to,
|
||||||
|
(int)msg->body_len,
|
||||||
|
msg->body == NULL ? "" : (const char *)msg->body);
|
||||||
|
break;
|
||||||
|
case MSG_TYPE_BINARY:
|
||||||
|
fprintf(stderr,
|
||||||
|
"teleop transport: dropped unexpected binary payload from %s to %s (%lu bytes)\n",
|
||||||
|
msg->from,
|
||||||
|
msg->to,
|
||||||
|
(unsigned long)msg->body_len);
|
||||||
|
break;
|
||||||
|
case MSG_TYPE_FILE:
|
||||||
|
fprintf(stderr,
|
||||||
|
"teleop transport: dropped unexpected file from %s to %s: %s (%lu bytes)\n",
|
||||||
|
msg->from,
|
||||||
|
msg->to,
|
||||||
|
msg->file_name,
|
||||||
|
(unsigned long)msg->body_len);
|
||||||
|
break;
|
||||||
|
case MSG_TYPE_REGISTER:
|
||||||
|
fprintf(stderr,
|
||||||
|
"teleop transport: dropped unexpected register message from %s to %s\n",
|
||||||
|
msg->from,
|
||||||
|
msg->to);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
fprintf(stderr,
|
||||||
|
"teleop transport: dropped unexpected message type %s from %s\n",
|
||||||
|
protocol_message_type_name(msg->type),
|
||||||
|
msg->from);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void *teleop_transport_kcp_recv_thread_main(void *arg)
|
||||||
|
{
|
||||||
|
teleop_transport_t *transport = (teleop_transport_t *)arg;
|
||||||
|
|
||||||
|
for (;;) {
|
||||||
|
message_t msg;
|
||||||
|
int rc;
|
||||||
|
|
||||||
|
if (transport->stop_requested) {
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
protocol_message_init(&msg);
|
||||||
|
rc = kcp_client_receive_timed(transport->kcp_client, &msg, 100);
|
||||||
|
if (rc == 1) {
|
||||||
|
protocol_message_clear(&msg);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (rc != 0) {
|
||||||
|
protocol_message_clear(&msg);
|
||||||
|
if (!transport->stop_requested) {
|
||||||
|
int saved_errno = errno;
|
||||||
|
fprintf(stderr,
|
||||||
|
"teleop transport: KCP receive loop stopped: %s (errno=%d)\n",
|
||||||
|
saved_errno != 0 ? strerror(saved_errno) : "unknown error",
|
||||||
|
saved_errno);
|
||||||
|
}
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
teleop_transport_log_incoming(&msg);
|
||||||
|
protocol_message_clear(&msg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static int teleop_transport_open_udp(teleop_transport_t *transport, const teleop_transport_config_t *config)
|
||||||
|
{
|
||||||
|
int sockfd;
|
||||||
|
|
||||||
|
if (transport == NULL || config == NULL || config->udp_ip == NULL) {
|
||||||
|
errno = EINVAL;
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
sockfd = socket(AF_INET, SOCK_DGRAM, 0);
|
||||||
|
if (sockfd < 0) {
|
||||||
|
perror("socket");
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
memset(&transport->udp_dest, 0, sizeof(transport->udp_dest));
|
||||||
|
transport->udp_dest.sin_family = AF_INET;
|
||||||
|
transport->udp_dest.sin_port = htons(config->udp_port);
|
||||||
|
if (inet_pton(AF_INET, config->udp_ip, &transport->udp_dest.sin_addr) <= 0) {
|
||||||
|
fprintf(stderr, "Invalid IP: %s\n", config->udp_ip);
|
||||||
|
close(sockfd);
|
||||||
|
errno = EINVAL;
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
transport->udp_fd = sockfd;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int teleop_transport_open_kcp(teleop_transport_t *transport, const teleop_transport_config_t *config)
|
||||||
|
{
|
||||||
|
kcp_conn_options_t options;
|
||||||
|
const char *relay_via;
|
||||||
|
|
||||||
|
if (transport == NULL || config == NULL ||
|
||||||
|
config->server_addr == NULL || config->peer_id == NULL || config->target_peer == NULL) {
|
||||||
|
errno = EINVAL;
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
kcp_conn_options_set_control_defaults(&options);
|
||||||
|
relay_via = (config->relay_via != NULL && config->relay_via[0] != '\0') ? config->relay_via : NULL;
|
||||||
|
transport->kcp_client = kcp_client_dial_with_options(
|
||||||
|
config->server_addr,
|
||||||
|
relay_via,
|
||||||
|
config->peer_id,
|
||||||
|
"",
|
||||||
|
"",
|
||||||
|
&options,
|
||||||
|
NULL,
|
||||||
|
NULL,
|
||||||
|
NULL,
|
||||||
|
KCP_DEFAULT_STATS_INTERVAL_MS
|
||||||
|
);
|
||||||
|
if (transport->kcp_client == NULL) {
|
||||||
|
int saved_errno = errno;
|
||||||
|
fprintf(stderr,
|
||||||
|
"teleop transport: failed to open KCP session as %s via %s%s%s: %s (errno=%d)\n",
|
||||||
|
config->peer_id,
|
||||||
|
config->server_addr,
|
||||||
|
relay_via != NULL ? ", relay=" : "",
|
||||||
|
relay_via != NULL ? relay_via : "",
|
||||||
|
saved_errno != 0 ? strerror(saved_errno) : "unknown error",
|
||||||
|
saved_errno);
|
||||||
|
errno = saved_errno;
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
int thread_rc = pthread_create(&transport->recv_thread, NULL, teleop_transport_kcp_recv_thread_main, transport);
|
||||||
|
if (thread_rc != 0) {
|
||||||
|
fprintf(stderr,
|
||||||
|
"teleop transport: failed to start KCP receive thread: %s (errno=%d)\n",
|
||||||
|
strerror(thread_rc),
|
||||||
|
thread_rc);
|
||||||
|
kcp_client_close(transport->kcp_client);
|
||||||
|
kcp_client_free(transport->kcp_client);
|
||||||
|
transport->kcp_client = NULL;
|
||||||
|
errno = thread_rc;
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
transport->recv_thread_started = 1;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
int teleop_transport_open(teleop_transport_t *transport, const teleop_transport_config_t *config)
|
||||||
|
{
|
||||||
|
if (transport == NULL || config == NULL) {
|
||||||
|
errno = EINVAL;
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
teleop_transport_clear(transport);
|
||||||
|
transport->mode = config->mode;
|
||||||
|
snprintf(transport->server_addr, sizeof(transport->server_addr), "%s",
|
||||||
|
config->server_addr == NULL ? "" : config->server_addr);
|
||||||
|
snprintf(transport->relay_via, sizeof(transport->relay_via), "%s",
|
||||||
|
config->relay_via == NULL ? "" : config->relay_via);
|
||||||
|
snprintf(transport->peer_id, sizeof(transport->peer_id), "%s",
|
||||||
|
config->peer_id == NULL ? "" : config->peer_id);
|
||||||
|
snprintf(transport->target_peer, sizeof(transport->target_peer), "%s",
|
||||||
|
config->target_peer == NULL ? "" : config->target_peer);
|
||||||
|
|
||||||
|
if (config->mode == TELEOP_TRANSPORT_MODE_KCP) {
|
||||||
|
return teleop_transport_open_kcp(transport, config);
|
||||||
|
}
|
||||||
|
return teleop_transport_open_udp(transport, config);
|
||||||
|
}
|
||||||
|
|
||||||
|
int teleop_transport_send_twist(teleop_transport_t *transport, const twist_cmd_t *cmd)
|
||||||
|
{
|
||||||
|
if (transport == NULL || cmd == NULL) {
|
||||||
|
errno = EINVAL;
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (transport->mode == TELEOP_TRANSPORT_MODE_KCP) {
|
||||||
|
if (kcp_client_send_binary(transport->kcp_client, transport->target_peer, cmd, TWIST_CMD_SIZE) != 0) {
|
||||||
|
int saved_errno = errno;
|
||||||
|
fprintf(stderr,
|
||||||
|
"teleop transport: failed to send KCP payload to %s: %s (errno=%d)\n",
|
||||||
|
transport->target_peer,
|
||||||
|
saved_errno != 0 ? strerror(saved_errno) : "unknown error",
|
||||||
|
saved_errno);
|
||||||
|
errno = saved_errno;
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
ssize_t sent = sendto(transport->udp_fd, cmd, TWIST_CMD_SIZE, 0,
|
||||||
|
(const struct sockaddr *)&transport->udp_dest, sizeof(transport->udp_dest));
|
||||||
|
if (sent < 0) {
|
||||||
|
perror("sendto");
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
if ((size_t)sent != TWIST_CMD_SIZE) {
|
||||||
|
fprintf(stderr, "sendto: short send (%zd/%zu)\n", sent, (size_t)TWIST_CMD_SIZE);
|
||||||
|
errno = EIO;
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
void teleop_transport_close(teleop_transport_t *transport)
|
||||||
|
{
|
||||||
|
if (transport == NULL) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
transport->stop_requested = 1;
|
||||||
|
if (transport->kcp_client != NULL) {
|
||||||
|
kcp_client_close(transport->kcp_client);
|
||||||
|
}
|
||||||
|
if (transport->recv_thread_started) {
|
||||||
|
pthread_join(transport->recv_thread, NULL);
|
||||||
|
transport->recv_thread_started = 0;
|
||||||
|
}
|
||||||
|
if (transport->kcp_client != NULL) {
|
||||||
|
kcp_client_free(transport->kcp_client);
|
||||||
|
transport->kcp_client = NULL;
|
||||||
|
}
|
||||||
|
if (transport->udp_fd >= 0) {
|
||||||
|
close(transport->udp_fd);
|
||||||
|
transport->udp_fd = -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
59
ros-control-c/common/teleop_transport.h
Normal file
59
ros-control-c/common/teleop_transport.h
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
#ifndef TELEOP_TRANSPORT_H
|
||||||
|
#define TELEOP_TRANSPORT_H
|
||||||
|
|
||||||
|
#include <netinet/in.h>
|
||||||
|
#include <pthread.h>
|
||||||
|
|
||||||
|
#include "protocol.h"
|
||||||
|
#include "peer_kcp_client.h"
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
extern "C" {
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#define DEFAULT_KCP_SERVER_ADDR "127.0.0.1:9002"
|
||||||
|
#define DEFAULT_KCP_KEYBOARD_PEER_ID "ros-keyboard-ctrl"
|
||||||
|
#define DEFAULT_KCP_GAMEPAD_PEER_ID "ros-gamepad-ctrl"
|
||||||
|
#define DEFAULT_KCP_TARGET_PEER_ID "ros-bridge-ctrl"
|
||||||
|
|
||||||
|
typedef enum teleop_transport_mode {
|
||||||
|
TELEOP_TRANSPORT_MODE_UDP = 0,
|
||||||
|
TELEOP_TRANSPORT_MODE_KCP = 1
|
||||||
|
} teleop_transport_mode_t;
|
||||||
|
|
||||||
|
typedef struct teleop_transport_config {
|
||||||
|
teleop_transport_mode_t mode;
|
||||||
|
const char *udp_ip;
|
||||||
|
int udp_port;
|
||||||
|
const char *server_addr;
|
||||||
|
const char *relay_via;
|
||||||
|
const char *peer_id;
|
||||||
|
const char *target_peer;
|
||||||
|
} teleop_transport_config_t;
|
||||||
|
|
||||||
|
typedef struct teleop_transport {
|
||||||
|
teleop_transport_mode_t mode;
|
||||||
|
int udp_fd;
|
||||||
|
struct sockaddr_in udp_dest;
|
||||||
|
kcp_client_t *kcp_client;
|
||||||
|
pthread_t recv_thread;
|
||||||
|
int recv_thread_started;
|
||||||
|
volatile int stop_requested;
|
||||||
|
char server_addr[OMNI_MAX_ADDR_TEXT];
|
||||||
|
char relay_via[OMNI_MAX_ADDR_TEXT];
|
||||||
|
char peer_id[OMNI_MAX_PEER_ID];
|
||||||
|
char target_peer[OMNI_MAX_PEER_ID];
|
||||||
|
} teleop_transport_t;
|
||||||
|
|
||||||
|
int teleop_transport_parse_mode(const char *raw, teleop_transport_mode_t *out_mode);
|
||||||
|
const char *teleop_transport_mode_name(teleop_transport_mode_t mode);
|
||||||
|
|
||||||
|
int teleop_transport_open(teleop_transport_t *transport, const teleop_transport_config_t *config);
|
||||||
|
int teleop_transport_send_twist(teleop_transport_t *transport, const twist_cmd_t *cmd);
|
||||||
|
void teleop_transport_close(teleop_transport_t *transport);
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#endif /* TELEOP_TRANSPORT_H */
|
||||||
293
ros-control-c/remote/gamepad_controller.c
Normal file
293
ros-control-c/remote/gamepad_controller.c
Normal file
@@ -0,0 +1,293 @@
|
|||||||
|
/*
|
||||||
|
* gamepad_controller.c — Gamepad/joystick teleop over UDP or KCP
|
||||||
|
*
|
||||||
|
* Uses the Linux joystick API (/dev/input/js*).
|
||||||
|
* Zero external dependencies.
|
||||||
|
*
|
||||||
|
* Xbox controller mapping (xpad driver):
|
||||||
|
* Left stick Y (axis 1) → linear.x (forward/back, inverted)
|
||||||
|
* Left stick X (axis 0) → linear.y (strafe)
|
||||||
|
* Right stick X (axis 3) → angular.z (turn)
|
||||||
|
* Button A (0) → emergency stop
|
||||||
|
* Button B (1) → quit
|
||||||
|
*
|
||||||
|
* Build: gcc -Wall -O2 -I../common -o gamepad_controller gamepad_controller.c -lm
|
||||||
|
* Usage: ./gamepad_controller [-i IP] [-p PORT] [-d /dev/input/js0]
|
||||||
|
* [-l MAX_LIN] [-a MAX_ANG] [-z DEADZONE]
|
||||||
|
* [-t udp|kcp] [-s SERVER] [-r RELAY]
|
||||||
|
* [-I PEER_ID] [-T TARGET_PEER]
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <string.h>
|
||||||
|
#include <unistd.h>
|
||||||
|
#include <fcntl.h>
|
||||||
|
#include <signal.h>
|
||||||
|
#include <math.h>
|
||||||
|
#include <errno.h>
|
||||||
|
#include <sys/select.h>
|
||||||
|
#include <sys/time.h>
|
||||||
|
#include <sys/ioctl.h>
|
||||||
|
#include <getopt.h>
|
||||||
|
#include <linux/joystick.h>
|
||||||
|
|
||||||
|
#include "../common/protocol.h"
|
||||||
|
#include "../common/teleop_transport.h"
|
||||||
|
|
||||||
|
/* ── config ─────────────────────────────────────────────────────────── */
|
||||||
|
#define MAX_AXES 16
|
||||||
|
#define MAX_BUTTONS 16
|
||||||
|
#define JS_AXIS_MAX 32767.0f
|
||||||
|
|
||||||
|
/* Xbox mapping indices */
|
||||||
|
#define AXIS_LX 0 /* left stick X → strafe */
|
||||||
|
#define AXIS_LY 1 /* left stick Y → fwd/back (inverted) */
|
||||||
|
#define AXIS_RX 3 /* right stick X → turn */
|
||||||
|
|
||||||
|
#define BTN_STOP 0 /* A → emergency stop */
|
||||||
|
#define BTN_QUIT 1 /* B → quit */
|
||||||
|
|
||||||
|
static volatile sig_atomic_t g_running = 1;
|
||||||
|
|
||||||
|
static void sigint_handler(int sig) { (void)sig; g_running = 0; }
|
||||||
|
|
||||||
|
static int parse_port(const char *text, int *port_out)
|
||||||
|
{
|
||||||
|
char *end = NULL;
|
||||||
|
long value = strtol(text, &end, 10);
|
||||||
|
|
||||||
|
if (end == text || *end != '\0' || value < 1 || value > 65535)
|
||||||
|
return -1;
|
||||||
|
|
||||||
|
*port_out = (int)value;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── apply deadzone ─────────────────────────────────────────────────── */
|
||||||
|
static float apply_deadzone(float v, float dz)
|
||||||
|
{
|
||||||
|
if (fabsf(v) < dz) return 0.0f;
|
||||||
|
/* rescale so the output starts from 0 just outside the deadzone */
|
||||||
|
float sign = (v > 0) ? 1.0f : -1.0f;
|
||||||
|
return sign * (fabsf(v) - dz) / (1.0f - dz);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── usage ──────────────────────────────────────────────────────────── */
|
||||||
|
static void usage(const char *prog)
|
||||||
|
{
|
||||||
|
fprintf(stderr,
|
||||||
|
"Usage: %s [options]\n"
|
||||||
|
" -i IP target IP (default %s)\n"
|
||||||
|
" -p PORT target port (default %d)\n"
|
||||||
|
" -d DEVICE joystick device (default /dev/input/js0)\n"
|
||||||
|
" -l SPEED max linear m/s (default 0.5)\n"
|
||||||
|
" -a SPEED max angular rad/s (default 0.5)\n"
|
||||||
|
" -z DZ deadzone 0<=DZ<1 (default 0.1)\n"
|
||||||
|
" -t MODE transport mode udp|kcp (default udp)\n"
|
||||||
|
" -s ADDR KCP server addr (default %s)\n"
|
||||||
|
" -r ADDR KCP relay addr (default none)\n"
|
||||||
|
" -I ID local KCP peer id (default %s)\n"
|
||||||
|
" -T ID target KCP peer id (default %s)\n"
|
||||||
|
" -h show help\n",
|
||||||
|
prog, DEFAULT_IP, DEFAULT_PORT,
|
||||||
|
DEFAULT_KCP_SERVER_ADDR,
|
||||||
|
DEFAULT_KCP_GAMEPAD_PEER_ID,
|
||||||
|
DEFAULT_KCP_TARGET_PEER_ID);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ──────────────────────────────────────────────────────────────────── */
|
||||||
|
int main(int argc, char *argv[])
|
||||||
|
{
|
||||||
|
char ip[64] = DEFAULT_IP;
|
||||||
|
int port = DEFAULT_PORT;
|
||||||
|
char device[128] = "/dev/input/js0";
|
||||||
|
char kcp_server[OMNI_MAX_ADDR_TEXT] = DEFAULT_KCP_SERVER_ADDR;
|
||||||
|
char kcp_relay[OMNI_MAX_ADDR_TEXT] = "";
|
||||||
|
char peer_id[OMNI_MAX_PEER_ID] = DEFAULT_KCP_GAMEPAD_PEER_ID;
|
||||||
|
char target_peer[OMNI_MAX_PEER_ID] = DEFAULT_KCP_TARGET_PEER_ID;
|
||||||
|
float max_lin = 0.5f;
|
||||||
|
float max_ang = 0.5f;
|
||||||
|
float deadzone = 0.1f;
|
||||||
|
teleop_transport_mode_t transport_mode = TELEOP_TRANSPORT_MODE_UDP;
|
||||||
|
teleop_transport_t transport;
|
||||||
|
teleop_transport_config_t transport_config;
|
||||||
|
|
||||||
|
int opt;
|
||||||
|
while ((opt = getopt(argc, argv, "i:p:d:l:a:z:t:s:r:I:T:h")) != -1) {
|
||||||
|
switch (opt) {
|
||||||
|
case 'i': strncpy(ip, optarg, sizeof(ip)-1); ip[sizeof(ip)-1] = '\0'; break;
|
||||||
|
case 'p':
|
||||||
|
if (parse_port(optarg, &port) != 0) {
|
||||||
|
fprintf(stderr, "Invalid port: %s (expected 1-65535)\n", optarg);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'd': strncpy(device, optarg, sizeof(device)-1); device[sizeof(device)-1] = '\0'; break;
|
||||||
|
case 'l': max_lin = strtof(optarg, NULL); break;
|
||||||
|
case 'a': max_ang = strtof(optarg, NULL); break;
|
||||||
|
case 'z': deadzone = strtof(optarg, NULL); break;
|
||||||
|
case 't':
|
||||||
|
if (teleop_transport_parse_mode(optarg, &transport_mode) != 0) {
|
||||||
|
fprintf(stderr, "Invalid transport mode: %s (expected udp or kcp)\n", optarg);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 's': strncpy(kcp_server, optarg, sizeof(kcp_server)-1); kcp_server[sizeof(kcp_server)-1] = '\0'; break;
|
||||||
|
case 'r': strncpy(kcp_relay, optarg, sizeof(kcp_relay)-1); kcp_relay[sizeof(kcp_relay)-1] = '\0'; break;
|
||||||
|
case 'I': strncpy(peer_id, optarg, sizeof(peer_id)-1); peer_id[sizeof(peer_id)-1] = '\0'; break;
|
||||||
|
case 'T': strncpy(target_peer, optarg, sizeof(target_peer)-1); target_peer[sizeof(target_peer)-1] = '\0'; break;
|
||||||
|
default: usage(argv[0]); return (opt == 'h') ? 0 : 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (deadzone < 0.0f || deadzone >= 1.0f) {
|
||||||
|
fprintf(stderr, "Invalid deadzone %.3f: expected 0 <= dz < 1\n", deadzone);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
signal(SIGINT, sigint_handler);
|
||||||
|
|
||||||
|
/* ── open joystick ───────────────────────────────────────────── */
|
||||||
|
int jsfd = open(device, O_RDONLY | O_NONBLOCK);
|
||||||
|
if (jsfd < 0) {
|
||||||
|
fprintf(stderr, "Cannot open %s: %s\n"
|
||||||
|
" Hint: connect Xbox controller, check 'ls /dev/input/js*'\n",
|
||||||
|
device, strerror(errno));
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
char js_name[128] = "Unknown";
|
||||||
|
ioctl(jsfd, JSIOCGNAME(sizeof(js_name)), js_name);
|
||||||
|
|
||||||
|
int num_axes = 0, num_buttons = 0;
|
||||||
|
ioctl(jsfd, JSIOCGAXES, &num_axes);
|
||||||
|
ioctl(jsfd, JSIOCGBUTTONS, &num_buttons);
|
||||||
|
|
||||||
|
printf("========================================\n");
|
||||||
|
printf(" Gamepad Teleop Controller\n");
|
||||||
|
printf("========================================\n");
|
||||||
|
printf(" Device : %s\n", device);
|
||||||
|
printf(" Name : %s\n", js_name);
|
||||||
|
printf(" Axes : %d Buttons: %d\n", num_axes, num_buttons);
|
||||||
|
printf(" Transport: %s\n", teleop_transport_mode_name(transport_mode));
|
||||||
|
if (transport_mode == TELEOP_TRANSPORT_MODE_KCP) {
|
||||||
|
printf(" KCP server: %s\n", kcp_server);
|
||||||
|
if (kcp_relay[0] != '\0')
|
||||||
|
printf(" Relay via : %s\n", kcp_relay);
|
||||||
|
printf(" Peer ID : %s -> %s\n", peer_id, target_peer);
|
||||||
|
} else {
|
||||||
|
printf(" Target : %s:%d\n", ip, port);
|
||||||
|
}
|
||||||
|
printf(" Linear : %.2f m/s Angular: %.2f rad/s\n", max_lin, max_ang);
|
||||||
|
printf(" Deadzone: %.2f\n", deadzone);
|
||||||
|
printf("----------------------------------------\n");
|
||||||
|
printf(" Left stick → forward/back + strafe\n");
|
||||||
|
printf(" Right stick → turn\n");
|
||||||
|
printf(" A button → emergency stop\n");
|
||||||
|
printf(" B button → quit\n");
|
||||||
|
printf("========================================\n\n");
|
||||||
|
|
||||||
|
memset(&transport_config, 0, sizeof(transport_config));
|
||||||
|
transport_config.mode = transport_mode;
|
||||||
|
transport_config.udp_ip = ip;
|
||||||
|
transport_config.udp_port = port;
|
||||||
|
transport_config.server_addr = kcp_server;
|
||||||
|
transport_config.relay_via = kcp_relay;
|
||||||
|
transport_config.peer_id = peer_id;
|
||||||
|
transport_config.target_peer = target_peer;
|
||||||
|
|
||||||
|
if (teleop_transport_open(&transport, &transport_config) != 0) {
|
||||||
|
close(jsfd);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── state ───────────────────────────────────────────────────── */
|
||||||
|
float axes[MAX_AXES];
|
||||||
|
int buttons[MAX_BUTTONS];
|
||||||
|
memset(axes, 0, sizeof(axes));
|
||||||
|
memset(buttons, 0, sizeof(buttons));
|
||||||
|
|
||||||
|
twist_cmd_t cmd;
|
||||||
|
twist_cmd_zero(&cmd);
|
||||||
|
|
||||||
|
struct timeval last_send;
|
||||||
|
gettimeofday(&last_send, NULL);
|
||||||
|
|
||||||
|
int e_stop = 0;
|
||||||
|
|
||||||
|
/* ── main loop ───────────────────────────────────────────────── */
|
||||||
|
while (g_running) {
|
||||||
|
/* read all pending joystick events */
|
||||||
|
struct js_event ev;
|
||||||
|
while (read(jsfd, &ev, sizeof(ev)) == sizeof(ev)) {
|
||||||
|
ev.type &= ~JS_EVENT_INIT; /* strip init flag */
|
||||||
|
if (ev.type == JS_EVENT_AXIS && ev.number < MAX_AXES) {
|
||||||
|
axes[ev.number] = (float)ev.value / JS_AXIS_MAX;
|
||||||
|
} else if (ev.type == JS_EVENT_BUTTON && ev.number < MAX_BUTTONS) {
|
||||||
|
buttons[ev.number] = ev.value;
|
||||||
|
if (ev.number == BTN_QUIT && ev.value) {
|
||||||
|
g_running = 0;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (ev.number == BTN_STOP && ev.value) {
|
||||||
|
e_stop = !e_stop;
|
||||||
|
if (e_stop)
|
||||||
|
printf("\r ** EMERGENCY STOP ** ");
|
||||||
|
else
|
||||||
|
printf("\r ** E-STOP released ** ");
|
||||||
|
fflush(stdout);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/* EAGAIN is expected in non-blocking mode */
|
||||||
|
if (errno != EAGAIN && errno != 0) {
|
||||||
|
perror("read joystick");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
errno = 0;
|
||||||
|
|
||||||
|
/* map axes → twist (skip if e-stopped) */
|
||||||
|
if (e_stop) {
|
||||||
|
twist_cmd_zero(&cmd);
|
||||||
|
} else {
|
||||||
|
float lx_raw = apply_deadzone(-axes[AXIS_LY], deadzone); /* Y inverted */
|
||||||
|
float ly_raw = apply_deadzone(-axes[AXIS_LX], deadzone);
|
||||||
|
float az_raw = apply_deadzone(-axes[AXIS_RX], deadzone);
|
||||||
|
|
||||||
|
cmd.lx = lx_raw * max_lin;
|
||||||
|
cmd.ly = ly_raw * max_lin;
|
||||||
|
cmd.lz = 0.0f;
|
||||||
|
cmd.ax = 0.0f;
|
||||||
|
cmd.ay = 0.0f;
|
||||||
|
cmd.az = az_raw * max_ang;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* rate-limit sending */
|
||||||
|
struct timeval now;
|
||||||
|
gettimeofday(&now, NULL);
|
||||||
|
long elapsed = (now.tv_sec - last_send.tv_sec) * 1000000
|
||||||
|
+ (now.tv_usec - last_send.tv_usec);
|
||||||
|
if (elapsed < SEND_INTERVAL_US) {
|
||||||
|
usleep(5000); /* 5 ms sleep to avoid busy-spin */
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
last_send = now;
|
||||||
|
|
||||||
|
teleop_transport_send_twist(&transport, &cmd);
|
||||||
|
|
||||||
|
printf("\r cmd: lx=%+.2f ly=%+.2f az=%+.2f | raw: LY=%+.2f LX=%+.2f RX=%+.2f ",
|
||||||
|
cmd.lx, cmd.ly, cmd.az,
|
||||||
|
axes[AXIS_LY], axes[AXIS_LX], axes[AXIS_RX]);
|
||||||
|
fflush(stdout);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* send final stop */
|
||||||
|
twist_cmd_zero(&cmd);
|
||||||
|
teleop_transport_send_twist(&transport, &cmd);
|
||||||
|
|
||||||
|
close(jsfd);
|
||||||
|
teleop_transport_close(&transport);
|
||||||
|
printf("\nStopped.\n");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
361
ros-control-c/remote/keyboard_controller.c
Normal file
361
ros-control-c/remote/keyboard_controller.c
Normal file
@@ -0,0 +1,361 @@
|
|||||||
|
/*
|
||||||
|
* keyboard_controller.c - Keyboard teleop over UDP or KCP
|
||||||
|
*
|
||||||
|
* Keys:
|
||||||
|
* W/Up forward S/Down backward
|
||||||
|
* A/Left turn left D/Right turn right
|
||||||
|
* Q strafe left E strafe right
|
||||||
|
* Space stop
|
||||||
|
* [ / ] linear speed down/up
|
||||||
|
* - / = angular speed down/up
|
||||||
|
* Ctrl-C quit
|
||||||
|
*
|
||||||
|
* Build: gcc -Wall -O2 -I../common -o keyboard_controller keyboard_controller.c
|
||||||
|
* Usage: ./keyboard_controller [-i IP] [-p PORT] [-l MAX_LIN] [-a MAX_ANG]
|
||||||
|
* [-t udp|kcp] [-s SERVER] [-r RELAY]
|
||||||
|
* [-I PEER_ID] [-T TARGET_PEER]
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include <getopt.h>
|
||||||
|
#include <signal.h>
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <string.h>
|
||||||
|
#include <sys/select.h>
|
||||||
|
#include <sys/time.h>
|
||||||
|
#include <termios.h>
|
||||||
|
#include <unistd.h>
|
||||||
|
|
||||||
|
#include "../common/protocol.h"
|
||||||
|
#include "../common/teleop_transport.h"
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Terminals do not provide key-release events, so keep the last motion command
|
||||||
|
* alive briefly to bridge the initial auto-repeat delay while a key is held.
|
||||||
|
*/
|
||||||
|
#define KEY_HOLD_TIMEOUT_US 500000L
|
||||||
|
|
||||||
|
static struct termios g_orig_termios;
|
||||||
|
static volatile sig_atomic_t g_running = 1;
|
||||||
|
|
||||||
|
static long elapsed_us(const struct timeval *start, const struct timeval *end)
|
||||||
|
{
|
||||||
|
return (end->tv_sec - start->tv_sec) * 1000000L
|
||||||
|
+ (end->tv_usec - start->tv_usec);
|
||||||
|
}
|
||||||
|
|
||||||
|
static int parse_port(const char *text, int *port_out)
|
||||||
|
{
|
||||||
|
char *end = NULL;
|
||||||
|
long value = strtol(text, &end, 10);
|
||||||
|
|
||||||
|
if (end == text || *end != '\0' || value < 1 || value > 65535)
|
||||||
|
return -1;
|
||||||
|
|
||||||
|
*port_out = (int)value;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void restore_terminal(void)
|
||||||
|
{
|
||||||
|
tcsetattr(STDIN_FILENO, TCSANOW, &g_orig_termios);
|
||||||
|
printf("\n\033[?25h");
|
||||||
|
fflush(stdout);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void sigint_handler(int sig)
|
||||||
|
{
|
||||||
|
(void)sig;
|
||||||
|
g_running = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void set_raw_mode(void)
|
||||||
|
{
|
||||||
|
struct termios raw;
|
||||||
|
tcgetattr(STDIN_FILENO, &g_orig_termios);
|
||||||
|
atexit(restore_terminal);
|
||||||
|
raw = g_orig_termios;
|
||||||
|
raw.c_lflag &= ~(ICANON | ECHO | ISIG);
|
||||||
|
raw.c_cc[VMIN] = 0;
|
||||||
|
raw.c_cc[VTIME] = 0;
|
||||||
|
tcsetattr(STDIN_FILENO, TCSANOW, &raw);
|
||||||
|
}
|
||||||
|
|
||||||
|
static int read_key(long timeout_us)
|
||||||
|
{
|
||||||
|
fd_set fds;
|
||||||
|
struct timeval tv;
|
||||||
|
unsigned char c;
|
||||||
|
|
||||||
|
FD_ZERO(&fds);
|
||||||
|
FD_SET(STDIN_FILENO, &fds);
|
||||||
|
tv.tv_sec = timeout_us / 1000000L;
|
||||||
|
tv.tv_usec = timeout_us % 1000000L;
|
||||||
|
|
||||||
|
if (select(STDIN_FILENO + 1, &fds, NULL, NULL, &tv) <= 0)
|
||||||
|
return -1;
|
||||||
|
if (read(STDIN_FILENO, &c, 1) != 1)
|
||||||
|
return -1;
|
||||||
|
|
||||||
|
if (c == 0x1B) {
|
||||||
|
unsigned char seq[2];
|
||||||
|
|
||||||
|
tv.tv_sec = 0;
|
||||||
|
tv.tv_usec = 20000;
|
||||||
|
FD_ZERO(&fds);
|
||||||
|
FD_SET(STDIN_FILENO, &fds);
|
||||||
|
if (select(STDIN_FILENO + 1, &fds, NULL, NULL, &tv) <= 0)
|
||||||
|
return 0x1B;
|
||||||
|
if (read(STDIN_FILENO, &seq[0], 1) != 1)
|
||||||
|
return 0x1B;
|
||||||
|
|
||||||
|
FD_ZERO(&fds);
|
||||||
|
FD_SET(STDIN_FILENO, &fds);
|
||||||
|
tv.tv_sec = 0;
|
||||||
|
tv.tv_usec = 20000;
|
||||||
|
if (select(STDIN_FILENO + 1, &fds, NULL, NULL, &tv) <= 0)
|
||||||
|
return 0x1B;
|
||||||
|
if (read(STDIN_FILENO, &seq[1], 1) != 1)
|
||||||
|
return 0x1B;
|
||||||
|
|
||||||
|
if (seq[0] == '[') {
|
||||||
|
switch (seq[1]) {
|
||||||
|
case 'A': return 'W';
|
||||||
|
case 'B': return 'S';
|
||||||
|
case 'D': return 'A';
|
||||||
|
case 'C': return 'D';
|
||||||
|
default: break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 0x1B;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (c >= 'a' && c <= 'z')
|
||||||
|
c = (unsigned char)(c - ('a' - 'A'));
|
||||||
|
return c;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void print_banner(void)
|
||||||
|
{
|
||||||
|
printf("\033[2J\033[H");
|
||||||
|
printf("========================================\n");
|
||||||
|
printf(" Keyboard Teleop Controller\n");
|
||||||
|
printf("========================================\n");
|
||||||
|
printf(" W/Up : forward S/Down : back\n");
|
||||||
|
printf(" A/Left : turn left D/Right: turn right\n");
|
||||||
|
printf(" Q : strafe left E : strafe right\n");
|
||||||
|
printf(" Space : stop\n");
|
||||||
|
printf(" [ / ] : linear speed -/+\n");
|
||||||
|
printf(" - / = : angular speed -/+\n");
|
||||||
|
printf(" Ctrl-C : quit\n");
|
||||||
|
printf("========================================\n\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
static void usage(const char *prog)
|
||||||
|
{
|
||||||
|
fprintf(stderr,
|
||||||
|
"Usage: %s [options]\n"
|
||||||
|
" -i IP target IP (default %s)\n"
|
||||||
|
" -p PORT target port (default %d)\n"
|
||||||
|
" -l SPEED max linear speed m/s (default 0.5)\n"
|
||||||
|
" -a SPEED max angular speed rad/s (default 0.5)\n"
|
||||||
|
" -t MODE transport mode udp|kcp (default udp)\n"
|
||||||
|
" -s ADDR KCP server addr (default %s)\n"
|
||||||
|
" -r ADDR KCP relay addr (default none)\n"
|
||||||
|
" -I ID local KCP peer id (default %s)\n"
|
||||||
|
" -T ID target KCP peer id (default %s)\n"
|
||||||
|
" -h show help\n",
|
||||||
|
prog, DEFAULT_IP, DEFAULT_PORT,
|
||||||
|
DEFAULT_KCP_SERVER_ADDR,
|
||||||
|
DEFAULT_KCP_KEYBOARD_PEER_ID,
|
||||||
|
DEFAULT_KCP_TARGET_PEER_ID);
|
||||||
|
}
|
||||||
|
|
||||||
|
int main(int argc, char *argv[])
|
||||||
|
{
|
||||||
|
char ip[64] = DEFAULT_IP;
|
||||||
|
int port = DEFAULT_PORT;
|
||||||
|
char kcp_server[OMNI_MAX_ADDR_TEXT] = DEFAULT_KCP_SERVER_ADDR;
|
||||||
|
char kcp_relay[OMNI_MAX_ADDR_TEXT] = "";
|
||||||
|
char peer_id[OMNI_MAX_PEER_ID] = DEFAULT_KCP_KEYBOARD_PEER_ID;
|
||||||
|
char target_peer[OMNI_MAX_PEER_ID] = DEFAULT_KCP_TARGET_PEER_ID;
|
||||||
|
float max_lin = 0.5f;
|
||||||
|
float max_ang = 0.5f;
|
||||||
|
const float speed_step = 0.1f;
|
||||||
|
teleop_transport_mode_t transport_mode = TELEOP_TRANSPORT_MODE_UDP;
|
||||||
|
teleop_transport_t transport;
|
||||||
|
teleop_transport_config_t transport_config;
|
||||||
|
|
||||||
|
int opt;
|
||||||
|
while ((opt = getopt(argc, argv, "i:p:l:a:t:s:r:I:T:h")) != -1) {
|
||||||
|
switch (opt) {
|
||||||
|
case 'i':
|
||||||
|
strncpy(ip, optarg, sizeof(ip) - 1);
|
||||||
|
ip[sizeof(ip) - 1] = '\0';
|
||||||
|
break;
|
||||||
|
case 'p':
|
||||||
|
if (parse_port(optarg, &port) != 0) {
|
||||||
|
fprintf(stderr, "Invalid port: %s (expected 1-65535)\n", optarg);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'l':
|
||||||
|
max_lin = strtof(optarg, NULL);
|
||||||
|
break;
|
||||||
|
case 'a':
|
||||||
|
max_ang = strtof(optarg, NULL);
|
||||||
|
break;
|
||||||
|
case 't':
|
||||||
|
if (teleop_transport_parse_mode(optarg, &transport_mode) != 0) {
|
||||||
|
fprintf(stderr, "Invalid transport mode: %s (expected udp or kcp)\n", optarg);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 's':
|
||||||
|
strncpy(kcp_server, optarg, sizeof(kcp_server) - 1);
|
||||||
|
kcp_server[sizeof(kcp_server) - 1] = '\0';
|
||||||
|
break;
|
||||||
|
case 'r':
|
||||||
|
strncpy(kcp_relay, optarg, sizeof(kcp_relay) - 1);
|
||||||
|
kcp_relay[sizeof(kcp_relay) - 1] = '\0';
|
||||||
|
break;
|
||||||
|
case 'I':
|
||||||
|
strncpy(peer_id, optarg, sizeof(peer_id) - 1);
|
||||||
|
peer_id[sizeof(peer_id) - 1] = '\0';
|
||||||
|
break;
|
||||||
|
case 'T':
|
||||||
|
strncpy(target_peer, optarg, sizeof(target_peer) - 1);
|
||||||
|
target_peer[sizeof(target_peer) - 1] = '\0';
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
usage(argv[0]);
|
||||||
|
return (opt == 'h') ? 0 : 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
memset(&transport_config, 0, sizeof(transport_config));
|
||||||
|
transport_config.mode = transport_mode;
|
||||||
|
transport_config.udp_ip = ip;
|
||||||
|
transport_config.udp_port = port;
|
||||||
|
transport_config.server_addr = kcp_server;
|
||||||
|
transport_config.relay_via = kcp_relay;
|
||||||
|
transport_config.peer_id = peer_id;
|
||||||
|
transport_config.target_peer = target_peer;
|
||||||
|
|
||||||
|
if (teleop_transport_open(&transport, &transport_config) != 0) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
set_raw_mode();
|
||||||
|
signal(SIGINT, sigint_handler);
|
||||||
|
print_banner();
|
||||||
|
printf(" Transport: %s\n", teleop_transport_mode_name(transport_mode));
|
||||||
|
if (transport_mode == TELEOP_TRANSPORT_MODE_KCP) {
|
||||||
|
printf(" KCP server: %s\n", kcp_server);
|
||||||
|
if (kcp_relay[0] != '\0')
|
||||||
|
printf(" Relay via : %s\n", kcp_relay);
|
||||||
|
printf(" Peer ID : %s -> %s\n", peer_id, target_peer);
|
||||||
|
} else {
|
||||||
|
printf(" Target: %s:%d\n", ip, port);
|
||||||
|
}
|
||||||
|
printf(" Linear: %.2f m/s Angular: %.2f rad/s\n\n", max_lin, max_ang);
|
||||||
|
printf("\033[?25l");
|
||||||
|
|
||||||
|
twist_cmd_t cmd;
|
||||||
|
twist_cmd_zero(&cmd);
|
||||||
|
|
||||||
|
struct timeval last_send;
|
||||||
|
struct timeval last_motion_key;
|
||||||
|
gettimeofday(&last_send, NULL);
|
||||||
|
last_motion_key = last_send;
|
||||||
|
|
||||||
|
while (g_running) {
|
||||||
|
int key = read_key(SEND_INTERVAL_US);
|
||||||
|
|
||||||
|
if (key >= 0) {
|
||||||
|
twist_cmd_zero(&cmd);
|
||||||
|
switch (key) {
|
||||||
|
case 'W':
|
||||||
|
cmd.lx = max_lin;
|
||||||
|
gettimeofday(&last_motion_key, NULL);
|
||||||
|
break;
|
||||||
|
case 'S':
|
||||||
|
cmd.lx = -max_lin;
|
||||||
|
gettimeofday(&last_motion_key, NULL);
|
||||||
|
break;
|
||||||
|
case 'A':
|
||||||
|
cmd.az = max_ang;
|
||||||
|
gettimeofday(&last_motion_key, NULL);
|
||||||
|
break;
|
||||||
|
case 'D':
|
||||||
|
cmd.az = -max_ang;
|
||||||
|
gettimeofday(&last_motion_key, NULL);
|
||||||
|
break;
|
||||||
|
case 'Q':
|
||||||
|
cmd.ly = max_lin;
|
||||||
|
gettimeofday(&last_motion_key, NULL);
|
||||||
|
break;
|
||||||
|
case 'E':
|
||||||
|
cmd.ly = -max_lin;
|
||||||
|
gettimeofday(&last_motion_key, NULL);
|
||||||
|
break;
|
||||||
|
case ' ':
|
||||||
|
break;
|
||||||
|
case ']':
|
||||||
|
max_lin += speed_step;
|
||||||
|
printf("\r Linear speed: %.2f m/s ", max_lin);
|
||||||
|
fflush(stdout);
|
||||||
|
continue;
|
||||||
|
case '[':
|
||||||
|
max_lin = (max_lin > speed_step) ? max_lin - speed_step : speed_step;
|
||||||
|
printf("\r Linear speed: %.2f m/s ", max_lin);
|
||||||
|
fflush(stdout);
|
||||||
|
continue;
|
||||||
|
case '=':
|
||||||
|
max_ang += speed_step;
|
||||||
|
printf("\r Angular speed: %.2f rad/s ", max_ang);
|
||||||
|
fflush(stdout);
|
||||||
|
continue;
|
||||||
|
case '-':
|
||||||
|
max_ang = (max_ang > speed_step) ? max_ang - speed_step : speed_step;
|
||||||
|
printf("\r Angular speed: %.2f rad/s ", max_ang);
|
||||||
|
fflush(stdout);
|
||||||
|
continue;
|
||||||
|
case 0x03:
|
||||||
|
g_running = 0;
|
||||||
|
continue;
|
||||||
|
default:
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
struct timeval now;
|
||||||
|
gettimeofday(&now, NULL);
|
||||||
|
if (elapsed_us(&last_motion_key, &now) > KEY_HOLD_TIMEOUT_US)
|
||||||
|
twist_cmd_zero(&cmd);
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
struct timeval now;
|
||||||
|
long elapsed;
|
||||||
|
|
||||||
|
gettimeofday(&now, NULL);
|
||||||
|
elapsed = elapsed_us(&last_send, &now);
|
||||||
|
if (elapsed < SEND_INTERVAL_US)
|
||||||
|
continue;
|
||||||
|
last_send = now;
|
||||||
|
}
|
||||||
|
|
||||||
|
teleop_transport_send_twist(&transport, &cmd);
|
||||||
|
|
||||||
|
printf("\r cmd: lx=%+.2f ly=%+.2f az=%+.2f | lin=%.2f ang=%.2f ",
|
||||||
|
cmd.lx, cmd.ly, cmd.az, max_lin, max_ang);
|
||||||
|
fflush(stdout);
|
||||||
|
}
|
||||||
|
|
||||||
|
twist_cmd_zero(&cmd);
|
||||||
|
teleop_transport_send_twist(&transport, &cmd);
|
||||||
|
|
||||||
|
teleop_transport_close(&transport);
|
||||||
|
printf("\nStopped.\n");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
247
ros-control-c/robot/udp_ros_bridge.py
Normal file
247
ros-control-c/robot/udp_ros_bridge.py
Normal file
@@ -0,0 +1,247 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
udp_ros_bridge.py — UDP/KCP → ROS2 TwistStamped bridge
|
||||||
|
|
||||||
|
Receives 24-byte binary twist commands from keyboard/gamepad controllers
|
||||||
|
via UDP or OmniSocket/KCP and publishes geometry_msgs/msg/TwistStamped to
|
||||||
|
/hric/robot/cmd_vel.
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
ros2 run <your_pkg> udp_ros_bridge (if installed as a ROS2 package)
|
||||||
|
python3 udp_ros_bridge.py (standalone)
|
||||||
|
|
||||||
|
ROS2 parameters:
|
||||||
|
transport (string) — udp or kcp (default udp)
|
||||||
|
udp_port (int) — UDP listen port (default 9870)
|
||||||
|
kcp_server (string) — KCP hub addr (default 127.0.0.1:9002)
|
||||||
|
kcp_relay_via (string) — optional relay addr (default "")
|
||||||
|
peer_id (string) — local KCP peer id (default ros-bridge-ctrl)
|
||||||
|
expected_sender (string) — optional sender filter (default "")
|
||||||
|
topic (string) — publish topic (default /hric/robot/cmd_vel)
|
||||||
|
frame_id (string) — TwistStamped frame_id (default pelvis)
|
||||||
|
timeout (float) — watchdog timeout seconds (default 0.5)
|
||||||
|
"""
|
||||||
|
|
||||||
|
from pathlib import Path
|
||||||
|
import struct
|
||||||
|
import socket
|
||||||
|
import sys
|
||||||
|
import threading
|
||||||
|
import time
|
||||||
|
|
||||||
|
import rclpy
|
||||||
|
from rclpy.node import Node
|
||||||
|
from geometry_msgs.msg import TwistStamped
|
||||||
|
|
||||||
|
TWIST_CMD_FMT = '<6f' # 6 little-endian floats, 24 bytes
|
||||||
|
TWIST_CMD_SIZE = struct.calcsize(TWIST_CMD_FMT)
|
||||||
|
|
||||||
|
|
||||||
|
def _load_omnisocket():
|
||||||
|
try:
|
||||||
|
from omnisocket import CONTROL_DEFAULTS, MSG_TYPE_BINARY, MSG_TYPE_ERROR, Session
|
||||||
|
return CONTROL_DEFAULTS, MSG_TYPE_BINARY, MSG_TYPE_ERROR, Session
|
||||||
|
except ImportError:
|
||||||
|
root = Path(__file__).resolve().parents[2]
|
||||||
|
python_dir = root / 'python'
|
||||||
|
if str(python_dir) not in sys.path:
|
||||||
|
sys.path.insert(0, str(python_dir))
|
||||||
|
from omnisocket import CONTROL_DEFAULTS, MSG_TYPE_BINARY, MSG_TYPE_ERROR, Session
|
||||||
|
return CONTROL_DEFAULTS, MSG_TYPE_BINARY, MSG_TYPE_ERROR, Session
|
||||||
|
|
||||||
|
|
||||||
|
class UdpTeleopBridge(Node):
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
super().__init__('udp_teleop_bridge')
|
||||||
|
|
||||||
|
# declare parameters
|
||||||
|
self.declare_parameter('transport', 'udp')
|
||||||
|
self.declare_parameter('udp_port', 9870)
|
||||||
|
self.declare_parameter('kcp_server', '127.0.0.1:9002')
|
||||||
|
self.declare_parameter('kcp_relay_via', '')
|
||||||
|
self.declare_parameter('peer_id', 'ros-bridge-ctrl')
|
||||||
|
self.declare_parameter('expected_sender', '')
|
||||||
|
self.declare_parameter('topic', '/hric/robot/cmd_vel')
|
||||||
|
self.declare_parameter('frame_id', 'pelvis')
|
||||||
|
self.declare_parameter('timeout', 0.5)
|
||||||
|
|
||||||
|
self._transport = str(self.get_parameter('transport').value).strip().lower()
|
||||||
|
self._port = self.get_parameter('udp_port').value
|
||||||
|
self._kcp_server = str(self.get_parameter('kcp_server').value)
|
||||||
|
self._kcp_relay_via = str(self.get_parameter('kcp_relay_via').value)
|
||||||
|
self._peer_id = str(self.get_parameter('peer_id').value)
|
||||||
|
self._expected_sender = str(self.get_parameter('expected_sender').value)
|
||||||
|
self._topic = self.get_parameter('topic').value
|
||||||
|
self._frame_id = self.get_parameter('frame_id').value
|
||||||
|
self._timeout = self.get_parameter('timeout').value
|
||||||
|
|
||||||
|
if self._transport not in ('udp', 'kcp'):
|
||||||
|
raise ValueError(f"Unsupported transport '{self._transport}', expected 'udp' or 'kcp'")
|
||||||
|
|
||||||
|
# publisher
|
||||||
|
self._pub = self.create_publisher(TwistStamped, self._topic, 10)
|
||||||
|
|
||||||
|
# watchdog timer
|
||||||
|
self._last_recv = time.monotonic()
|
||||||
|
self._lock = threading.Lock()
|
||||||
|
self._latest_cmd = (0.0, 0.0, 0.0, 0.0, 0.0, 0.0)
|
||||||
|
self._timer = self.create_timer(1.0 / 20.0, self._timer_cb)
|
||||||
|
self._sock = None
|
||||||
|
self._session = None
|
||||||
|
self._msg_type_binary = None
|
||||||
|
self._msg_type_error = None
|
||||||
|
self._closing = False
|
||||||
|
|
||||||
|
if self._transport == 'kcp':
|
||||||
|
control_defaults, self._msg_type_binary, self._msg_type_error, session_cls = _load_omnisocket()
|
||||||
|
self._session = session_cls()
|
||||||
|
self._session.connect(
|
||||||
|
server_addr=self._kcp_server,
|
||||||
|
peer_id=self._peer_id,
|
||||||
|
relay_via=self._kcp_relay_via,
|
||||||
|
**control_defaults,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
self._sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
||||||
|
self._sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
||||||
|
self._sock.bind(('0.0.0.0', self._port))
|
||||||
|
self._sock.settimeout(0.1)
|
||||||
|
|
||||||
|
# receive thread
|
||||||
|
recv_target = self._recv_loop_kcp if self._transport == 'kcp' else self._recv_loop_udp
|
||||||
|
self._recv_thread = threading.Thread(target=recv_target, daemon=True)
|
||||||
|
self._recv_thread.start()
|
||||||
|
|
||||||
|
if self._transport == 'kcp':
|
||||||
|
self.get_logger().info(
|
||||||
|
f'Bridge ready — KCP {self._kcp_server} as {self._peer_id} → {self._topic} '
|
||||||
|
f'(frame_id={self._frame_id}, timeout={self._timeout}s)'
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
self.get_logger().info(
|
||||||
|
f'Bridge ready — UDP 0.0.0.0:{self._port} → {self._topic} '
|
||||||
|
f'(frame_id={self._frame_id}, timeout={self._timeout}s)'
|
||||||
|
)
|
||||||
|
|
||||||
|
def _recv_loop_udp(self):
|
||||||
|
"""Background thread: receive UDP packets and update latest command."""
|
||||||
|
while rclpy.ok():
|
||||||
|
try:
|
||||||
|
data, addr = self._sock.recvfrom(TWIST_CMD_SIZE + 64)
|
||||||
|
except socket.timeout:
|
||||||
|
continue
|
||||||
|
except OSError:
|
||||||
|
break
|
||||||
|
|
||||||
|
if len(data) != TWIST_CMD_SIZE:
|
||||||
|
self.get_logger().warn(
|
||||||
|
f'Packet has invalid size {len(data)} bytes from {addr}, '
|
||||||
|
f'expected {TWIST_CMD_SIZE}'
|
||||||
|
)
|
||||||
|
continue
|
||||||
|
|
||||||
|
values = struct.unpack(TWIST_CMD_FMT, data)
|
||||||
|
with self._lock:
|
||||||
|
self._latest_cmd = values
|
||||||
|
self._last_recv = time.monotonic()
|
||||||
|
|
||||||
|
def _recv_loop_kcp(self):
|
||||||
|
"""Background thread: receive KCP packets and update latest command."""
|
||||||
|
while rclpy.ok():
|
||||||
|
try:
|
||||||
|
result = self._session.recv(timeout_ms=100)
|
||||||
|
except OSError as exc:
|
||||||
|
if not self._closing:
|
||||||
|
self.get_logger().error(f'KCP receive failed: {exc}')
|
||||||
|
break
|
||||||
|
|
||||||
|
if result is None:
|
||||||
|
continue
|
||||||
|
|
||||||
|
from_peer, msg_type, payload = result
|
||||||
|
|
||||||
|
if msg_type == self._msg_type_error:
|
||||||
|
self.get_logger().error(
|
||||||
|
f'KCP server error from {from_peer}: {payload.decode("utf-8", errors="replace")}'
|
||||||
|
)
|
||||||
|
continue
|
||||||
|
|
||||||
|
if self._expected_sender and from_peer != self._expected_sender:
|
||||||
|
self.get_logger().warn(
|
||||||
|
f'Ignoring KCP packet from unexpected sender {from_peer}, '
|
||||||
|
f'expected {self._expected_sender}'
|
||||||
|
)
|
||||||
|
continue
|
||||||
|
|
||||||
|
if msg_type != self._msg_type_binary:
|
||||||
|
self.get_logger().warn(
|
||||||
|
f'Ignoring non-binary KCP message type {msg_type} from {from_peer}'
|
||||||
|
)
|
||||||
|
continue
|
||||||
|
|
||||||
|
if len(payload) != TWIST_CMD_SIZE:
|
||||||
|
self.get_logger().warn(
|
||||||
|
f'KCP payload has invalid size {len(payload)} bytes from {from_peer}, '
|
||||||
|
f'expected {TWIST_CMD_SIZE}'
|
||||||
|
)
|
||||||
|
continue
|
||||||
|
|
||||||
|
values = struct.unpack(TWIST_CMD_FMT, payload)
|
||||||
|
with self._lock:
|
||||||
|
self._latest_cmd = values
|
||||||
|
self._last_recv = time.monotonic()
|
||||||
|
|
||||||
|
def _timer_cb(self):
|
||||||
|
"""20 Hz: publish TwistStamped from latest received command."""
|
||||||
|
with self._lock:
|
||||||
|
elapsed = time.monotonic() - self._last_recv
|
||||||
|
if elapsed > self._timeout:
|
||||||
|
lx, ly, lz, ax, ay, az = 0.0, 0.0, 0.0, 0.0, 0.0, 0.0
|
||||||
|
else:
|
||||||
|
lx, ly, lz, ax, ay, az = self._latest_cmd
|
||||||
|
|
||||||
|
msg = TwistStamped()
|
||||||
|
msg.header.stamp = self.get_clock().now().to_msg()
|
||||||
|
msg.header.frame_id = self._frame_id
|
||||||
|
msg.twist.linear.x = float(lx)
|
||||||
|
msg.twist.linear.y = float(ly)
|
||||||
|
msg.twist.linear.z = float(lz)
|
||||||
|
msg.twist.angular.x = float(ax)
|
||||||
|
msg.twist.angular.y = float(ay)
|
||||||
|
msg.twist.angular.z = float(az)
|
||||||
|
|
||||||
|
self._pub.publish(msg)
|
||||||
|
|
||||||
|
def destroy_node(self):
|
||||||
|
self._closing = True
|
||||||
|
if self._sock is not None:
|
||||||
|
self._sock.close()
|
||||||
|
self._sock = None
|
||||||
|
if self._session is not None:
|
||||||
|
try:
|
||||||
|
self._session.close()
|
||||||
|
except OSError as exc:
|
||||||
|
self.get_logger().warn(f'Closing KCP session failed: {exc}')
|
||||||
|
self._session = None
|
||||||
|
if hasattr(self, '_recv_thread') and self._recv_thread.is_alive():
|
||||||
|
self._recv_thread.join(timeout=0.2)
|
||||||
|
super().destroy_node()
|
||||||
|
|
||||||
|
|
||||||
|
def main(args=None):
|
||||||
|
rclpy.init(args=args)
|
||||||
|
node = None
|
||||||
|
try:
|
||||||
|
node = UdpTeleopBridge()
|
||||||
|
rclpy.spin(node)
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
pass
|
||||||
|
finally:
|
||||||
|
if node is not None:
|
||||||
|
node.destroy_node()
|
||||||
|
rclpy.shutdown()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
||||||
Reference in New Issue
Block a user