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.
@@ -0,0 +1,10 @@
1
+ from .finger import Finger, FingerData
2
+ from .loadcell_device import LoadCellDevice
3
+ from .dexter_hand_controller import DexterHandController
4
+
5
+ __all__ = [
6
+ "Finger",
7
+ "FingerData",
8
+ "LoadCellDevice",
9
+ "DexterHandController",
10
+ ]
@@ -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,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.27.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any