sr-robot3 2024.0.1__py3-none-any.whl → 2025.0.1__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.
- sr/robot3/_version.py +14 -2
- sr/robot3/arduino.py +87 -17
- sr/robot3/astoria.py +7 -2
- sr/robot3/camera.py +92 -24
- sr/robot3/kch.py +256 -30
- sr/robot3/motor_board.py +42 -10
- sr/robot3/mqtt.py +23 -8
- sr/robot3/power_board.py +53 -13
- sr/robot3/robot.py +97 -27
- sr/robot3/serial_wrapper.py +13 -5
- sr/robot3/servo_board.py +44 -11
- sr/robot3/simulator/__init__.py +0 -0
- sr/robot3/simulator/camera.py +75 -0
- sr/robot3/simulator/time_server.py +94 -0
- sr/robot3/timeout.py +29 -3
- sr/robot3/utils.py +40 -0
- {sr_robot3-2024.0.1.dist-info → sr_robot3-2025.0.1.dist-info}/METADATA +17 -15
- sr_robot3-2025.0.1.dist-info/RECORD +29 -0
- {sr_robot3-2024.0.1.dist-info → sr_robot3-2025.0.1.dist-info}/WHEEL +1 -1
- sr_robot3-2024.0.1.dist-info/RECORD +0 -26
- {sr_robot3-2024.0.1.dist-info → sr_robot3-2025.0.1.dist-info}/LICENSE +0 -0
- {sr_robot3-2024.0.1.dist-info → sr_robot3-2025.0.1.dist-info}/top_level.txt +0 -0
sr/robot3/kch.py
CHANGED
@@ -2,17 +2,28 @@
|
|
2
2
|
from __future__ import annotations
|
3
3
|
|
4
4
|
import atexit
|
5
|
+
import logging
|
5
6
|
import warnings
|
6
7
|
from enum import IntEnum, unique
|
7
8
|
from typing import Optional
|
8
9
|
|
10
|
+
from .exceptions import BoardDisconnectionError, IncorrectBoardError
|
11
|
+
from .serial_wrapper import SerialWrapper
|
12
|
+
from .utils import IN_SIMULATOR, Board, BoardIdentity, get_simulator_boards
|
13
|
+
|
9
14
|
try:
|
10
15
|
import RPi.GPIO as GPIO # isort: ignore
|
11
|
-
HAS_HAT = True
|
16
|
+
HAS_HAT = True if not IN_SIMULATOR else False
|
12
17
|
except ImportError:
|
13
18
|
HAS_HAT = False
|
14
19
|
|
15
20
|
|
21
|
+
logger = logging.getLogger(__name__)
|
22
|
+
|
23
|
+
# Only used in the simulator
|
24
|
+
BAUDRATE = 115200
|
25
|
+
|
26
|
+
|
16
27
|
@unique
|
17
28
|
class RobotLEDs(IntEnum):
|
18
29
|
"""Mapping of LEDs to GPIO Pins."""
|
@@ -71,6 +82,8 @@ class KCH:
|
|
71
82
|
__slots__ = ('_leds', '_pwm')
|
72
83
|
|
73
84
|
def __init__(self) -> None:
|
85
|
+
self._leds: tuple[LED, ...]
|
86
|
+
|
74
87
|
if HAS_HAT:
|
75
88
|
GPIO.setmode(GPIO.BCM)
|
76
89
|
with warnings.catch_warnings():
|
@@ -81,11 +94,7 @@ class KCH:
|
|
81
94
|
# suppress this as we know the reason behind the warning
|
82
95
|
GPIO.setup(RobotLEDs.all_leds(), GPIO.OUT, initial=GPIO.LOW)
|
83
96
|
self._pwm: Optional[GPIO.PWM] = None
|
84
|
-
self._leds = tuple(
|
85
|
-
LED(rgb_led) for rgb_led in RobotLEDs.user_leds()
|
86
|
-
)
|
87
97
|
|
88
|
-
if HAS_HAT:
|
89
98
|
# We are not running cleanup so the LED state persists after the code completes,
|
90
99
|
# this will cause a warning with `GPIO.setup()` which we can suppress
|
91
100
|
# atexit.register(GPIO.cleanup)
|
@@ -94,6 +103,25 @@ class KCH:
|
|
94
103
|
# Mypy isn't aware of the version of atexit.register(func, *args)
|
95
104
|
atexit.register(GPIO.cleanup, RobotLEDs.START) # type: ignore[call-arg]
|
96
105
|
|
106
|
+
self._leds = tuple(
|
107
|
+
PhysicalLED(rgb_led) for rgb_led in RobotLEDs.user_leds()
|
108
|
+
)
|
109
|
+
elif IN_SIMULATOR:
|
110
|
+
led_server = LedServer.initialise()
|
111
|
+
if led_server is not None:
|
112
|
+
self._leds = tuple(
|
113
|
+
SimulationLED(v, led_server)
|
114
|
+
for v in range(len(RobotLEDs.user_leds()))
|
115
|
+
)
|
116
|
+
else:
|
117
|
+
self._leds = tuple(
|
118
|
+
LED() for _ in RobotLEDs.user_leds()
|
119
|
+
)
|
120
|
+
else:
|
121
|
+
self._leds = tuple(
|
122
|
+
LED() for _ in RobotLEDs.user_leds()
|
123
|
+
)
|
124
|
+
|
97
125
|
@property
|
98
126
|
def _start(self) -> bool:
|
99
127
|
"""Get the state of the start LED."""
|
@@ -122,7 +150,60 @@ class KCH:
|
|
122
150
|
|
123
151
|
|
124
152
|
class LED:
|
125
|
-
"""
|
153
|
+
"""
|
154
|
+
User programmable LED.
|
155
|
+
|
156
|
+
This is a dummy class to handle the case where this is run on neither the
|
157
|
+
Raspberry Pi nor the simulator.
|
158
|
+
As such, this class does nothing.
|
159
|
+
"""
|
160
|
+
__slots__ = ('_led',)
|
161
|
+
|
162
|
+
@property
|
163
|
+
def r(self) -> bool:
|
164
|
+
"""Get the state of the Red LED segment."""
|
165
|
+
return False
|
166
|
+
|
167
|
+
@r.setter
|
168
|
+
def r(self, value: bool) -> None:
|
169
|
+
"""Set the state of the Red LED segment."""
|
170
|
+
|
171
|
+
@property
|
172
|
+
def g(self) -> bool:
|
173
|
+
"""Get the state of the Green LED segment."""
|
174
|
+
return False
|
175
|
+
|
176
|
+
@g.setter
|
177
|
+
def g(self, value: bool) -> None:
|
178
|
+
"""Set the state of the Green LED segment."""
|
179
|
+
|
180
|
+
@property
|
181
|
+
def b(self) -> bool:
|
182
|
+
"""Get the state of the Blue LED segment."""
|
183
|
+
return False
|
184
|
+
|
185
|
+
@b.setter
|
186
|
+
def b(self, value: bool) -> None:
|
187
|
+
"""Set the state of the Blue LED segment."""
|
188
|
+
|
189
|
+
@property
|
190
|
+
def colour(self) -> tuple[bool, bool, bool]:
|
191
|
+
"""Get the colour of the user LED."""
|
192
|
+
return False, False, False
|
193
|
+
|
194
|
+
@colour.setter
|
195
|
+
def colour(self, value: tuple[bool, bool, bool]) -> None:
|
196
|
+
"""Set the colour of the user LED."""
|
197
|
+
if not isinstance(value, (tuple, list)) or len(value) != 3:
|
198
|
+
raise ValueError("The LED requires 3 values for it's colour")
|
199
|
+
|
200
|
+
|
201
|
+
class PhysicalLED(LED):
|
202
|
+
"""
|
203
|
+
User programmable LED.
|
204
|
+
|
205
|
+
Used when running on the Raspberry Pi to control the actual LEDs.
|
206
|
+
"""
|
126
207
|
__slots__ = ('_led',)
|
127
208
|
|
128
209
|
def __init__(self, led: tuple[int, int, int]):
|
@@ -131,47 +212,41 @@ class LED:
|
|
131
212
|
@property
|
132
213
|
def r(self) -> bool:
|
133
214
|
"""Get the state of the Red LED segment."""
|
134
|
-
return GPIO.input(self._led[0])
|
215
|
+
return GPIO.input(self._led[0])
|
135
216
|
|
136
217
|
@r.setter
|
137
218
|
def r(self, value: bool) -> None:
|
138
219
|
"""Set the state of the Red LED segment."""
|
139
|
-
if
|
140
|
-
GPIO.output(self._led[0], GPIO.HIGH if value else GPIO.LOW)
|
220
|
+
GPIO.output(self._led[0], GPIO.HIGH if value else GPIO.LOW)
|
141
221
|
|
142
222
|
@property
|
143
223
|
def g(self) -> bool:
|
144
224
|
"""Get the state of the Green LED segment."""
|
145
|
-
return GPIO.input(self._led[1])
|
225
|
+
return GPIO.input(self._led[1])
|
146
226
|
|
147
227
|
@g.setter
|
148
228
|
def g(self, value: bool) -> None:
|
149
229
|
"""Set the state of the Green LED segment."""
|
150
|
-
if
|
151
|
-
GPIO.output(self._led[1], GPIO.HIGH if value else GPIO.LOW)
|
230
|
+
GPIO.output(self._led[1], GPIO.HIGH if value else GPIO.LOW)
|
152
231
|
|
153
232
|
@property
|
154
233
|
def b(self) -> bool:
|
155
234
|
"""Get the state of the Blue LED segment."""
|
156
|
-
return GPIO.input(self._led[2])
|
235
|
+
return GPIO.input(self._led[2])
|
157
236
|
|
158
237
|
@b.setter
|
159
238
|
def b(self, value: bool) -> None:
|
160
239
|
"""Set the state of the Blue LED segment."""
|
161
|
-
if
|
162
|
-
GPIO.output(self._led[2], GPIO.HIGH if value else GPIO.LOW)
|
240
|
+
GPIO.output(self._led[2], GPIO.HIGH if value else GPIO.LOW)
|
163
241
|
|
164
242
|
@property
|
165
243
|
def colour(self) -> tuple[bool, bool, bool]:
|
166
244
|
"""Get the colour of the user LED."""
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
)
|
173
|
-
else:
|
174
|
-
return False, False, False
|
245
|
+
return (
|
246
|
+
GPIO.input(self._led[0]),
|
247
|
+
GPIO.input(self._led[1]),
|
248
|
+
GPIO.input(self._led[2]),
|
249
|
+
)
|
175
250
|
|
176
251
|
@colour.setter
|
177
252
|
def colour(self, value: tuple[bool, bool, bool]) -> None:
|
@@ -179,10 +254,161 @@ class LED:
|
|
179
254
|
if not isinstance(value, (tuple, list)) or len(value) != 3:
|
180
255
|
raise ValueError("The LED requires 3 values for it's colour")
|
181
256
|
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
|
186
|
-
|
187
|
-
|
188
|
-
|
257
|
+
GPIO.output(
|
258
|
+
self._led,
|
259
|
+
tuple(
|
260
|
+
GPIO.HIGH if v else GPIO.LOW for v in value
|
261
|
+
),
|
262
|
+
)
|
263
|
+
|
264
|
+
|
265
|
+
class LedServer(Board):
|
266
|
+
"""
|
267
|
+
LED control over a socket.
|
268
|
+
|
269
|
+
Used when running in the simulator to control the simulated LEDs.
|
270
|
+
"""
|
271
|
+
|
272
|
+
@staticmethod
|
273
|
+
def get_board_type() -> str:
|
274
|
+
"""
|
275
|
+
Return the type of the board.
|
276
|
+
|
277
|
+
:return: The literal string 'KCHv1B'.
|
278
|
+
"""
|
279
|
+
return 'KCHv1B'
|
280
|
+
|
281
|
+
def __init__(
|
282
|
+
self,
|
283
|
+
serial_port: str,
|
284
|
+
initial_identity: BoardIdentity | None = None,
|
285
|
+
) -> None:
|
286
|
+
if initial_identity is None:
|
287
|
+
initial_identity = BoardIdentity()
|
288
|
+
self._serial = SerialWrapper(
|
289
|
+
serial_port,
|
290
|
+
BAUDRATE,
|
291
|
+
identity=initial_identity,
|
292
|
+
)
|
293
|
+
|
294
|
+
self._identity = self.identify()
|
295
|
+
if self._identity.board_type != self.get_board_type():
|
296
|
+
raise IncorrectBoardError(self._identity.board_type, self.get_board_type())
|
297
|
+
self._serial.set_identity(self._identity)
|
298
|
+
|
299
|
+
# Reset the board to a known state
|
300
|
+
self._serial.write('*RESET')
|
301
|
+
|
302
|
+
@classmethod
|
303
|
+
def initialise(cls) -> 'LedServer' | None:
|
304
|
+
"""Initialise the LED server using simulator discovery."""
|
305
|
+
# The filter here is the name of the emulated board in the simulator
|
306
|
+
boards = get_simulator_boards('LedBoard')
|
307
|
+
|
308
|
+
if not boards:
|
309
|
+
return None
|
310
|
+
|
311
|
+
board_info = boards[0]
|
312
|
+
|
313
|
+
# Create board identity from the info given
|
314
|
+
initial_identity = BoardIdentity(
|
315
|
+
manufacturer='sbot_simulator',
|
316
|
+
board_type=board_info.type_str,
|
317
|
+
asset_tag=board_info.serial_number,
|
318
|
+
)
|
319
|
+
|
320
|
+
try:
|
321
|
+
board = cls(board_info.url, initial_identity)
|
322
|
+
except BoardDisconnectionError:
|
323
|
+
logger.warning(
|
324
|
+
f"Simulator specified LED board at port {board_info.url!r}, "
|
325
|
+
"could not be identified. Ignoring this device")
|
326
|
+
return None
|
327
|
+
except IncorrectBoardError as err:
|
328
|
+
logger.warning(
|
329
|
+
f"Board returned type {err.returned_type!r}, "
|
330
|
+
f"expected {err.expected_type!r}. Ignoring this device")
|
331
|
+
return None
|
332
|
+
|
333
|
+
return board
|
334
|
+
|
335
|
+
def identify(self) -> BoardIdentity:
|
336
|
+
"""
|
337
|
+
Get the identity of the board.
|
338
|
+
|
339
|
+
:return: The identity of the board.
|
340
|
+
"""
|
341
|
+
response = self._serial.query('*IDN?')
|
342
|
+
return BoardIdentity(*response.split(':'))
|
343
|
+
|
344
|
+
def set_leds(self, led_num: int, value: tuple[bool, bool, bool]) -> None:
|
345
|
+
"""Set the colour of the LED."""
|
346
|
+
self._serial.write(f'LED:{led_num}:SET:{value[0]:d}:{value[1]:d}:{value[2]:d}')
|
347
|
+
|
348
|
+
def get_leds(self, led_num: int) -> tuple[bool, bool, bool]:
|
349
|
+
"""Get the colour of the LED."""
|
350
|
+
response = self._serial.query(f'LED:{led_num}:GET?')
|
351
|
+
red, green, blue = response.split(':')
|
352
|
+
return bool(int(red)), bool(int(green)), bool(int(blue))
|
353
|
+
|
354
|
+
|
355
|
+
class SimulationLED(LED):
|
356
|
+
"""
|
357
|
+
User programmable LED.
|
358
|
+
|
359
|
+
Used when running in the simulator to control the simulated LEDs.
|
360
|
+
"""
|
361
|
+
__slots__ = ('_led_num', '_server')
|
362
|
+
|
363
|
+
def __init__(self, led_num: int, server: LedServer) -> None:
|
364
|
+
self._led_num = led_num
|
365
|
+
self._server = server
|
366
|
+
|
367
|
+
@property
|
368
|
+
def r(self) -> bool:
|
369
|
+
"""Get the state of the Red LED segment."""
|
370
|
+
return self._server.get_leds(self._led_num)[0]
|
371
|
+
|
372
|
+
@r.setter
|
373
|
+
def r(self, value: bool) -> None:
|
374
|
+
"""Set the state of the Red LED segment."""
|
375
|
+
# Fetch the current state of the LED so we can update only the red value
|
376
|
+
current = self._server.get_leds(self._led_num)
|
377
|
+
self._server.set_leds(self._led_num, (value, current[1], current[2]))
|
378
|
+
|
379
|
+
@property
|
380
|
+
def g(self) -> bool:
|
381
|
+
"""Get the state of the Green LED segment."""
|
382
|
+
return self._server.get_leds(self._led_num)[1]
|
383
|
+
|
384
|
+
@g.setter
|
385
|
+
def g(self, value: bool) -> None:
|
386
|
+
"""Set the state of the Green LED segment."""
|
387
|
+
# Fetch the current state of the LED so we can update only the green value
|
388
|
+
current = self._server.get_leds(self._led_num)
|
389
|
+
self._server.set_leds(self._led_num, (current[0], value, current[2]))
|
390
|
+
|
391
|
+
@property
|
392
|
+
def b(self) -> bool:
|
393
|
+
"""Get the state of the Blue LED segment."""
|
394
|
+
return self._server.get_leds(self._led_num)[2]
|
395
|
+
|
396
|
+
@b.setter
|
397
|
+
def b(self, value: bool) -> None:
|
398
|
+
"""Set the state of the Blue LED segment."""
|
399
|
+
# Fetch the current state of the LED so we can update only the blue value
|
400
|
+
current = self._server.get_leds(self._led_num)
|
401
|
+
self._server.set_leds(self._led_num, (current[0], current[1], value))
|
402
|
+
|
403
|
+
@property
|
404
|
+
def colour(self) -> tuple[bool, bool, bool]:
|
405
|
+
"""Get the colour of the user LED."""
|
406
|
+
return self._server.get_leds(self._led_num)
|
407
|
+
|
408
|
+
@colour.setter
|
409
|
+
def colour(self, value: tuple[bool, bool, bool]) -> None:
|
410
|
+
"""Set the colour of the user LED."""
|
411
|
+
if not isinstance(value, (tuple, list)) or len(value) != 3:
|
412
|
+
raise ValueError("The LED requires 3 values for it's colour")
|
413
|
+
|
414
|
+
self._server.set_leds(self._led_num, value)
|
sr/robot3/motor_board.py
CHANGED
@@ -13,8 +13,8 @@ from .exceptions import IncorrectBoardError
|
|
13
13
|
from .logging import log_to_debug
|
14
14
|
from .serial_wrapper import SerialWrapper
|
15
15
|
from .utils import (
|
16
|
-
Board, BoardIdentity, float_bounds_check,
|
17
|
-
get_USB_identity, map_to_float, map_to_int,
|
16
|
+
IN_SIMULATOR, Board, BoardIdentity, float_bounds_check,
|
17
|
+
get_simulator_boards, get_USB_identity, map_to_float, map_to_int,
|
18
18
|
)
|
19
19
|
|
20
20
|
logger = logging.getLogger(__name__)
|
@@ -113,17 +113,46 @@ class MotorBoard(Board):
|
|
113
113
|
f"expected {err.expected_type!r}. Ignoring this device")
|
114
114
|
return None
|
115
115
|
except Exception:
|
116
|
-
if initial_identity is not None
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
116
|
+
if initial_identity is not None:
|
117
|
+
if initial_identity.board_type == 'manual':
|
118
|
+
logger.warning(
|
119
|
+
f"Manually specified motor board at port {serial_port!r}, "
|
120
|
+
"could not be identified. Ignoring this device")
|
121
|
+
elif initial_identity.manufacturer == 'sbot_simulator':
|
122
|
+
logger.warning(
|
123
|
+
f"Simulator specified motor board at port {serial_port!r}, "
|
124
|
+
"could not be identified. Ignoring this device")
|
125
|
+
return None
|
126
|
+
|
127
|
+
logger.warning(
|
128
|
+
f"Found motor board-like serial port at {serial_port!r}, "
|
129
|
+
"but it could not be identified. Ignoring this device")
|
124
130
|
return None
|
125
131
|
return board
|
126
132
|
|
133
|
+
@classmethod
|
134
|
+
def _get_simulator_boards(cls) -> MappingProxyType[str, MotorBoard]:
|
135
|
+
"""
|
136
|
+
Get the simulator boards.
|
137
|
+
|
138
|
+
:return: A mapping of board serial numbers to boards.
|
139
|
+
"""
|
140
|
+
boards = {}
|
141
|
+
# The filter here is the name of the emulated board in the simulator
|
142
|
+
for board_info in get_simulator_boards('MotorBoard'):
|
143
|
+
|
144
|
+
# Create board identity from the info given
|
145
|
+
initial_identity = BoardIdentity(
|
146
|
+
manufacturer='sbot_simulator',
|
147
|
+
board_type=board_info.type_str,
|
148
|
+
asset_tag=board_info.serial_number,
|
149
|
+
)
|
150
|
+
if (board := cls._get_valid_board(board_info.url, initial_identity)) is None:
|
151
|
+
continue
|
152
|
+
|
153
|
+
boards[board._identity.asset_tag] = board
|
154
|
+
return MappingProxyType(boards)
|
155
|
+
|
127
156
|
@classmethod
|
128
157
|
def _get_supported_boards(
|
129
158
|
cls, manual_boards: Optional[list[str]] = None,
|
@@ -137,6 +166,9 @@ class MotorBoard(Board):
|
|
137
166
|
to connect to, defaults to None
|
138
167
|
:return: A mapping of serial numbers to motor boards.
|
139
168
|
"""
|
169
|
+
if IN_SIMULATOR:
|
170
|
+
return cls._get_simulator_boards()
|
171
|
+
|
140
172
|
boards = {}
|
141
173
|
serial_ports = comports()
|
142
174
|
for port in serial_ports:
|
sr/robot3/mqtt.py
CHANGED
@@ -3,6 +3,7 @@ from __future__ import annotations
|
|
3
3
|
import atexit
|
4
4
|
import json
|
5
5
|
import logging
|
6
|
+
import time
|
6
7
|
from threading import Lock
|
7
8
|
from typing import Any, Callable, Optional, TypedDict, Union
|
8
9
|
from urllib.parse import urlparse
|
@@ -17,7 +18,7 @@ class MQTTClient:
|
|
17
18
|
self,
|
18
19
|
client_name: Optional[str] = None,
|
19
20
|
topic_prefix: Optional[str] = None,
|
20
|
-
mqtt_version:
|
21
|
+
mqtt_version: mqtt.MQTTProtocolVersion = mqtt.MQTTProtocolVersion.MQTTv5,
|
21
22
|
use_tls: Union[bool, str] = False,
|
22
23
|
username: str = '',
|
23
24
|
password: str = '',
|
@@ -29,7 +30,11 @@ class MQTTClient:
|
|
29
30
|
self.topic_prefix = topic_prefix
|
30
31
|
self._client_name = client_name
|
31
32
|
|
32
|
-
self._client = mqtt.Client(
|
33
|
+
self._client = mqtt.Client(
|
34
|
+
callback_api_version=mqtt.CallbackAPIVersion.VERSION2,
|
35
|
+
client_id=client_name,
|
36
|
+
protocol=mqtt_version,
|
37
|
+
)
|
33
38
|
self._client.on_connect = self._on_connect
|
34
39
|
|
35
40
|
if use_tls:
|
@@ -143,19 +148,29 @@ class MQTTClient:
|
|
143
148
|
"""Wrap a payload up to be decodable as JSON."""
|
144
149
|
if isinstance(payload, bytes):
|
145
150
|
payload = payload.decode('utf-8')
|
151
|
+
|
152
|
+
payload_dict = {
|
153
|
+
"timestamp": time.time(),
|
154
|
+
"data": payload,
|
155
|
+
}
|
156
|
+
|
146
157
|
self.publish(
|
147
158
|
topic,
|
148
|
-
json.dumps(
|
159
|
+
json.dumps(payload_dict),
|
149
160
|
retain=retain, abs_topic=abs_topic)
|
150
161
|
|
151
162
|
def _on_connect(
|
152
|
-
self,
|
153
|
-
|
163
|
+
self,
|
164
|
+
client: mqtt.Client,
|
165
|
+
userdata: Any,
|
166
|
+
connect_flags: mqtt.ConnectFlags,
|
167
|
+
reason_code: mqtt.ReasonCode,
|
168
|
+
properties: mqtt.Properties | None = None,
|
154
169
|
) -> None:
|
155
170
|
"""Callback run each time the client connects to the broker."""
|
156
|
-
if
|
171
|
+
if reason_code.is_failure:
|
157
172
|
LOGGER.warning(
|
158
|
-
f"Failed to connect to MQTT broker. Return code: {
|
173
|
+
f"Failed to connect to MQTT broker. Return code: {reason_code.getName()}" # type: ignore[no-untyped-call] # noqa: E501
|
159
174
|
)
|
160
175
|
return
|
161
176
|
|
@@ -182,7 +197,7 @@ def unpack_mqtt_url(url: str) -> MQTTVariables:
|
|
182
197
|
The URL should be in the format:
|
183
198
|
mqtt[s]://[<username>[:<password>]]@<host>[:<port>]/<topic_root>
|
184
199
|
"""
|
185
|
-
url_parts = urlparse(url)
|
200
|
+
url_parts = urlparse(url, allow_fragments=False)
|
186
201
|
|
187
202
|
if url_parts.scheme not in ('mqtt', 'mqtts'):
|
188
203
|
raise ValueError(f"Invalid scheme: {url_parts.scheme}")
|
sr/robot3/power_board.py
CHANGED
@@ -6,17 +6,21 @@ import logging
|
|
6
6
|
from enum import IntEnum
|
7
7
|
from time import sleep
|
8
8
|
from types import MappingProxyType
|
9
|
-
from typing import NamedTuple, Optional
|
9
|
+
from typing import Callable, NamedTuple, Optional
|
10
10
|
|
11
11
|
from serial.tools.list_ports import comports
|
12
12
|
|
13
13
|
from .exceptions import IncorrectBoardError
|
14
14
|
from .logging import log_to_debug
|
15
15
|
from .serial_wrapper import SerialWrapper
|
16
|
-
from .utils import
|
16
|
+
from .utils import (
|
17
|
+
IN_SIMULATOR, Board, BoardIdentity, float_bounds_check,
|
18
|
+
get_simulator_boards, get_USB_identity,
|
19
|
+
)
|
17
20
|
|
18
21
|
logger = logging.getLogger(__name__)
|
19
22
|
BAUDRATE = 115200 # Since the power board is a USB device, this is ignored
|
23
|
+
_SLEEP_FN = sleep # For use in the simulator
|
20
24
|
|
21
25
|
|
22
26
|
class PowerOutputPosition(IntEnum):
|
@@ -132,20 +136,50 @@ class PowerBoard(Board):
|
|
132
136
|
f"expected {err.expected_type!r}. Ignoring this device")
|
133
137
|
return None
|
134
138
|
except Exception:
|
135
|
-
if initial_identity is not None
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
139
|
+
if initial_identity is not None:
|
140
|
+
if initial_identity.board_type == 'manual':
|
141
|
+
logger.warning(
|
142
|
+
f"Manually specified power board at port {serial_port!r}, "
|
143
|
+
"could not be identified. Ignoring this device")
|
144
|
+
elif initial_identity.manufacturer == 'sbot_simulator':
|
145
|
+
logger.warning(
|
146
|
+
f"Simulator specified power board at port {serial_port!r}, "
|
147
|
+
"could not be identified. Ignoring this device")
|
148
|
+
|
149
|
+
logger.warning(
|
150
|
+
f"Found power board-like serial port at {serial_port!r}, "
|
151
|
+
"but it could not be identified. Ignoring this device")
|
143
152
|
return None
|
144
153
|
return board
|
145
154
|
|
155
|
+
@classmethod
|
156
|
+
def _get_simulator_boards(cls) -> MappingProxyType[str, PowerBoard]:
|
157
|
+
"""
|
158
|
+
Get the simulator boards.
|
159
|
+
|
160
|
+
:return: A mapping of board serial numbers to boards.
|
161
|
+
"""
|
162
|
+
boards = {}
|
163
|
+
# The filter here is the name of the emulated board in the simulator
|
164
|
+
for board_info in get_simulator_boards('PowerBoard'):
|
165
|
+
|
166
|
+
# Create board identity from the info given
|
167
|
+
initial_identity = BoardIdentity(
|
168
|
+
manufacturer='sbot_simulator',
|
169
|
+
board_type=board_info.type_str,
|
170
|
+
asset_tag=board_info.serial_number,
|
171
|
+
)
|
172
|
+
if (board := cls._get_valid_board(board_info.url, initial_identity)) is None:
|
173
|
+
continue
|
174
|
+
|
175
|
+
boards[board._identity.asset_tag] = board
|
176
|
+
return MappingProxyType(boards)
|
177
|
+
|
146
178
|
@classmethod
|
147
179
|
def _get_supported_boards(
|
148
|
-
cls,
|
180
|
+
cls,
|
181
|
+
manual_boards: Optional[list[str]] = None,
|
182
|
+
sleep_fn: Optional[Callable[[float], None]] = None,
|
149
183
|
) -> MappingProxyType[str, PowerBoard]:
|
150
184
|
"""
|
151
185
|
Find all connected power boards.
|
@@ -156,6 +190,12 @@ class PowerBoard(Board):
|
|
156
190
|
to connect to, defaults to None
|
157
191
|
:return: A mapping of serial numbers to power boards.
|
158
192
|
"""
|
193
|
+
if sleep_fn is not None:
|
194
|
+
global _SLEEP_FN
|
195
|
+
_SLEEP_FN = sleep_fn
|
196
|
+
if IN_SIMULATOR:
|
197
|
+
return cls._get_simulator_boards()
|
198
|
+
|
159
199
|
boards = {}
|
160
200
|
serial_ports = comports()
|
161
201
|
for port in serial_ports:
|
@@ -501,13 +541,13 @@ class Piezo:
|
|
501
541
|
frequency, 8, 10_000, "Frequency must be between 8 and 10000Hz"))
|
502
542
|
duration_ms = int(float_bounds_check(
|
503
543
|
duration * 1000, 0, 2**31 - 1,
|
504
|
-
f"Duration is a float in seconds between 0 and {(2**31-1)/1000:,.0f}"))
|
544
|
+
f"Duration is a float in seconds between 0 and {(2**31 - 1) / 1000:,.0f}"))
|
505
545
|
|
506
546
|
cmd = f'NOTE:{frequency_int}:{duration_ms}'
|
507
547
|
self._serial.write(cmd)
|
508
548
|
|
509
549
|
if blocking:
|
510
|
-
|
550
|
+
_SLEEP_FN(duration)
|
511
551
|
|
512
552
|
def __repr__(self) -> str:
|
513
553
|
return f"<{self.__class__.__qualname__}: {self._serial}>"
|