dexter-controller 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- dexter_controller/__init__.py +10 -0
- dexter_controller/dexter_hand_controller.py +72 -0
- dexter_controller/finger.py +16 -0
- dexter_controller/loadcell_device.py +47 -0
- dexter_controller-0.1.0.dist-info/METADATA +53 -0
- dexter_controller-0.1.0.dist-info/RECORD +7 -0
- dexter_controller-0.1.0.dist-info/WHEEL +4 -0
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import threading
|
|
2
|
+
import time
|
|
3
|
+
from collections import defaultdict
|
|
4
|
+
|
|
5
|
+
from dexter_controller import Finger, FingerData, LoadCellDevice
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class DexterHandController:
|
|
9
|
+
def __init__(self, mapping):
|
|
10
|
+
if not mapping:
|
|
11
|
+
raise ValueError("mapping cannot be empty")
|
|
12
|
+
self._finger_to_device = {}
|
|
13
|
+
self.finger_data = defaultdict(lambda: FingerData([0, 0, 0, 0]))
|
|
14
|
+
self._devices = []
|
|
15
|
+
self._callbacks = defaultdict(list) # {Finger: [callback, ...]}
|
|
16
|
+
self._stop_event = threading.Event()
|
|
17
|
+
|
|
18
|
+
for com_port, fingers in mapping.items():
|
|
19
|
+
device = LoadCellDevice(com_port, start_thread=False)
|
|
20
|
+
self._devices.append(device)
|
|
21
|
+
for i, finger in enumerate(fingers):
|
|
22
|
+
self._finger_to_device[finger] = (device, i)
|
|
23
|
+
|
|
24
|
+
self._poll_thread = threading.Thread(target=self._poll_loop, daemon=True)
|
|
25
|
+
self._poll_thread.start()
|
|
26
|
+
|
|
27
|
+
def _poll_loop(self):
|
|
28
|
+
while not self._stop_event.is_set():
|
|
29
|
+
for device in self._devices:
|
|
30
|
+
for event in device.device.get_events():
|
|
31
|
+
data = event.payload # 8 values: 4 per finger
|
|
32
|
+
for i in (0, 1):
|
|
33
|
+
finger_data = data[i * 4 : (i + 1) * 4]
|
|
34
|
+
# Find which finger this is
|
|
35
|
+
for finger, (dev, idx) in self._finger_to_device.items():
|
|
36
|
+
if dev is device and idx == i:
|
|
37
|
+
self.finger_data[finger].raw_data = finger_data
|
|
38
|
+
for cb in self._callbacks[finger]:
|
|
39
|
+
cb(finger_data)
|
|
40
|
+
time.sleep(0.001)
|
|
41
|
+
|
|
42
|
+
def register_finger_callback(self, finger, callback):
|
|
43
|
+
"""Register a callback for a finger. Callback will be called with new data."""
|
|
44
|
+
self._callbacks[finger].append(callback)
|
|
45
|
+
|
|
46
|
+
def close(self):
|
|
47
|
+
self._stop_event.set()
|
|
48
|
+
self._poll_thread.join()
|
|
49
|
+
# Close all devices
|
|
50
|
+
for device in self._devices:
|
|
51
|
+
device.close()
|
|
52
|
+
self._callbacks.clear()
|
|
53
|
+
|
|
54
|
+
@property
|
|
55
|
+
def thumb(self):
|
|
56
|
+
return self.finger_data[Finger.THUMB]
|
|
57
|
+
|
|
58
|
+
@property
|
|
59
|
+
def index(self):
|
|
60
|
+
return self.finger_data[Finger.INDEX]
|
|
61
|
+
|
|
62
|
+
@property
|
|
63
|
+
def middle(self):
|
|
64
|
+
return self.finger_data[Finger.MIDDLE]
|
|
65
|
+
|
|
66
|
+
@property
|
|
67
|
+
def ring(self):
|
|
68
|
+
return self.finger_data[Finger.RING]
|
|
69
|
+
|
|
70
|
+
@property
|
|
71
|
+
def pinky(self):
|
|
72
|
+
return self.finger_data[Finger.PINKY]
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
from enum import Enum
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class Finger(Enum):
|
|
5
|
+
THUMB = 0
|
|
6
|
+
INDEX = 1
|
|
7
|
+
MIDDLE = 2
|
|
8
|
+
RING = 3
|
|
9
|
+
PINKY = 4
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class FingerData:
|
|
13
|
+
def __init__(self, raw_data):
|
|
14
|
+
if raw_data is None or len(raw_data) != 4:
|
|
15
|
+
raise ValueError("raw_data must have exactly 4 elements")
|
|
16
|
+
self.raw_data = raw_data
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import threading
|
|
2
|
+
from typing import Optional
|
|
3
|
+
|
|
4
|
+
from pyharp.devices.loadcells import LoadCellEvents, LoadCells
|
|
5
|
+
from pyharp.protocol import OperationMode
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class LoadCellDevice:
|
|
9
|
+
def __init__(self, com_port, start_thread=True):
|
|
10
|
+
if not com_port:
|
|
11
|
+
raise ValueError("com_port cannot be empty")
|
|
12
|
+
self.com_port = com_port
|
|
13
|
+
self.device = LoadCells(com_port)
|
|
14
|
+
self.device.set_mode(OperationMode.ACTIVE)
|
|
15
|
+
# set to only receive events from the load cells
|
|
16
|
+
self.device.write_enable_events(LoadCellEvents.LOAD_CELL_DATA)
|
|
17
|
+
self._finger_callbacks = {
|
|
18
|
+
0: [],
|
|
19
|
+
1: [],
|
|
20
|
+
} # 0: first finger, 1: second finger
|
|
21
|
+
self._stop_event = threading.Event()
|
|
22
|
+
self._thread: Optional[threading.Thread] = None
|
|
23
|
+
if start_thread:
|
|
24
|
+
self._thread = threading.Thread(target=self._event_loop)
|
|
25
|
+
self._thread.daemon = True
|
|
26
|
+
self._thread.start()
|
|
27
|
+
|
|
28
|
+
def register_callback(self, finger_index, callback):
|
|
29
|
+
if finger_index not in (0, 1):
|
|
30
|
+
raise IndexError("finger_index must be 0 or 1")
|
|
31
|
+
self._finger_callbacks[finger_index].append(callback)
|
|
32
|
+
|
|
33
|
+
def _event_loop(self):
|
|
34
|
+
with self.device as dev:
|
|
35
|
+
while not self._stop_event.is_set():
|
|
36
|
+
for event in dev.get_events():
|
|
37
|
+
data = event.payload # Should be a list of 8 values
|
|
38
|
+
# Each finger gets 4 values: [0:4] and [4:8]
|
|
39
|
+
for i in (0, 1):
|
|
40
|
+
finger_data = data[i * 4 : (i + 1) * 4]
|
|
41
|
+
for cb in self._finger_callbacks[i]:
|
|
42
|
+
cb(finger_data)
|
|
43
|
+
|
|
44
|
+
def close(self):
|
|
45
|
+
self._stop_event.set()
|
|
46
|
+
if self._thread:
|
|
47
|
+
self._thread.join()
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: dexter-controller
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Dexter Controller for the HARP Load Cells
|
|
5
|
+
Author-email: "Hardware and Software Platform, Champalimaud Foundation" <software@research.fchampalimaud.org>
|
|
6
|
+
Requires-Python: >=3.11
|
|
7
|
+
Requires-Dist: harp-loadcells>=0.1.0a1
|
|
8
|
+
Description-Content-Type: text/markdown
|
|
9
|
+
|
|
10
|
+
# dexter-controller (Python)
|
|
11
|
+
This is a Python package for controlling the Dexter device. It provides an interface to interact with it using a mapping between the device's and the connected LoadCells for each finger.
|
|
12
|
+
|
|
13
|
+
# Installation
|
|
14
|
+
You can install the package using uv:
|
|
15
|
+
```bash
|
|
16
|
+
uv add "harp.loadcells @ git+ssh://git@github.com/fchampalimaud/pyharp.loadcells.test.git"
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
# Usage example
|
|
20
|
+
|
|
21
|
+
```python
|
|
22
|
+
import time
|
|
23
|
+
from dexter_controller import Finger, DexterHandController
|
|
24
|
+
|
|
25
|
+
# The mapping dictionary should contain the serial port paths as keys and a list of Finger enums as values.
|
|
26
|
+
mapping = {
|
|
27
|
+
"/dev/ttyUSB0": [Finger.THUMB, Finger.INDEX], # "COMx" on Windows
|
|
28
|
+
"/dev/ttyUSB1": [Finger.MIDDLE, Finger.RING], # "COMy" on Windows
|
|
29
|
+
"/dev/ttyUSB2": [Finger.PINKY], # "COMz on Windows
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
# When using the controller, you can access the raw data for each finger.
|
|
33
|
+
# Devices are connected on creation of the controller.
|
|
34
|
+
controller = DexterHandController(mapping)
|
|
35
|
+
|
|
36
|
+
try:
|
|
37
|
+
print("Press Ctrl+C to exit.")
|
|
38
|
+
while True:
|
|
39
|
+
line = (
|
|
40
|
+
f"Thumb: {controller.thumb.raw_data if controller.thumb else None} | "
|
|
41
|
+
f"Index: {controller.index.raw_data if controller.index else None} | "
|
|
42
|
+
f"Middle: {controller.middle.raw_data if controller.middle else None} | "
|
|
43
|
+
f"Ring: {controller.ring.raw_data if controller.ring else None} | "
|
|
44
|
+
f"Pinky: {controller.pinky.raw_data if controller.pinky else None} "
|
|
45
|
+
)
|
|
46
|
+
print(line, end="\r", flush=True)
|
|
47
|
+
# sleep for a short duration to avoid flooding the output (20 milliseconds)
|
|
48
|
+
time.sleep(0.02)
|
|
49
|
+
except KeyboardInterrupt:
|
|
50
|
+
print("Exiting...")
|
|
51
|
+
finally:
|
|
52
|
+
controller.close()
|
|
53
|
+
```
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
dexter_controller/__init__.py,sha256=Bhn2cO7oTKBTCR4DA_dLEiOUws_qPp3jDBFRMOYBHGo,237
|
|
2
|
+
dexter_controller/dexter_hand_controller.py,sha256=DdFbunkEuZ6TKdpyuc38dcqSbzToiyx1EkFg2m24v5U,2488
|
|
3
|
+
dexter_controller/finger.py,sha256=SiS4IVW_7oW4YZ9tpL6IqDiDlt0sFBc-XTBPSNdBg2M,322
|
|
4
|
+
dexter_controller/loadcell_device.py,sha256=bIk3TxPOq9ThoCaipwEtNzpZ27rmrPDkIfPejF2xJ2E,1781
|
|
5
|
+
dexter_controller-0.1.0.dist-info/METADATA,sha256=uDwCnX-CbJYU_0HLNzx1WQoV7cR-3v4CR1v2J6efZho,2046
|
|
6
|
+
dexter_controller-0.1.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
7
|
+
dexter_controller-0.1.0.dist-info/RECORD,,
|