Metadata-Version: 2.4
Name: hex_flow_node_teleop
Version: 0.0.1a5
Summary: Hex Flow Node for Teleoperation Input (Keyboard & Joystick)
Author-email: Dong Zhaorui <847235539@qq.com>
License-Expression: Apache-2.0
Requires-Python: >=3.10
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: evdev>=1.7.0
Requires-Dist: hex_flow_core<0.1.0,>=0.0.0
Requires-Dist: hex_util_msg<0.1.0,>=0.0.0
Requires-Dist: hex_util_runtime<0.1.0,>=0.0.0
Dynamic: license-file

<h1 align="center">HEX FLOW NODE TELEOP</h1>

---

# Overview

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`.

## 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:

```bash
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.0
  - `hex_flow_core` >= 0.0.0, < 0.1.0
  - `hex_util_msg` >= 0.0.0, < 0.1.0
  - `hex_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:

```bash
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
```

## Install from PyPI

```bash
pip install hex_flow_node_teleop
```

# Python Config API

The package provides helper functions that return `NodeConfig` objects for easy integration into your `LaunchConfig`.

```python
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/`](example/) directory. Each script prints a launch YAML to stdout, intended for use with `hexflow run`:

### Keyboard

```bash
# Auto-detect device, 100 Hz publish, 10 Hz test display
hexflow run example/keyboard_test_config.py
```

### Joystick

```bash
# Auto-detect device, 100 Hz publish, 10 Hz test display
hexflow run example/joystick_test_config.py
```

# YAML Examples

### Keyboard (100 Hz)

```yaml
nodes:
  - name: teleop_keyboard
    build: pip install hex_flow_node_teleop
    run: hex-flow-teleop-keyboard
    required: 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
    remap:
      teleop_keyboard: teleop_keyboard/teleop_keyboard
    env:
      RATE_HZ: "10"
```

### Joystick / Gamepad (100 Hz)

```yaml
nodes:
  - name: teleop_joystick
    build: pip install hex_flow_node_teleop
    run: hex-flow-teleop-joystick
    required: 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
    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`](https://github.com/hexfellow/hex_util_msg/blob/main/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`](https://github.com/hexfellow/hex_util_msg/blob/main/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:

1. **Device discovery** — enumerate `/dev/input/*` via `evdev.list_devices()` and filter by capabilities (letter keys for keyboard, analog axes for joystick).
2. **Background reader thread** — a daemon thread continuously reads `evdev` events and updates an in-memory state table protected by a `threading.Lock`.
3. **FlatBuffer serialization** — on each publish cycle, the node snapshots the current state, builds a FlatBuffer message (`HexTeleopKeyboard` or `HexTeleopJoy`) with a nanosecond timestamp, and publishes the binary payload via `hex_flow_core.Node.pub()`.
4. **Rate-controlled publish loop** — using `HexRate` from `hex_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](LICENSE).
