HEX FLOW NODE TELEOP
📖 Overview
What is hex_flow_node_teleop
hex_flow_node_teleop provides hex-flow nodes for reading teleoperation inputs from keyboard and joystick/gamepad using evdev. Each node maintains an internal state table updated by background threads and publishes the full state at the configured rate (default 100 Hz) over Zenoh via hex_flow_core, using FlatBuffer messages from hex_util_msg.
What problem it solves
- Hardware abstraction: Abstracts Linux input devices (
/dev/input/*) into structured, rate-controlled teleoperation topics over Zenoh. - Rate-controlled publishing: Decouples variable-rate hardware events from fixed-rate publish output, ensuring consistent state updates regardless of actual input event frequency.
- Flexible deployment: Supports device auto-detection, topic remapping, and environment variable configuration — no code changes needed to adapt to different hardware setups.
- Integrated testing: Ships with terminal-based test nodes for validating keyboard and joystick state without additional tools.
Target users
- Engineers integrating teleoperation input devices (keyboard, gamepad, joystick) into HEXFELLOW robot systems.
- Researchers running remote teleoperation experiments with HEXFELLOW robots.
- Developers building on the hex-flow framework who need a working teleop input layer.
Nodes
| Node | Description | Publishes | Subscribes |
|---|---|---|---|
hex-flow-teleop-keyboard |
Keyboard letter-key state reader | teleop_keyboard |
- |
hex-flow-teleop-joystick |
Joystick/Gamepad button & axis state reader | teleop_joy |
- |
hex-flow-teleop-keyboard-test |
Terminal viewer for keyboard state | - | teleop_keyboard |
hex-flow-teleop-joystick-test |
Terminal viewer for joystick state | - | teleop_joy |
Prerequisites
Accessing /dev/input/* requires the current user to be in the input group:
sudo usermod -aG input $USER
# Log out and back in for the change to take effect
📦 Installation
Requirements
- Python >= 3.10
- OS: Ubuntu (or other Linux)
- Core dependencies:
evdev>= 1.7.0hex_flow_core>= 0.0.0, < 0.1.0hex_util_msg>= 0.0.0, < 0.1.0hex_util_runtime>= 0.0.0, < 0.1.0
Install hex-flow-cli
For Ubuntu or any Debian-based system, install Zenoh and hex-flow CLI:
sudo apt update
sudo apt install -y curl gpg
curl -L https://download.eclipse.org/zenoh/debian-repo/zenoh-public-key | sudo gpg --dearmor --yes --output /etc/apt/keyrings/zenoh-public-key.gpg
echo "deb [signed-by=/etc/apt/keyrings/zenoh-public-key.gpg] https://download.eclipse.org/zenoh/debian-repo/ /" | sudo tee -a /etc/apt/sources.list > /dev/null
sudo apt update
sudo apt install zenoh curl
curl -fsSL https://raw.githubusercontent.com/hexfellow/hex-flow/main/install.sh | sh
For other systems, please install zenohd yourself, then run the install script.
Install hex-flow-node-teleop from PyPI
pip install hex_flow_node_teleop
Install hex-flow-node-teleop from source
We provide a venv.sh script to create a virtual environment with all dependencies installed. However, you need to install uv first. For uv installation, please refer to uv official installation guide.
curl -LsSf https://astral.sh/uv/install.sh | sh
Then you can use our venv.sh to create a virtual environment with all dependencies installed:
git clone https://github.com/hexfellow/hex_flow_node_teleop.git
cd hex_flow_node_teleop
./venv.sh
📑 Python Config API
The package provides helper functions that return NodeConfig objects for easy integration into your LaunchConfig.
from hex_flow_core import LaunchConfig
from hex_flow_node_teleop import (
default_teleop_keyboard_node,
default_teleop_keyboard_test_node,
default_teleop_joystick_node,
default_teleop_joystick_test_node,
)
config = LaunchConfig(
local_only=True,
enable_tui=True,
log_to_file=True,
save_path="/tmp/teleop_launch.yml",
)
nodes = {
"teleop_keyboard": default_teleop_keyboard_node(
name="teleop_keyboard",
device_path="",
rate_hz=100.0,
required=True,
remap_dict={"teleop_keyboard": "teleop_keyboard/teleop_keyboard"},
),
"test_keyboard": default_teleop_keyboard_test_node(
name="test_keyboard",
rate_hz=10.0,
required=False,
remap_dict={"teleop_keyboard": "teleop_keyboard/teleop_keyboard"},
),
"teleop_joystick": default_teleop_joystick_node(
name="teleop_joystick",
device_path="",
rate_hz=100.0,
required=True,
remap_dict={"teleop_joy": "teleop_joystick/teleop_joy"},
),
"test_joystick": default_teleop_joystick_test_node(
name="test_joystick",
rate_hz=10.0,
required=False,
remap_dict={"teleop_joy": "teleop_joystick/teleop_joy"},
),
}
config.set_nodes(nodes)
print(config.export())
default_teleop_keyboard_node
| Parameter | Type | Default | Description |
|---|---|---|---|
name |
str |
"teleop_keyboard" |
Node name and remap prefix |
device_path |
str |
"" |
/dev/input/eventN or empty for auto-detect |
rate_hz |
float |
100.0 |
Publishing rate in Hz |
required |
bool |
True |
Required for launch |
remap_dict |
dict |
None |
Custom remap; defaults to {name}/teleop_keyboard |
default_teleop_keyboard_test_node
| Parameter | Type | Default | Description |
|---|---|---|---|
name |
str |
"test_keyboard" |
Node name |
rate_hz |
float |
10.0 |
Display refresh rate in Hz |
required |
bool |
False |
Required for launch |
remap_dict |
dict |
None |
Custom remap; defaults to {teleop_name}/teleop_keyboard |
teleop_name |
str |
"teleop_keyboard" |
Keyboard teleop node to subscribe to |
default_teleop_joystick_node
| Parameter | Type | Default | Description |
|---|---|---|---|
name |
str |
"teleop_joystick" |
Node name and remap prefix |
device_path |
str |
"" |
/dev/input/eventN or empty for auto-detect |
rate_hz |
float |
100.0 |
Publishing rate in Hz |
required |
bool |
True |
Required for launch |
remap_dict |
dict |
None |
Custom remap; defaults to {name}/teleop_joy |
default_teleop_joystick_test_node
| Parameter | Type | Default | Description |
|---|---|---|---|
name |
str |
"test_joystick" |
Node name |
rate_hz |
float |
10.0 |
Display refresh rate in Hz |
required |
bool |
False |
Required for launch |
remap_dict |
dict |
None |
Custom remap; defaults to {teleop_name}/teleop_joy |
teleop_name |
str |
"teleop_joystick" |
Joystick teleop node to subscribe to |
💡 Examples
Ready-to-run config scripts are provided in the example/ directory. Each script prints a launch YAML to stdout, intended for use with hexflow run:
Keyboard
# Auto-detect device, 100 Hz publish, 10 Hz test display
hexflow run example/keyboard_test.launch.py
Joystick
# Auto-detect device, 100 Hz publish, 10 Hz test display
hexflow run example/joystick_test.launch.py
YAML Examples
Keyboard (100 Hz)
nodes:
- name: teleop_keyboard
build: pip install hex_flow_node_teleop
run: hex-flow-teleop-keyboard
required: true
hidden: true
remap:
teleop_keyboard: teleop_keyboard/teleop_keyboard
env:
DEVICE_PATH: ""
RATE_HZ: "100"
- name: test_keyboard
build: pip install hex_flow_node_teleop
run: hex-flow-teleop-keyboard-test
required: false
hidden: true
remap:
teleop_keyboard: teleop_keyboard/teleop_keyboard
env:
RATE_HZ: "10"
Joystick / Gamepad (100 Hz)
nodes:
- name: teleop_joystick
build: pip install hex_flow_node_teleop
run: hex-flow-teleop-joystick
required: true
hidden: true
remap:
teleop_joy: teleop_joystick/teleop_joy
env:
DEVICE_PATH: ""
RATE_HZ: "100"
- name: test_joystick
build: pip install hex_flow_node_teleop
run: hex-flow-teleop-joystick-test
required: false
hidden: true
remap:
teleop_joy: teleop_joystick/teleop_joy
env:
RATE_HZ: "10"
Message Types (FlatBuffer)
All topics use FlatBuffer messages from hex_util_msg.msg_teleop.
teleop_keyboard — HexTeleopKeyboard
| Field | Type | Description |
|---|---|---|
ts_ns |
int64 |
Timestamp in nanoseconds |
key_a … key_z |
bool |
Per-letter key state (true = pressed) |
Schema: msgs/msg_teleop/teleop_keyboard.fbs
teleop_joy — HexTeleopJoy
| Field | Type | Description |
|---|---|---|
ts_ns |
int64 |
Timestamp in nanoseconds |
btn_x, btn_y, btn_a, btn_b |
bool |
Face buttons |
btn_lb, btn_rb |
bool |
Shoulder bumpers |
btn_lt, btn_rt |
bool |
Trigger buttons |
btn_lthumb, btn_rthumb |
bool |
Thumbstick clicks |
axis_lx, axis_ly |
float64 |
Left stick axes (normalized to [-1, 1]) |
axis_rx, axis_ry |
float64 |
Right stick axes (normalized to [-1, 1]) |
axis_lt, axis_rt |
float64 |
Trigger analog values (normalized to [-1, 1]) |
hat_x, hat_y |
float64 |
D-pad / hat switch values |
Schema: msgs/msg_teleop/teleop_joy.fbs
Environment Variables
All Nodes
| Variable | Type | Default | Description |
|---|---|---|---|
HEX_FLOW_NODE_NAME |
str |
constructor arg | Overrides node name (handled by hex_flow_core) |
HEX_FLOW_REMAP |
str |
{} |
JSON dict for topic remapping (handled by hex_flow_core) |
RUST_LOG |
str |
info |
Log level for envlog |
Teleop Nodes (keyboard / joystick)
| Variable | Type | Default | Description |
|---|---|---|---|
DEVICE_PATH |
str |
"" (auto) |
Specific /dev/input/eventN path; empty = auto-detect |
RATE_HZ |
float |
100.0 |
Publishing rate in Hz |
Test Nodes (test-keyboard / test-joystick)
| Variable | Type | Default | Description |
|---|---|---|---|
RATE_HZ |
float |
10.0 |
Display refresh rate in Hz |
Architecture
Both keyboard and joystick nodes follow the same pattern:
- Device discovery — enumerate
/dev/input/*viaevdev.list_devices()and filter by capabilities (letter keys for keyboard, analog axes for joystick). - Background reader thread — a daemon thread continuously reads
evdevevents and updates an in-memory state table protected by athreading.Lock. - FlatBuffer serialization — on each publish cycle, the node snapshots the current state, builds a FlatBuffer message (
HexTeleopKeyboardorHexTeleopJoy) with a nanosecond timestamp, and publishes the binary payload viahex_flow_core.Node.pub(). - Rate-controlled publish loop — using
HexRatefromhex_util_runtime, the node publishes at the configured rate (default 100 Hz).
Test nodes subscribe via hex_flow_core.Node.create_sub() and poll with Node.get(topic, latest=True) at a lower rate (default 10 Hz), deserializing the FlatBuffer to refresh the terminal display.
This decouples the variable-rate hardware events from the fixed-rate publish output, ensuring consistent state updates regardless of actual input event frequency.
📄 License
Apache License 2.0. See LICENSE.
🌟 Star History
👥 Contributors
Metadata
Release files for hex-flow-node-teleop 0.0.1
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| hex_flow_node_teleop-0.0.1.tar.gz | 17.7 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| hex_flow_node_teleop-0.0.1-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 35.0 kB
Release files / hex_flow_node_teleop-0.0.1.tar.gz
| Download URL | hex_flow_node_teleop-0.0.1.tar.gz |
|---|---|
| Size | 17.7 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
948a1c3fa50f7772dd59eba6387983f25cf17f73541345460c07aef50f2475fc
|
|
BLAKE2b-256 checksum How to use checksums |
830c3c63cb492a1236f8abbd323c979840ef6fa92d5f2122c9f8b782cefbf331
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.2.0 CPython/3.10.12
|
Release files / hex_flow_node_teleop-0.0.1-py3-none-any.whl
| Download URL | hex_flow_node_teleop-0.0.1-py3-none-any.whl |
|---|---|
| Size | 17.3 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
09aa678f74e45b56d1e10d4cafe9f2515120f72404165b7cfb72efdcc385aaf3
|
|
BLAKE2b-256 checksum How to use checksums |
edbe23812795640b4cb71340126370e36cefd1646e696e8ed10ab4bb4b8e43b0
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.2.0 CPython/3.10.12
|