# digity SDK

> Python SDK for the **Digity sensorized exohand** — plug in the glove, stream live sensor data directly into your Python code in three lines.

The Digity exohand is a sensorized glove that captures real-time hand motion data: joint angles for every finger, 6-axis IMU (accelerometer + gyroscope), and 6-channel capacitive touch per node. This SDK handles the USB connection, binary protocol decoding, and threading — so you get clean, typed Python objects at ~50 Hz without any low-level plumbing.

```python
from digity import GloveStream, AnglesSensor

with GloveStream() as stream:
    for frame in stream:
        for sensor in frame.sensors:
            if isinstance(sensor, AnglesSensor):
                print(sensor.finger, sensor.samples[-1].angles_deg)
```

**Use cases:** motion capture, rehabilitation, robotics control, gesture recognition, HCI research, data collection.

---

## Requirements

- Python 3.9 or later
- The Digity exohand connected via USB

---

## Installation

```bash
# SDK only
pip install digity

# SDK + real-time dashboard
pip install digity[viz]
```

---

## Real-time dashboard

`digity[viz]` includes a full web-based dashboard that visualises live hand data, records sessions, and relays data from a remote agent — all locally, no internet required.

```bash
digity-viz              # opens the dashboard as a desktop window
digity-viz --port COM3  # explicit serial port
digity-viz --browser    # open in system browser instead
```

Or launch from Python:

```python
import digity.viz
digity.viz.start()
```

The dashboard runs on `http://localhost:5001/chiro/` and opens automatically. No login needed — it is intended for single-user local use.

---

## Quick start

Plug in the glove, then run:

```python
from digity import GloveStream, AnglesSensor, ImuSensor, TouchSensor

with GloveStream() as stream:
    for frame in stream:
        print(f"side={frame.side}  node={frame.node_id}  seq={frame.seq}")

        for sensor in frame.sensors:

            if isinstance(sensor, AnglesSensor):
                angles = sensor.samples[-1].angles_deg
                print(f"  finger {sensor.finger} angles: {angles}")

            elif isinstance(sensor, ImuSensor):
                s = sensor.samples[-1]
                print(f"  finger {sensor.finger} acc={s.acc}  gyro={s.gyro}")

            elif isinstance(sensor, TouchSensor):
                print(f"  finger {sensor.finger} touch={sensor.channels}")
```

The `with` block handles connection and cleanup automatically. Press **Ctrl+C** to stop.

---

## Specifying the USB port

The SDK auto-detects the glove port on all platforms. If auto-detection fails, pass the port explicitly:

```python
GloveStream(port="/dev/ttyUSB0")   # Linux
GloveStream(port="/dev/tty.usbserial-0001")  # macOS
GloveStream(port="COM3")           # Windows
```

---

## Data types

### `GloveFrame`

Every iteration of `GloveStream` yields one `GloveFrame`:

| Field | Type | Description |
|---|---|---|
| `ts` | `float` | Host timestamp (seconds since epoch) when the frame arrived |
| `side` | `str` | `"right"` or `"left"` |
| `group` | `str` | `"hand"` or `"arm"` |
| `node_id` | `int` | PCB node number on the glove |
| `seq` | `int` | Packet counter 0–65535, wraps around |
| `sensors` | `list[Sensor]` | List of sensor readings in this frame |

### `AnglesSensor`

Joint angles for one finger node.

| Field | Type | Description |
|---|---|---|
| `finger` | `int` | Finger index (0=thumb, 1=index, …, 4=pinky) |
| `com` | `int` | Communication line index |
| `samples` | `list[AnglesSample]` | One or more timestamped angle readings |

**`AnglesSample`**

| Field | Type | Description |
|---|---|---|
| `ts_us` | `int` | Sensor timestamp in microseconds |
| `angles_deg` | `list[float]` | Joint angles in degrees (5 values for hand group) |

```python
if isinstance(sensor, AnglesSensor):
    latest = sensor.samples[-1]
    print(latest.angles_deg)   # e.g. [12.3, 45.1, 30.0, 5.5, 2.0]
```

### `ImuSensor`

6-axis IMU (accelerometer + gyroscope) for one finger node.

