Metadata-Version: 2.4
Name: hex_dora_node_teleop
Version: 0.0.1a2
Summary: Dora Node for Teleoperation Input (Keyboard & Joystick)
Author-email: Dong Zhaorui <847235539@qq.com>
License-Expression: Apache-2.0
Requires-Python: >=3.8
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: dora-rs>=0.3.9
Requires-Dist: evdev>=1.7.0
Requires-Dist: pyarrow>=14.0.0
Requires-Dist: hex-util-runtime<0.1.0,>=0.0.0
Dynamic: license-file

# hex_dora_node_teleop

Dora nodes for reading teleoperation inputs from keyboard and joystick/gamepad using `evdev`. Each node maintains an internal state table updated by background threads and outputs the full state at the configured tick rate (recommended 100 Hz).

## Nodes

| Node | Description | Inputs | Outputs |
| --- | --- | --- | --- |
| `hex-dora-teleop-keyboard` | Keyboard letter-key state reader | `tick` | `state` |
| `hex-dora-teleop-joystick` | Joystick/Gamepad button & axis state reader | `tick` | `buttons`, `axes` |
| `hex-dora-teleop-test-keyboard` | Terminal viewer for keyboard state | `tick`, `state` | - |
| `hex-dora-teleop-test-joystick` | Terminal viewer for joystick state | `tick`, `buttons`, `axes` | - |

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

```bash
pip install hex_dora_node_teleop
```

## YAML Examples

### Keyboard (100 Hz)

```yaml
nodes:
  - id: teleop_keyboard
    build: pip install hex_dora_node_teleop
    path: hex-dora-teleop-keyboard
    inputs:
      tick: dora/timer/millis/10
    outputs:
      - state
    env:
      NODE_NAME: teleop_keyboard
      DEVICE_PATH: ""  # auto-detect; or set e.g. /dev/input/event0

  - id: test_keyboard
    build: pip install hex_dora_node_teleop
    path: hex-dora-teleop-test-keyboard
    inputs:
      tick: dora/timer/millis/10
      state: teleop_keyboard/state
    env:
      NODE_NAME: test_keyboard
```

### Joystick / Gamepad (100 Hz)

```yaml
nodes:
  - id: teleop_joystick
    build: pip install hex_dora_node_teleop
    path: hex-dora-teleop-joystick
    inputs:
      tick: dora/timer/millis/10
    outputs:
      - buttons
      - axes
    env:
      NODE_NAME: teleop_joystick
      DEVICE_PATH: ""  # auto-detect; or set e.g. /dev/input/event5

  - id: test_joystick
    build: pip install hex_dora_node_teleop
    path: hex-dora-teleop-test-joystick
    inputs:
      tick: dora/timer/millis/10
      buttons: teleop_joystick/buttons
      axes: teleop_joystick/axes
    env:
      NODE_NAME: test_joystick
```

## Inputs

### Teleop Nodes (`keyboard` / `joystick`)

| Input | Type | Description |
| --- | --- | --- |
| `tick` | `dora/timer/millis/*` | Timer tick to trigger state output (use `millis/10` for 100 Hz) |

### Test Nodes (`test-keyboard`)

| Input | Type | Description |
| --- | --- | --- |
| `tick` | `dora/timer/millis/*` | Timer tick to refresh terminal display |
| `state` | Arrow UInt8Array | Keyboard letter-key state from keyboard node |

### Test Nodes (`test-joystick`)

| Input | Type | Description |
| --- | --- | --- |
| `tick` | `dora/timer/millis/*` | Timer tick to refresh terminal display |
| `buttons` | Arrow UInt8Array | Button states from joystick node |
| `axes` | Arrow Float64Array | Axis values from joystick node |

## Outputs

### `state` (keyboard)

Arrow UInt8Array of length 26 — one element per letter key (a–z, in order). `1` = pressed, `0` = released.

```python
state_data: UInt8Array  # pa.array([0, 0, 1, ...], type=pa.uint8())
metadata = {
    "keys": "a,b,c,...,z",         # comma-separated key names
    "primitive": "keyboard_state",
}
```

### `buttons` (joystick)

Arrow UInt8Array — one element per detected gamepad button. `1` = pressed, `0` = released. Button list is auto-detected from device capabilities.

```python
buttons_data: UInt8Array  # pa.array([0, 1, 0, ...], type=pa.uint8())
metadata = {
    "names": "BTN_SOUTH,BTN_EAST,...",  # comma-separated button names
    "primitive": "joystick_buttons",
}
```

### `axes` (joystick)

Arrow Float64Array — one element per detected analog axis, normalized to `[-1.0, 1.0]`. Axis list is auto-detected from device capabilities.

```python
axes_data: Float64Array  # pa.array([-0.5, 0.0, ...], type=pa.float64())
metadata = {
    "names": "ABS_X,ABS_Y,...",    # comma-separated axis names
    "primitive": "joystick_axes",
}
```

## Environment Variables

### All Nodes

| Variable | Type | Default | Description |
| --- | --- | --- | --- |
| `NODE_NAME` | `str` | `""` | Dora node name passed to `Node()` constructor; empty = use default |

### Keyboard Node

| Variable | Type | Default | Description |
| --- | --- | --- | --- |
| `DEVICE_PATH` | `str` | `""` (auto) | Specific `/dev/input/eventN` path; empty = auto-detect all keyboards |

### Joystick Node

| Variable | Type | Default | Description |
| --- | --- | --- | --- |
| `DEVICE_PATH` | `str` | `""` (auto) | Specific `/dev/input/eventN` path; empty = auto-detect first joystick |

## 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. **Dora tick loop** — on each `tick` input, the node snapshots the current state table and sends it as an Arrow array output.

This decouples the variable-rate hardware events from the fixed-rate dora output, ensuring consistent 100 Hz state updates regardless of actual input event frequency.

## License

This project is licensed under Apache-2.0.
