dexter-controller 0.1.0__tar.gz

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
+ # Python-generated files
2
+ __pycache__/
3
+ *.py[oc]
4
+ build/
5
+ dist/
6
+ wheels/
7
+ *.egg-info
8
+
9
+ # Virtual environments
10
+ .venv
@@ -0,0 +1 @@
1
+ 3.11
@@ -0,0 +1,16 @@
1
+ {
2
+ // Use IntelliSense to learn about possible attributes.
3
+ // Hover to view descriptions of existing attributes.
4
+ // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
5
+ "version": "0.2.0",
6
+ "configurations": [
7
+
8
+ {
9
+ "name": "Python Debugger: Current File",
10
+ "type": "debugpy",
11
+ "request": "launch",
12
+ "program": "${file}",
13
+ "console": "integratedTerminal"
14
+ }
15
+ ]
16
+ }
@@ -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,44 @@
1
+ # dexter-controller (Python)
2
+ 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.
3
+
4
+ # Installation
5
+ You can install the package using uv:
6
+ ```bash
7
+ uv add "harp.loadcells @ git+ssh://git@github.com/fchampalimaud/pyharp.loadcells.test.git"
8
+ ```
9
+
10
+ # Usage example
11
+
12
+ ```python
13
+ import time
14
+ from dexter_controller import Finger, DexterHandController
15
+
16
+ # The mapping dictionary should contain the serial port paths as keys and a list of Finger enums as values.
17
+ mapping = {
18
+ "/dev/ttyUSB0": [Finger.THUMB, Finger.INDEX], # "COMx" on Windows
19
+ "/dev/ttyUSB1": [Finger.MIDDLE, Finger.RING], # "COMy" on Windows
20
+ "/dev/ttyUSB2": [Finger.PINKY], # "COMz on Windows
21
+ }
22
+
23
+ # When using the controller, you can access the raw data for each finger.
24
+ # Devices are connected on creation of the controller.
25
+ controller = DexterHandController(mapping)
26
+
27
+ try:
28
+ print("Press Ctrl+C to exit.")
29
+ while True:
30
+ line = (
31
+ f"Thumb: {controller.thumb.raw_data if controller.thumb else None} | "
32
+ f"Index: {controller.index.raw_data if controller.index else None} | "
33
+ f"Middle: {controller.middle.raw_data if controller.middle else None} | "
34
+ f"Ring: {controller.ring.raw_data if controller.ring else None} | "
35
+ f"Pinky: {controller.pinky.raw_data if controller.pinky else None} "
36
+ )
37
+ print(line, end="\r", flush=True)
38
+ # sleep for a short duration to avoid flooding the output (20 milliseconds)
39
+ time.sleep(0.02)
40
+ except KeyboardInterrupt:
41
+ print("Exiting...")
42
+ finally:
43
+ controller.close()
44
+ ```
@@ -0,0 +1,94 @@
1
+ import time
2
+ from dexter_controller import Finger, DexterHandController
3
+
4
+ mapping = {
5
+ "COM20": [Finger.THUMB, Finger.INDEX],
6
+ "COM5": [Finger.MIDDLE, Finger.RING],
7
+ "COM8": [Finger.PINKY],
8
+ }
9
+ controller = DexterHandController(mapping)
10
+
11
+ event_counts = {
12
+ Finger.THUMB: 0,
13
+ Finger.INDEX: 0,
14
+ Finger.MIDDLE: 0,
15
+ Finger.RING: 0,
16
+ Finger.PINKY: 0,
17
+ }
18
+
19
+
20
+ def make_counter(finger):
21
+ def on_next(data):
22
+ event_counts[finger] += 1
23
+
24
+ return on_next
25
+
26
+
27
+ # Register direct callbacks for event counting
28
+ controller.register_finger_callback(Finger.THUMB, make_counter(Finger.THUMB))
29
+ controller.register_finger_callback(Finger.INDEX, make_counter(Finger.INDEX))
30
+ controller.register_finger_callback(Finger.MIDDLE, make_counter(Finger.MIDDLE))
31
+ controller.register_finger_callback(Finger.RING, make_counter(Finger.RING))
32
+ controller.register_finger_callback(Finger.PINKY, make_counter(Finger.PINKY))
33
+
34
+ start = time.time()
35
+ try:
36
+ print("Press Ctrl+C to exit.")
37
+ while True:
38
+ time.sleep(1)
39
+ elapsed = time.time() - start
40
+ thumb_rate = event_counts[Finger.THUMB] / elapsed
41
+ index_rate = event_counts[Finger.INDEX] / elapsed
42
+ middle_rate = event_counts[Finger.MIDDLE] / elapsed
43
+ ring_rate = event_counts[Finger.RING] / elapsed
44
+ pinky_rate = event_counts[Finger.PINKY] / elapsed
45
+
46
+ thumb_val = controller.thumb.raw_data
47
+ index_val = controller.index.raw_data
48
+ middle_val = controller.middle.raw_data
49
+ ring_val = controller.ring.raw_data
50
+ pinky_val = controller.pinky.raw_data
51
+ print(
52
+ f"Thumb: {thumb_val} ({thumb_rate:.1f} Hz) | "
53
+ f"Index: {index_val} ({index_rate:.1f} Hz) | "
54
+ f"Middle: {middle_val} ({middle_rate:.1f} Hz) | "
55
+ f"Ring: {ring_val} ({ring_rate:.1f} Hz) | "
56
+ f"Pinky: {pinky_val} ({pinky_rate:.1f} Hz) ",
57
+ end="\r",
58
+ flush=True,
59
+ )
60
+ except KeyboardInterrupt:
61
+ print("\nExiting...")
62
+ finally:
63
+ controller.close()
64
+
65
+ # import time
66
+ # from dexter_controller import Finger, DexterHandController
67
+
68
+ # mapping = {"COM8": [Finger.THUMB, Finger.INDEX]}
69
+ # controller = DexterHandController(mapping)
70
+
71
+ # event_counts = {Finger.THUMB: 0, Finger.INDEX: 0}
72
+
73
+
74
+ # def make_counter(finger):
75
+ # def on_next(data):
76
+ # event_counts[finger] += 1
77
+
78
+ # return on_next
79
+
80
+
81
+ # controller.thumb_observable.subscribe(make_counter(Finger.THUMB))
82
+ # controller.index_observable.subscribe(make_counter(Finger.INDEX))
83
+
84
+ # start = time.time()
85
+ # try:
86
+ # while True:
87
+ # time.sleep(1)
88
+ # elapsed = time.time() - start
89
+ # print(
90
+ # f"Thumb: {event_counts[Finger.THUMB] / elapsed:.1f} Hz | "
91
+ # f"Index: {event_counts[Finger.INDEX] / elapsed:.1f} Hz"
92
+ # )
93
+ # except KeyboardInterrupt:
94
+ # controller.close()
@@ -0,0 +1,16 @@
1
+ [project]
2
+ name = "dexter-controller"
3
+ version = "0.1.0"
4
+ description = "Dexter Controller for the HARP Load Cells"
5
+ readme = "README.md"
6
+ authors = [
7
+ { name = "Hardware and Software Platform, Champalimaud Foundation", email = "software@research.fchampalimaud.org" }
8
+ ]
9
+ requires-python = ">=3.11"
10
+ dependencies = [
11
+ "harp-loadcells>=0.1.0a1",
12
+ ]
13
+
14
+ [build-system]
15
+ requires = ["hatchling"]
16
+ build-backend = "hatchling.build"
@@ -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,47 @@
1
+ version = 1
2
+ revision = 2
3
+ requires-python = ">=3.11"
4
+
5
+ [[package]]
6
+ name = "dexter-controller"
7
+ version = "0.1.0"
8
+ source = { editable = "." }
9
+ dependencies = [
10
+ { name = "harp-loadcells" },
11
+ ]
12
+
13
+ [package.metadata]
14
+ requires-dist = [{ name = "harp-loadcells", specifier = ">=0.1.0a1" }]
15
+
16
+ [[package]]
17
+ name = "harp-loadcells"
18
+ version = "0.1.0a1"
19
+ source = { registry = "https://pypi.org/simple" }
20
+ dependencies = [
21
+ { name = "harp-protocol" },
22
+ ]
23
+ sdist = { url = "https://files.pythonhosted.org/packages/bf/4e/5c7dfd822e8ebfe21027c6d2c0196fad2a9094c1d00672b8446824c03e00/harp_loadcells-0.1.0a1.tar.gz", hash = "sha256:1c96806dfa4dfe3f97a3f44b699e94e4198081f6916c8973c84fcef98c3503f5", size = 12635, upload-time = "2025-07-02T10:32:37.076Z" }
24
+ wheels = [
25
+ { url = "https://files.pythonhosted.org/packages/e5/d1/60ef756842c910185d18124bb3930310ca749310465e2880251470774f81/harp_loadcells-0.1.0a1-py3-none-any.whl", hash = "sha256:f4d31851457487199e1faac977274dab89ce91072c4580ea459b472ff23e57bb", size = 9497, upload-time = "2025-07-02T10:32:35.248Z" },
26
+ ]
27
+
28
+ [[package]]
29
+ name = "harp-protocol"
30
+ version = "0.2.0"
31
+ source = { registry = "https://pypi.org/simple" }
32
+ dependencies = [
33
+ { name = "pyserial" },
34
+ ]
35
+ sdist = { url = "https://files.pythonhosted.org/packages/bb/9d/cb689a64faa50b11a96eef252a22d52853d5d057a11e25d741c94aad0665/harp_protocol-0.2.0.tar.gz", hash = "sha256:37483cf538d618d3de5fcbd66673dc61916246dd078a20ce2af0c74954c5621e", size = 86316, upload-time = "2025-07-02T09:29:47.138Z" }
36
+ wheels = [
37
+ { url = "https://files.pythonhosted.org/packages/82/7f/6ee08075cf060311953067589940ad0a132336695ebb4d8337cd849d93c8/harp_protocol-0.2.0-py3-none-any.whl", hash = "sha256:d07fe0f530900fd137a8f3e8d36b4184603e34d407c3c98919d5d7b2e20cf5d6", size = 16831, upload-time = "2025-07-02T09:29:45.809Z" },
38
+ ]
39
+
40
+ [[package]]
41
+ name = "pyserial"
42
+ version = "3.5"
43
+ source = { registry = "https://pypi.org/simple" }
44
+ sdist = { url = "https://files.pythonhosted.org/packages/1e/7d/ae3f0a63f41e4d2f6cb66a5b57197850f919f59e558159a4dd3a818f5082/pyserial-3.5.tar.gz", hash = "sha256:3c77e014170dfffbd816e6ffc205e9842efb10be9f58ec16d3e8675b4925cddb", size = 159125, upload-time = "2020-11-23T03:59:15.045Z" }
45
+ wheels = [
46
+ { url = "https://files.pythonhosted.org/packages/07/bc/587a445451b253b285629263eb51c2d8e9bcea4fc97826266d186f96f558/pyserial-3.5-py2.py3-none-any.whl", hash = "sha256:c4451db6ba391ca6ca299fb3ec7bae67a5c55dde170964c7a14ceefec02f2cf0", size = 90585, upload-time = "2020-11-23T03:59:13.41Z" },
47
+ ]