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:
uv add dexter-controller
# or use pip
pip install dexter-controller
Optional extras for reading and writing recordings:
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:
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):
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:
processors[Finger.INDEX].tare.start_tare(sample_count=20)
Recording data
Record sessions to disk in CSV, SQLite (default), or Parquet format:
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:
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:
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.
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:
RecordingReader.to_csv("recordings/session.db") # produces session.csv
BLE device configuration
When connected via BLE, you can read and write device settings:
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
Available sampling rates: 100, 200, 400, 800, 1000, 2000 Hz.
Examples
See the example/ directory for complete, runnable scripts:
| File | Description |
|---|---|
| example_ble.py | BLE connection with raw data streaming and event rate monitoring |
| example_ble_force.py | BLE connection with real-time force computation using ForceProcessor |
| example_ble_recording.py | Full recording session: BLE connect, force processing, and data recording to file |
| 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:
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).
Metadata
Release files for dexter-controller 0.3.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| dexter_controller-0.3.0.tar.gz | 21.6 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| dexter_controller-0.3.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 50.0 kB
Release files / dexter_controller-0.3.0.tar.gz
| Download URL | dexter_controller-0.3.0.tar.gz |
|---|---|
| Size | 21.6 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
2480d75943762c0bcd76cea25c542060c14895711000cff8371215bddf4430b8
|
|
BLAKE2b-256 checksum How to use checksums |
b81bbc15ffd1f4a3a8ded1fb3cc7d606a85e89d63b9eb74b5a0ed2b8b0ce8227
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 18, 2026.
Transparency logRelease files / dexter_controller-0.3.0-py3-none-any.whl
| Download URL | dexter_controller-0.3.0-py3-none-any.whl |
|---|---|
| Size | 28.4 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
0e8856dd7999425ca4b32263245870f2fed22fa8d0083f5fbcd98f51754dac37
|
|
BLAKE2b-256 checksum How to use checksums |
097e0db9ae06381ef868944aceae468803f06f5faa021082a6b76f24f6302211
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 18, 2026.
Transparency log