| Field | Type | Description |
|---|---|---|
| `finger` | `int` | Finger index |
| `com` | `int` | Communication line index |
| `samples` | `list[ImuSample]` | One or more timestamped IMU readings |

**`ImuSample`**

| Field | Type | Description |
|---|---|---|
| `ts_us` | `int` | Sensor timestamp in microseconds |
| `acc` | `tuple[int, int, int]` | Accelerometer x/y/z, raw i16 counts |
| `gyro` | `tuple[int, int, int]` | Gyroscope x/y/z, raw i16 counts |

```python
if isinstance(sensor, ImuSensor):
    s = sensor.samples[-1]
    print(s.acc)    # e.g. (312, -128, 16384)
    print(s.gyro)   # e.g. (5, -12, 3)
```

### `TouchSensor`

6-channel capacitive touch for one finger node.

| Field | Type | Description |
|---|---|---|
| `finger` | `int` | Finger index |
| `com` | `int` | Communication line index |
| `ts_us` | `int` | Sensor timestamp in microseconds |
| `channels` | `list[float]` | 6 values normalised 0.0–1.0 |
| `channels_raw` | `list[int]` | 6 raw ADC counts 0–4095 |

```python
if isinstance(sensor, TouchSensor):
    print(sensor.channels)      # [0.0, 0.82, 0.0, 0.0, 0.41, 0.0]
    print(sensor.channels_raw)  # [0, 3358, 0, 0, 1680, 0]
```

### Type annotation helper

```python
from digity import Sensor  # Union[AnglesSensor, ImuSensor, TouchSensor]

def process(sensor: Sensor) -> None:
    if isinstance(sensor, AnglesSensor):
        ...
```

---

## Common patterns

### Record data to a file

```python
import csv
from digity import GloveStream, AnglesSensor

with GloveStream() as stream, open("recording.csv", "w", newline="") as f:
    writer = csv.writer(f)
    writer.writerow(["ts", "finger", "a0", "a1", "a2", "a3", "a4"])

    for frame in stream:
        for sensor in frame.sensors:
            if isinstance(sensor, AnglesSensor):
                a = sensor.samples[-1].angles_deg
                writer.writerow([frame.ts, sensor.finger] + a)
```

### Detect a closed fist

```python
from digity import GloveStream, AnglesSensor

with GloveStream() as stream:
    finger_angles = {}

    for frame in stream:
        for sensor in frame.sensors:
            if isinstance(sensor, AnglesSensor):
                finger_angles[sensor.finger] = sensor.samples[-1].angles_deg

        if len(finger_angles) == 5:
            avg_flex = [sum(finger_angles[f][1] for f in range(1, 5)) / 4]
            if avg_flex[0] > 60:
                print("Fist detected!")
```

### Stop the stream from another thread

```python
import threading
from digity import GloveStream

stream = GloveStream()
stream.connect()

def stop_after(seconds):
    import time; time.sleep(seconds)
    stream.disconnect()

threading.Thread(target=stop_after, args=(10,), daemon=True).start()

for frame in stream:
    print(frame.seq)
```

---

## Remote mode (ZMQ)

If the glove is connected to a **remote machine** running glove-core, you can receive frames over the network:

```python
GloveStream(host="192.168.1.10")          # default port 5555
GloveStream(host="192.168.1.10", zmq_port=5556)
```

No USB connection is needed on the client machine in this mode.

---

## Error handling

```python
from digity import GloveStream, GloveNotFoundError

try:
    with GloveStream() as stream:
        for frame in stream:
            ...
except GloveNotFoundError:
    print("Glove not found — check the USB cable")
except KeyboardInterrupt:
    pass
```

`GloveNotFoundError` is raised at connection time if the glove port cannot be found.

---

## Connection speed

The glove streams at approximately **50 Hz**. Each call to `for frame in stream` blocks until the next frame arrives (up to 1 second timeout before checking for errors).

The SDK uses a background thread and an internal queue so that your processing code never blocks the serial buffer. If your code is slower than 50 Hz, frames are dropped to keep the stream live rather than accumulating memory.

---

## License

© Digity. All rights reserved.
