dexter-controller 0.2.0__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.0
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,6 +1,6 @@
1
1
  [project]
2
2
  name = "dexter-controller"
3
- version = "0.2.0"
3
+ version = "0.2.2"
4
4
  description = "Dexter Controller for the HARP Load Cells"
5
5
  readme = "README.md"
6
6
  authors = [
@@ -26,10 +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
+ self._recent_timestamps = deque(maxlen=NUM_READINGS * 2)
34
+ self._recent_timestamps_set = set()
35
+ self._scan_timeout = scan_timeout
33
36
 
34
37
  self._stop_event = threading.Event()
35
38
  self._ready_event = threading.Event()
@@ -59,29 +62,40 @@ class BLELoadCellDevice:
59
62
 
60
63
  async def _discover_target(self):
61
64
  target = DEVICE_NAME.lower()
62
- discovered = await BleakScanner.discover(timeout=10, return_adv=True)
63
-
64
- for _, (candidate, adv_data) in discovered.items():
65
- name = (candidate.name or adv_data.local_name or "").lower()
66
- if target in name:
67
- self.identifier = (
68
- f"BLE:{candidate.name or DEVICE_NAME} ({candidate.address})"
69
- )
70
- return candidate
71
-
72
- 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
+ )
73
71
 
74
72
  def _notification_callback(self, _, data: bytearray):
75
73
  if len(data) < PACKET_SIZE:
76
74
  return
77
75
 
78
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))
84
+
85
+ readings.sort(key=lambda x: x[0])
79
86
 
80
87
  with self._queue_lock:
81
- for reading_index in range(NUM_READINGS):
82
- base = 4 + (reading_index * BLOCK_SIZE)
83
- timestamp_us = struct.unpack_from("<Q", data, base)[0]
84
- load_cells = list(struct.unpack_from(f"<{LOAD_CELLS}H", data, base + 8))
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:
91
+ continue
92
+
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)
96
+
97
+ self._recent_timestamps.append(timestamp_us)
98
+ self._recent_timestamps_set.add(timestamp_us)
85
99
  self._queue.append(
86
100
  BLELoadCellEvent(
87
101
  payload=load_cells,
@@ -93,6 +107,7 @@ class BLELoadCellDevice:
93
107
  async def _run(self):
94
108
  print("Searching for BLE device...")
95
109
  device = await self._discover_target()
110
+ print(f"Found {self.identifier}")
96
111
  if not device:
97
112
  self._connection_error = RuntimeError("No matching BLE peripheral found")
98
113
  self._ready_event.set()
@@ -102,7 +117,7 @@ class BLELoadCellDevice:
102
117
  device,
103
118
  disconnected_callback=lambda _: self._stop_event.set(),
104
119
  )
105
-
120
+ print(f"Connecting to {self.identifier}")
106
121
  try:
107
122
  await self._client.connect()
108
123
  await self._client.start_notify(CHAR_UUID, self._notification_callback)
@@ -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}")