dexter-controller 0.2.1__tar.gz → 0.2.2__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: dexter-controller
3
- Version: 0.2.1
3
+ Version: 0.2.2
4
4
  Summary: Dexter Controller for the HARP Load Cells
5
5
  Author: Hardware and Software Platform, Champalimaud Foundation
6
6
  Author-email: Hardware and Software Platform, Champalimaud Foundation <software@research.fchampalimaud.org>
@@ -1,21 +1,21 @@
1
- [project]
2
- name = "dexter-controller"
3
- version = "0.2.1"
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.10"
10
- dependencies = [
11
- "bleak==2.1.1",
12
- "harp-loadcells>=0.1.0a3",
13
- ]
14
-
15
- [project.urls]
16
- Repository = "https://github.com/fchampalimaud/dexter-controller/"
17
- "Bug Tracker" = "https://github.com/fchampalimaud/dexter-controller/issues"
18
-
19
- [build-system]
20
- requires = ["uv_build>=0.9.5,<0.10.0"]
21
- build-backend = "uv_build"
1
+ [project]
2
+ name = "dexter-controller"
3
+ version = "0.2.2"
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.10"
10
+ dependencies = [
11
+ "bleak==2.1.1",
12
+ "harp-loadcells>=0.1.0a3",
13
+ ]
14
+
15
+ [project.urls]
16
+ Repository = "https://github.com/fchampalimaud/dexter-controller/"
17
+ "Bug Tracker" = "https://github.com/fchampalimaud/dexter-controller/issues"
18
+
19
+ [build-system]
20
+ requires = ["uv_build>=0.9.5,<0.10.0"]
21
+ build-backend = "uv_build"
@@ -26,11 +26,13 @@ class BLELoadCellEvent:
26
26
 
27
27
 
28
28
  class BLELoadCellDevice:
29
- def __init__(self):
29
+ def __init__(self, scan_timeout=1.0):
30
30
  self.identifier = f"BLE:{DEVICE_NAME}"
31
31
  self._queue = deque()
32
32
  self._queue_lock = threading.Lock()
33
33
  self._recent_timestamps = deque(maxlen=NUM_READINGS * 2)
34
+ self._recent_timestamps_set = set()
35
+ self._scan_timeout = scan_timeout
34
36
 
35
37
  self._stop_event = threading.Event()
36
38
  self._ready_event = threading.Event()
@@ -60,35 +62,40 @@ class BLELoadCellDevice:
60
62
 
61
63
  async def _discover_target(self):
62
64
  target = DEVICE_NAME.lower()
63
- discovered = await BleakScanner.discover(timeout=10, return_adv=True)
64
-
65
- for _, (candidate, adv_data) in discovered.items():
66
- name = (candidate.name or adv_data.local_name or "").lower()
67
- if target in name:
68
- self.identifier = (
69
- f"BLE:{candidate.name or DEVICE_NAME} ({candidate.address})"
70
- )
71
- return candidate
72
-
73
- return None
65
+ return await BleakScanner.find_device_by_filter(
66
+ lambda device, adv_data: (
67
+ target in (device.name or adv_data.local_name or "").lower()
68
+ ),
69
+ timeout=self._scan_timeout,
70
+ )
74
71
 
75
72
  def _notification_callback(self, _, data: bytearray):
76
73
  if len(data) < PACKET_SIZE:
77
74
  return
78
75
 
79
76
  counter = struct.unpack_from("<i", data, 0)[0]
77
+ # make sure timestamps are monotonically increasing
78
+ readings = []
79
+ for reading_index in range(NUM_READINGS):
80
+ base = 4 + (reading_index * BLOCK_SIZE)
81
+ timestamp_us = struct.unpack_from("<Q", data, base)[0]
82
+ load_cells = list(struct.unpack_from(f"<{LOAD_CELLS}H", data, base + 8))
83
+ readings.append((timestamp_us, load_cells))
80
84
 
81
- with self._queue_lock:
82
- for reading_index in range(NUM_READINGS):
83
- base = 4 + (reading_index * BLOCK_SIZE)
84
- timestamp_us = struct.unpack_from("<Q", data, base)[0]
85
+ readings.sort(key=lambda x: x[0])
85
86
 
86
- if timestamp_us in self._recent_timestamps:
87
+ with self._queue_lock:
88
+ for timestamp_us, load_cells in readings:
89
+ # ignore if it is a repeated timestamp
90
+ if timestamp_us in self._recent_timestamps_set:
87
91
  continue
88
92
 
89
- self._recent_timestamps.append(timestamp_us)
93
+ if len(self._recent_timestamps) == self._recent_timestamps.maxlen:
94
+ expired_timestamp = self._recent_timestamps.popleft()
95
+ self._recent_timestamps_set.discard(expired_timestamp)
90
96
 
91
- load_cells = list(struct.unpack_from(f"<{LOAD_CELLS}H", data, base + 8))
97
+ self._recent_timestamps.append(timestamp_us)
98
+ self._recent_timestamps_set.add(timestamp_us)
92
99
  self._queue.append(
93
100
  BLELoadCellEvent(
94
101
  payload=load_cells,
@@ -12,12 +12,30 @@ class DexterHandController:
12
12
  self,
13
13
  mapping: dict | None = None, # {com_port: [Finger, ...]}
14
14
  use_ble=False, # use BLE instead of serial
15
+ ble_scan_timeout=1.0, # seconds to scan for BLE devices in each connection attempt
15
16
  ):
17
+ """Controller for Dexter hand load cells, supporting both serial and BLE devices.
18
+
19
+ Attributes
20
+ ----------
21
+ mapping : dict
22
+ Optional mapping of serial COM ports to lists of Fingers. Ignored if use_ble is True. Example:
23
+ mapping = {
24
+ "COM3": [Finger.THUMB, Finger.INDEX],
25
+ "COM4": [Finger.MIDDLE, Finger.RING, Finger.PINKY],
26
+ }
27
+ use_ble : bool
28
+ If True, ignore mapping and connect to the first available BLE device with the expected service/characteristic.
29
+ ble_scan_timeout : float
30
+ Seconds to scan for BLE devices in each connection attempt (only relevant if use_ble is True).
31
+ """
32
+
16
33
  if not use_ble and not mapping:
17
34
  raise ValueError("mapping cannot be empty when use_ble is False")
18
35
 
19
36
  self._mapping = dict(mapping or {})
20
37
  self._use_ble = use_ble
38
+ self._ble_scan_timeout = ble_scan_timeout
21
39
  self._lock = threading.Lock()
22
40
 
23
41
  self._finger_to_binding = {} # {Finger: (device, start, stop)}
@@ -103,7 +121,7 @@ class DexterHandController:
103
121
  self._reset_connections()
104
122
 
105
123
  try:
106
- device = BLELoadCellDevice()
124
+ device = BLELoadCellDevice(scan_timeout=self._ble_scan_timeout)
107
125
  except Exception as e:
108
126
  self.unavailable_ports.append("BLE:Dexter")
109
127
  print(f"Failed to connect to BLE device: {e}")