Metadata-Version: 2.3
Name: dexter-controller
Version: 0.3.0
Summary: Dexter device Controller
Author: Hardware and Software Platform, Champalimaud Foundation
Author-email: Hardware and Software Platform, Champalimaud Foundation <software@research.fchampalimaud.org>
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Requires-Dist: bleak==2.1.1
Requires-Dist: harp-loadcells>=0.1.0a3
Requires-Dist: pydoc-markdown>=4.8.2 ; extra == 'docs'
Requires-Dist: pyarrow>=14.0.0 ; extra == 'parquet'
Requires-Dist: dexter-controller[reader] ; extra == 'parquet'
Requires-Dist: numpy>=1.25.0 ; extra == 'reader'
Requires-Python: >=3.10
Project-URL: Repository, https://github.com/fchampalimaud/dexter-controller/
Project-URL: Bug Tracker, https://github.com/fchampalimaud/dexter-controller/issues
Provides-Extra: docs
Provides-Extra: parquet
Provides-Extra: reader
Description-Content-Type: text/markdown

# dexter-controller

[![PyPI](https://img.shields.io/pypi/v/dexter-controller)](https://pypi.org/project/dexter-controller/)
[![Python](https://img.shields.io/pypi/pyversions/dexter-controller)](https://pypi.org/project/dexter-controller/)

Python library for the **Dexter** hand device. This library handles device communication, force computation, calibration, and data recording.

## Installation

The recommended way to install the library is via `uv`. In your project add the dependency with:

```bash
uv add dexter-controller
# or use pip
pip install dexter-controller
```

Optional extras for reading and writing recordings:

```bash
uv add dexter-controller[reader]    # numpy, for loading recordings
uv add dexter-controller[parquet]   # pyarrow + numpy, for Parquet format support
# or use pip
pip install dexter-controller[reader]    # numpy, for loading recordings
pip install dexter-controller[parquet]   # pyarrow + numpy, for Parquet format support
```

## Quick start

At this moment, the recommended way to connect is over Bluetooth Low Energy. The controller auto-discovers and connects to the first Dexter device in range, but you can specify a particular device by name:

```python
import time
from dexter_controller import DexterHandController, Finger

controller = DexterHandController(use_ble=True,
    # if you want to connect to a specific device by name
    #ble_device_name="Dexter-001"
)

device_name = controller.get_device_name()
battery = controller.get_battery_level()
rate = controller.get_sampling_rate()
print(f"Device: {device_name} | Battery: {battery}% | Rate: {rate.value} Hz")

try:
    while True:
        for finger in Finger:
            data = controller.finger_data[finger]
            print(f"{finger.name}: {data.raw_data}", end="  ")
        print(end="\r", flush=True)
        time.sleep(0.02)
except KeyboardInterrupt:
    pass
finally:
    controller.close()
```

## Force computation

Convert raw load-cell readings into calibrated 2D force vectors (Fx, Fy):

```python
from dexter_controller import (
    DEFAULT_CALIBRATION_3CH,
    DexterHandController,
    Finger,
    ForceProcessor,
    get_default_orientation,
)

controller = DexterHandController(use_ble=True)

processors = {}
for finger in Finger:
    orientation = get_default_orientation(finger)
    processors[finger] = ForceProcessor(DEFAULT_CALIBRATION_3CH, orientation)

# In your read loop:
data = controller.finger_data[Finger.INDEX]
fx, fy = processors[Finger.INDEX].process_3(data)
```

Each `ForceProcessor` includes a `TareState` that lets you zero-out the baseline:

```python
processors[Finger.INDEX].tare.start_tare(sample_count=20)
```

## Recording data

Record sessions to disk in CSV, SQLite (**default**), or Parquet format:

```python
from dexter_controller import (
    DEFAULT_CALIBRATION_3CH,
    DataRecorder,
    DexterHandController,
    Finger,
    ForceProcessor,
    RecordingMetadata,
    get_default_orientation,
)
from dexter_controller.recording_format import RecordingFormat

controller = DexterHandController(use_ble=True)

processors = {}
for finger in Finger:
    processors[finger] = ForceProcessor(
        DEFAULT_CALIBRATION_3CH, get_default_orientation(finger)
    )

recorder = DataRecorder(
    "./recordings",
    file_format=RecordingFormat.SQLITE,
    metadata=RecordingMetadata(
        device_name=controller.get_device_name(),
        sampling_rate_hz=controller.get_sampling_rate().value,
    ),
)

def on_sample(_data):
    fingers = [controller.finger_data[f] for f in Finger]
    forces = [processors[f].process_3(controller.finger_data[f]) for f in Finger]
    baselines = [(processors[f].tare.baseline_x, processors[f].tare.baseline_y) for f in Finger]
    recorder.write_sample(fingers, forces, baselines)

controller.register_finger_callback(Finger.PINKY, on_sample)

recorder.start_recording()
# ... run until done ...
recorder.stop_recording()
controller.close()
```

Each recording file includes session metadata.

Session metadata has the following fields:

- `session_id`: Unique identifier for the recording session.
- `recording_started_at`: Timestamp when the recording started.
- `recording_stopped_at`: Timestamp when the recording stopped.
- `total_samples`: Total number of samples recorded.
- `library_version`: Version of the library used for recording.
- `timestamps`: Array of timestamps for each sample.
- `device_name`: Name of the device used for recording.
- `sampling_rate_hz`: Sampling rate in Hz.
- `extras`: Dictionary for experiment-specific metadata.

You can attach experiment-specific metadata via `RecordingMetadata.extras`:

```python
metadata = RecordingMetadata(
    device_name="Dexter-001",
    sampling_rate_hz=1000,
    extras={"subject_id": "S01", "trial": "3", "condition": "baseline"},
)
```

## Reading recordings

Load recorded sessions back for analysis:

```python
from dexter_controller import RecordingReader

recording = RecordingReader.read("recordings/session.db")
print(f"Samples: {recording.num_rows}")
print(f"Session: {recording.metadata.session_id}")
print(f"Forces shape: {recording.forces.shape}")    # (N, 10) - Fx,Fy per finger
print(f"Raw channels: {recording.raw_channels.shape}")  # (N, 15) - 3 channels per finger
```

For either the Sqlite and Parquet formats, you can use Pandas or Polars to load the data for analysis.

```python
import pandas as pd

# for Sqlite format, use:
conn = RecordingReader.open("recordings/session.db")
df = pd.read_sql("SELECT * FROM samples", conn)
print(df.head())

# or for Parquet format, use:
df = pd.read_parquet("recordings/session.parquet")
print(df.head())
```

You can also convert those formats to CSV for easier analysis or interoperability:

```python
RecordingReader.to_csv("recordings/session.db")  # produces session.csv
```

## BLE device configuration

When connected via BLE, you can read and write device settings:

```python
from dexter_controller import SamplingRate

controller.get_device_name()                          # read name
controller.set_device_name("MyDexter")                # rename device
controller.get_sampling_rate()                        # current rate
controller.set_sampling_rate(SamplingRate.HZ_1000)    # change rate
controller.get_battery_level()                        # battery percentage
```

> [!WARNING]
> The Visualizer application requires that the device's name must include "Dexter" within the name (e.g., "MyDexter").

Available sampling rates: 100, 200, 400, 800, 1000, 2000 Hz.

## Examples

See the [example/](example/) directory for complete, runnable scripts:

| File                                                         | Description                                                                       |
| ------------------------------------------------------------ | --------------------------------------------------------------------------------- |
| [example_ble.py](example/example_ble.py)                     | BLE connection with raw data streaming and event rate monitoring                  |
| [example_ble_force.py](example/example_ble_force.py)         | BLE connection with real-time force computation using `ForceProcessor`            |
| [example_ble_recording.py](example/example_ble_recording.py) | Full recording session: BLE connect, force processing, and data recording to file |
| [example_serial.py](example/example_serial.py)               | Serial (wired) connection with multi-port finger mapping                          |

## Serial connection (old prototype only, to be deprecated)

For wired setups using Harp load-cell boards, provide a port-to-finger mapping:

```python
from dexter_controller import DexterHandController, Finger

mapping = {
    "COM3": [Finger.THUMB, Finger.INDEX],     # /dev/ttyUSB0 on Linux
    "COM4": [Finger.MIDDLE, Finger.RING],
    "COM5": [Finger.PINKY],
}
controller = DexterHandController(mapping)
```

Each serial device supports up to two fingers (8 channels, 4 per finger).
