sr-robot3 2024.0.0rc2__py3-none-any.whl → 2025.0.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.
- sr/robot3/_version.py +14 -2
- sr/robot3/arduino.py +91 -25
- sr/robot3/astoria.py +7 -2
- sr/robot3/camera.py +92 -24
- sr/robot3/kch.py +256 -30
- sr/robot3/logging.py +1 -1
- sr/robot3/marker.py +8 -0
- sr/robot3/motor_board.py +42 -10
- sr/robot3/mqtt.py +23 -8
- sr/robot3/power_board.py +54 -13
- sr/robot3/raw_serial.py +71 -10
- sr/robot3/robot.py +100 -28
- sr/robot3/serial_wrapper.py +25 -13
- 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 +30 -4
- sr/robot3/utils.py +41 -1
- {sr_robot3-2024.0.0rc2.dist-info → sr_robot3-2025.0.0.dist-info}/METADATA +17 -15
- sr_robot3-2025.0.0.dist-info/RECORD +29 -0
- {sr_robot3-2024.0.0rc2.dist-info → sr_robot3-2025.0.0.dist-info}/WHEEL +1 -1
- sr_robot3-2024.0.0rc2.dist-info/RECORD +0 -26
- {sr_robot3-2024.0.0rc2.dist-info → sr_robot3-2025.0.0.dist-info}/LICENSE +0 -0
- {sr_robot3-2024.0.0rc2.dist-info → sr_robot3-2025.0.0.dist-info}/top_level.txt +0 -0
sr/robot3/raw_serial.py
CHANGED
@@ -2,13 +2,15 @@
|
|
2
2
|
from __future__ import annotations
|
3
3
|
|
4
4
|
import logging
|
5
|
+
from contextlib import contextmanager
|
5
6
|
from time import sleep
|
6
7
|
from types import MappingProxyType
|
7
|
-
from typing import NamedTuple, Optional
|
8
|
+
from typing import Generator, NamedTuple, Optional
|
8
9
|
|
9
|
-
from serial import Serial, serial_for_url
|
10
|
+
from serial import Serial, SerialException, serial_for_url
|
10
11
|
from serial.tools.list_ports import comports
|
11
12
|
|
13
|
+
from .exceptions import BoardDisconnectionError
|
12
14
|
from .logging import log_to_debug
|
13
15
|
from .utils import Board, BoardIdentity, get_USB_identity
|
14
16
|
|
@@ -33,7 +35,7 @@ class RawSerial(Board):
|
|
33
35
|
:param baudrate: The baudrate to use when connecting.
|
34
36
|
:param identity: The identity of the board, as reported by the USB descriptor.
|
35
37
|
"""
|
36
|
-
__slots__ = ('_serial', '_identity')
|
38
|
+
__slots__ = ('_serial', '_identity', 'delay_after_connect')
|
37
39
|
|
38
40
|
@staticmethod
|
39
41
|
def get_board_type() -> str:
|
@@ -63,9 +65,13 @@ class RawSerial(Board):
|
|
63
65
|
serial_port,
|
64
66
|
baudrate=baudrate,
|
65
67
|
timeout=0.5, # 500ms timeout
|
68
|
+
do_not_open=True,
|
66
69
|
)
|
67
70
|
|
68
|
-
|
71
|
+
# Certain boards will reset when the serial port is opened
|
72
|
+
# Wait for the board to be ready to receive data
|
73
|
+
self.delay_after_connect = 2
|
74
|
+
self._reconnect()
|
69
75
|
|
70
76
|
@classmethod
|
71
77
|
def _get_supported_boards(
|
@@ -75,7 +81,7 @@ class RawSerial(Board):
|
|
75
81
|
"""
|
76
82
|
Discover connected serial devices filtered by serial number.
|
77
83
|
|
78
|
-
:param
|
84
|
+
:param serial_devices: A list of serial ports to check (serial_number, baudrate),
|
79
85
|
these are matched by serial number.
|
80
86
|
:return: A mapping of board serial numbers to Arduinos
|
81
87
|
"""
|
@@ -123,9 +129,9 @@ class RawSerial(Board):
|
|
123
129
|
|
124
130
|
@property
|
125
131
|
@log_to_debug
|
126
|
-
def
|
132
|
+
def port(self) -> Serial:
|
127
133
|
"""
|
128
|
-
The
|
134
|
+
The pySerial Serial object.
|
129
135
|
|
130
136
|
:return: The raw serial port.
|
131
137
|
"""
|
@@ -138,7 +144,8 @@ class RawSerial(Board):
|
|
138
144
|
|
139
145
|
:param data: The bytes to send.
|
140
146
|
"""
|
141
|
-
self.
|
147
|
+
with self._handle_serial_reconnect():
|
148
|
+
self._serial.write(data)
|
142
149
|
|
143
150
|
@log_to_debug
|
144
151
|
def read(self, size: int) -> bytes:
|
@@ -148,7 +155,8 @@ class RawSerial(Board):
|
|
148
155
|
:param size: The number of bytes to read.
|
149
156
|
:return: The line read from the port.
|
150
157
|
"""
|
151
|
-
|
158
|
+
with self._handle_serial_reconnect():
|
159
|
+
return self._serial.read(size)
|
152
160
|
|
153
161
|
@log_to_debug
|
154
162
|
def read_until(self, terminator: bytes = b'\n') -> bytes:
|
@@ -158,7 +166,60 @@ class RawSerial(Board):
|
|
158
166
|
:param terminator: The terminator character to stop reading at.
|
159
167
|
:return: The data read from the port.
|
160
168
|
"""
|
161
|
-
|
169
|
+
with self._handle_serial_reconnect():
|
170
|
+
return self._serial.read_until(terminator)
|
171
|
+
|
172
|
+
@contextmanager
|
173
|
+
def _handle_serial_reconnect(self) -> Generator[None, None, None]:
|
174
|
+
"""
|
175
|
+
A context manager to handle reconnecting the serial ports on faults.
|
176
|
+
"""
|
177
|
+
if not self._serial.is_open:
|
178
|
+
if not self._reconnect():
|
179
|
+
# If the serial port cannot be opened raise an error
|
180
|
+
raise BoardDisconnectionError(
|
181
|
+
f'Connection to port {self._identity.board_type}:'
|
182
|
+
f'{self._identity.asset_tag} could not be established',
|
183
|
+
)
|
184
|
+
try:
|
185
|
+
yield
|
186
|
+
except (SerialException, OSError):
|
187
|
+
# Serial connection failed, close the port and raise an error
|
188
|
+
self._serial.close()
|
189
|
+
logger.warning(
|
190
|
+
f'Port {self._identity.board_type}:{self._identity.asset_tag} disconnected',
|
191
|
+
)
|
192
|
+
raise BoardDisconnectionError(
|
193
|
+
f'Port {self._identity.board_type}:{self._identity.asset_tag} '
|
194
|
+
'disconnected during transaction',
|
195
|
+
)
|
196
|
+
|
197
|
+
def _reconnect(self) -> bool:
|
198
|
+
"""
|
199
|
+
Connect to the class's serial port.
|
200
|
+
|
201
|
+
This is called automatically when a message is sent to the board if the port is closed.
|
202
|
+
|
203
|
+
:raises serial.SerialException: If the serial port cannot be opened.
|
204
|
+
:return: True if the serial port was opened successfully, False otherwise.
|
205
|
+
"""
|
206
|
+
try:
|
207
|
+
self._serial.open()
|
208
|
+
# Wait for the board to be ready to receive data
|
209
|
+
# Certain boards will reset when the serial port is opened
|
210
|
+
sleep(self.delay_after_connect)
|
211
|
+
self._serial.reset_input_buffer()
|
212
|
+
except SerialException:
|
213
|
+
logger.error(
|
214
|
+
'Failed to connect to port '
|
215
|
+
f'{self._identity.board_type}:{self._identity.asset_tag}',
|
216
|
+
)
|
217
|
+
return False
|
218
|
+
|
219
|
+
logger.info(
|
220
|
+
f'Connected to port {self._identity.board_type}: {self._identity.asset_tag}',
|
221
|
+
)
|
222
|
+
return True
|
162
223
|
|
163
224
|
def __repr__(self) -> str:
|
164
225
|
return f"<{self.__class__.__qualname__}: {self._serial}>"
|
sr/robot3/robot.py
CHANGED
@@ -3,8 +3,10 @@ from __future__ import annotations
|
|
3
3
|
|
4
4
|
import itertools
|
5
5
|
import logging
|
6
|
+
import os
|
6
7
|
import time
|
7
8
|
from pathlib import Path
|
9
|
+
from socket import socket
|
8
10
|
from types import MappingProxyType
|
9
11
|
from typing import Mapping, Optional
|
10
12
|
|
@@ -20,7 +22,8 @@ from .motor_board import MotorBoard
|
|
20
22
|
from .power_board import Note, PowerBoard
|
21
23
|
from .raw_serial import RawSerial
|
22
24
|
from .servo_board import ServoBoard
|
23
|
-
from .
|
25
|
+
from .simulator.time_server import TimeServer
|
26
|
+
from .utils import IN_SIMULATOR, ensure_atexit_on_term, obtain_lock, singular
|
24
27
|
|
25
28
|
logger = logging.getLogger(__name__)
|
26
29
|
|
@@ -36,13 +39,16 @@ class Robot:
|
|
36
39
|
:param wait_for_start: Wait in the constructor until the start button is pressed,
|
37
40
|
defaults to True
|
38
41
|
:param trace_logging: Enable trace level logging to the console, defaults to False
|
42
|
+
:param ignored_arduinos: A list of Arduino serial numbers to avoid connecting to
|
39
43
|
:param manual_boards: A dictionary of board types to a list of serial port paths
|
40
44
|
to allow for connecting to boards that are not automatically detected, defaults to None
|
41
45
|
:param raw_ports: A list of serial number, baudrate tuples to try connecting to.
|
46
|
+
:param no_powerboard: If True, initialize the robot without a powerboard, defaults to False
|
42
47
|
"""
|
43
48
|
__slots__ = (
|
44
49
|
'_lock', '_metadata', '_power_board', '_motor_boards', '_servo_boards',
|
45
50
|
'_arduinos', '_cameras', '_mqtt', '_astoria', '_kch', '_raw_ports',
|
51
|
+
'_time_server', '_no_powerboard',
|
46
52
|
)
|
47
53
|
|
48
54
|
def __init__(
|
@@ -54,16 +60,30 @@ class Robot:
|
|
54
60
|
ignored_arduinos: Optional[list[str]] = None,
|
55
61
|
manual_boards: Optional[dict[str, list[str]]] = None,
|
56
62
|
raw_ports: Optional[list[tuple[str, int]]] = None,
|
63
|
+
no_powerboard: bool = False,
|
57
64
|
) -> None:
|
58
|
-
self._lock
|
65
|
+
self._lock: TimeServer | socket | None
|
66
|
+
if IN_SIMULATOR:
|
67
|
+
self._lock = TimeServer.initialise()
|
68
|
+
if self._lock is None:
|
69
|
+
raise OSError(
|
70
|
+
'Unable to obtain lock, Is another robot instance already running?'
|
71
|
+
)
|
72
|
+
else:
|
73
|
+
self._lock = obtain_lock()
|
59
74
|
self._metadata: Optional[Metadata] = None
|
75
|
+
self._no_powerboard = no_powerboard
|
60
76
|
|
61
77
|
setup_logging(debug, trace_logging)
|
62
78
|
ensure_atexit_on_term()
|
63
79
|
|
64
80
|
logger.info(f"sr.robot3 version {__version__}")
|
65
81
|
|
66
|
-
|
82
|
+
if IN_SIMULATOR:
|
83
|
+
self._mqtt = None
|
84
|
+
self._astoria = None
|
85
|
+
else:
|
86
|
+
self._mqtt, self._astoria = init_astoria_mqtt()
|
67
87
|
|
68
88
|
if manual_boards:
|
69
89
|
self._init_power_board(manual_boards.get(PowerBoard.get_board_type(), []))
|
@@ -87,7 +107,10 @@ class Robot:
|
|
87
107
|
defaults to None
|
88
108
|
:raises RuntimeError: If exactly one PowerBoard is not found
|
89
109
|
"""
|
90
|
-
|
110
|
+
if self._no_powerboard:
|
111
|
+
return
|
112
|
+
|
113
|
+
power_boards = PowerBoard._get_supported_boards(manual_boards, sleep_fn=self.sleep)
|
91
114
|
self._power_board = singular(power_boards)
|
92
115
|
|
93
116
|
# Enable all the outputs, so that we can find other boards.
|
@@ -108,6 +131,8 @@ class Robot:
|
|
108
131
|
|
109
132
|
:param manual_boards: A dictionary of board types to a list of additional
|
110
133
|
serial port paths that should be checked for boards of that type, defaults to None
|
134
|
+
:param ignored_arduinos: A list of Arduino serial numbers to avoid connecting to
|
135
|
+
:param raw_ports: A list of serial number, baudrate tuples to try connecting to
|
111
136
|
"""
|
112
137
|
self._raw_ports: MappingProxyType[str, RawSerial] = MappingProxyType({})
|
113
138
|
if manual_boards is None:
|
@@ -122,7 +147,10 @@ class Robot:
|
|
122
147
|
self._servo_boards = ServoBoard._get_supported_boards(manual_servoboards)
|
123
148
|
self._arduinos = Arduino._get_supported_boards(manual_arduinos, ignored_arduinos)
|
124
149
|
if raw_ports:
|
125
|
-
|
150
|
+
if not IN_SIMULATOR:
|
151
|
+
self._raw_ports = RawSerial._get_supported_boards(raw_ports)
|
152
|
+
else:
|
153
|
+
logger.warning("Raw ports are not available in the simulator.")
|
126
154
|
|
127
155
|
def _init_camera(self) -> None:
|
128
156
|
"""
|
@@ -131,9 +159,15 @@ class Robot:
|
|
131
159
|
These cameras are used for AprilTag detection and provide location data of
|
132
160
|
markers in its field of view.
|
133
161
|
"""
|
162
|
+
if IN_SIMULATOR:
|
163
|
+
publish_fn = None
|
164
|
+
else:
|
165
|
+
assert self._mqtt is not None
|
166
|
+
publish_fn = self._mqtt.wrapped_publish
|
167
|
+
|
134
168
|
self._cameras = MappingProxyType(_setup_cameras(
|
135
169
|
game_specific.MARKER_SIZES,
|
136
|
-
|
170
|
+
publish_fn,
|
137
171
|
))
|
138
172
|
|
139
173
|
def _log_connected_boards(self) -> None:
|
@@ -142,8 +176,10 @@ class Robot:
|
|
142
176
|
|
143
177
|
Firmware versions are also logged at debug level.
|
144
178
|
"""
|
179
|
+
# we only have one power board so make it iterable
|
180
|
+
power_board = [] if self._no_powerboard else [self.power_board]
|
145
181
|
boards = itertools.chain(
|
146
|
-
|
182
|
+
power_board,
|
147
183
|
self.motor_boards.values(),
|
148
184
|
self.servo_boards.values(),
|
149
185
|
self.arduinos.values(),
|
@@ -175,6 +211,9 @@ class Robot:
|
|
175
211
|
|
176
212
|
:return: The power board object
|
177
213
|
"""
|
214
|
+
if self._no_powerboard:
|
215
|
+
raise RuntimeError("No power board was initialized")
|
216
|
+
|
178
217
|
return self._power_board
|
179
218
|
|
180
219
|
@property
|
@@ -276,27 +315,35 @@ class Robot:
|
|
276
315
|
"""
|
277
316
|
Sleep for a number of seconds.
|
278
317
|
|
279
|
-
This is a convenience method that
|
280
|
-
|
318
|
+
This is a convenience method that should be used instead of time.sleep()
|
319
|
+
to make your code compatible with the simulator.
|
281
320
|
|
282
321
|
:param secs: The number of seconds to sleep for
|
283
322
|
"""
|
284
|
-
|
323
|
+
if IN_SIMULATOR:
|
324
|
+
assert isinstance(self._lock, TimeServer)
|
325
|
+
self._lock.sleep(secs)
|
326
|
+
else:
|
327
|
+
time.sleep(secs)
|
285
328
|
|
286
329
|
@log_to_debug
|
287
330
|
def time(self) -> float:
|
288
331
|
"""
|
289
332
|
Get the number of seconds since the Unix Epoch.
|
290
333
|
|
291
|
-
This is a convenience method that
|
292
|
-
|
334
|
+
This is a convenience method that should be used instead of time.time()
|
335
|
+
to make your code compatible with the simulator.
|
293
336
|
|
294
337
|
NOTE: The robot's clock resets each time the robot is restarted, so this
|
295
338
|
will not be the correct actual time but can be used to measure elapsed time.
|
296
339
|
|
297
340
|
:returns: the number of seconds since the Unix Epoch.
|
298
341
|
"""
|
299
|
-
|
342
|
+
if IN_SIMULATOR:
|
343
|
+
assert isinstance(self._lock, TimeServer)
|
344
|
+
return self._lock.get_time()
|
345
|
+
else:
|
346
|
+
return time.time()
|
300
347
|
|
301
348
|
@property
|
302
349
|
@log_to_debug
|
@@ -346,9 +393,12 @@ class Robot:
|
|
346
393
|
The path of the USB code drive.
|
347
394
|
|
348
395
|
:returns: path to the mountpoint of the USB code drive.
|
349
|
-
:raises MetadataNotReadyError: If the start button has not been pressed yet
|
350
396
|
"""
|
351
|
-
|
397
|
+
if IN_SIMULATOR:
|
398
|
+
return Path(os.environ['SBOT_USBKEY_PATH'])
|
399
|
+
else:
|
400
|
+
assert self._astoria is not None
|
401
|
+
return self._astoria.fetch_mount_path()
|
352
402
|
|
353
403
|
@property
|
354
404
|
def is_simulated(self) -> bool:
|
@@ -357,7 +407,7 @@ class Robot:
|
|
357
407
|
|
358
408
|
:returns: True if the robot is simulated. False otherwise.
|
359
409
|
"""
|
360
|
-
return
|
410
|
+
return IN_SIMULATOR
|
361
411
|
|
362
412
|
@log_to_debug
|
363
413
|
def wait_start(self) -> None:
|
@@ -369,25 +419,47 @@ class Robot:
|
|
369
419
|
Once the start button is pressed, the metadata will be loaded and the timeout
|
370
420
|
will start if in competition mode.
|
371
421
|
"""
|
422
|
+
def null_button_pressed() -> bool:
|
423
|
+
return False
|
424
|
+
|
425
|
+
if self._no_powerboard:
|
426
|
+
# null out the start button function
|
427
|
+
start_button_pressed = null_button_pressed
|
428
|
+
else:
|
429
|
+
start_button_pressed = self.power_board._start_button
|
430
|
+
|
431
|
+
if IN_SIMULATOR:
|
432
|
+
remote_start_pressed = null_button_pressed
|
433
|
+
else:
|
434
|
+
assert self._astoria is not None
|
435
|
+
remote_start_pressed = self._astoria.get_start_button_pressed
|
436
|
+
|
372
437
|
# ignore previous button presses
|
373
|
-
_ =
|
374
|
-
_ =
|
438
|
+
_ = start_button_pressed()
|
439
|
+
_ = remote_start_pressed()
|
375
440
|
logger.info('Waiting for start button.')
|
376
441
|
|
377
|
-
self.
|
378
|
-
|
442
|
+
if not self._no_powerboard:
|
443
|
+
self.power_board.piezo.buzz(Note.A6, 0.1)
|
444
|
+
self.power_board._run_led.flash()
|
379
445
|
self.kch._flash_start()
|
380
446
|
|
381
|
-
while not (
|
382
|
-
self.
|
383
|
-
):
|
384
|
-
time.sleep(0.1)
|
447
|
+
while not start_button_pressed() or remote_start_pressed():
|
448
|
+
self.sleep(0.1)
|
385
449
|
logger.info("Start signal received; continuing.")
|
386
|
-
self.
|
450
|
+
if not self._no_powerboard:
|
451
|
+
self.power_board._run_led.on()
|
387
452
|
self.kch._start = False
|
388
453
|
|
389
|
-
|
390
|
-
|
454
|
+
if IN_SIMULATOR:
|
455
|
+
self._metadata = Metadata.parse_file(
|
456
|
+
Path(os.environ['SBOT_METADATA_PATH']) / 'astoria.json'
|
457
|
+
)
|
458
|
+
else:
|
459
|
+
assert self._astoria is not None
|
460
|
+
# Load the latest metadata that we have received over MQTT
|
461
|
+
self._metadata = self._astoria.fetch_current_metadata()
|
391
462
|
|
392
|
-
|
463
|
+
# Simulator timeout is handled by the simulator supervisor
|
464
|
+
if self._metadata.game_timeout and not IN_SIMULATOR:
|
393
465
|
timeout.kill_after_delay(self._metadata.game_timeout)
|
sr/robot3/serial_wrapper.py
CHANGED
@@ -17,7 +17,7 @@ import serial
|
|
17
17
|
|
18
18
|
from .exceptions import BoardDisconnectionError
|
19
19
|
from .logging import TRACE
|
20
|
-
from .utils import BoardIdentity
|
20
|
+
from .utils import IN_SIMULATOR, BoardIdentity
|
21
21
|
|
22
22
|
logger = logging.getLogger(__name__)
|
23
23
|
|
@@ -29,6 +29,12 @@ Param = ParamSpec("Param")
|
|
29
29
|
RetType = TypeVar("RetType")
|
30
30
|
|
31
31
|
E = TypeVar("E", bound=BaseException)
|
32
|
+
BASE_TIMEOUT: float | None
|
33
|
+
|
34
|
+
if IN_SIMULATOR:
|
35
|
+
BASE_TIMEOUT = None # Disable timeouts while in the simulator to allow for pausing
|
36
|
+
else:
|
37
|
+
BASE_TIMEOUT = 0.5
|
32
38
|
|
33
39
|
|
34
40
|
def retry(
|
@@ -41,7 +47,7 @@ def retry(
|
|
41
47
|
This outer function is used to pass arguments to the decorator.
|
42
48
|
|
43
49
|
:param times: The number of times to retry the function.
|
44
|
-
:param exceptions: The
|
50
|
+
:param exceptions: The exceptions to catch and retry on.
|
45
51
|
:return: The templated decorator function.
|
46
52
|
"""
|
47
53
|
def decorator(func: Callable[Param, RetType]) -> Callable[Param, RetType]:
|
@@ -80,7 +86,7 @@ class SerialWrapper:
|
|
80
86
|
self,
|
81
87
|
port: str,
|
82
88
|
baud: int,
|
83
|
-
timeout: float =
|
89
|
+
timeout: float | None = BASE_TIMEOUT,
|
84
90
|
identity: BoardIdentity = BoardIdentity(),
|
85
91
|
delay_after_connect: float = 0,
|
86
92
|
):
|
@@ -96,6 +102,7 @@ class SerialWrapper:
|
|
96
102
|
port,
|
97
103
|
baudrate=baud,
|
98
104
|
timeout=timeout,
|
105
|
+
write_timeout=timeout,
|
99
106
|
do_not_open=True,
|
100
107
|
)
|
101
108
|
|
@@ -124,19 +131,21 @@ class SerialWrapper:
|
|
124
131
|
up to 3 times on serial errors.
|
125
132
|
|
126
133
|
:param data: The data to write to the board.
|
134
|
+
:param endl: The endline character to add to outgoing data and remove from
|
135
|
+
received data
|
127
136
|
:raises BoardDisconnectionError: If the serial connection fails during the transaction,
|
128
137
|
including failing to respond to the command.
|
129
|
-
:return: The response from the board with the trailing
|
138
|
+
:return: The response from the board with the trailing endline character removed.
|
130
139
|
"""
|
131
140
|
with self._lock:
|
132
141
|
if not self.serial.is_open:
|
133
142
|
if not self._connect():
|
134
143
|
# If the serial port cannot be opened raise an error,
|
135
144
|
# this will be caught by the retry decorator
|
136
|
-
raise BoardDisconnectionError(
|
145
|
+
raise BoardDisconnectionError(
|
137
146
|
f'Connection to board {self.identity.board_type}:'
|
138
147
|
f'{self.identity.asset_tag} could not be established',
|
139
|
-
)
|
148
|
+
)
|
140
149
|
|
141
150
|
try:
|
142
151
|
if data is not None:
|
@@ -172,10 +181,10 @@ class SerialWrapper:
|
|
172
181
|
|
173
182
|
if response_str.startswith('NACK'):
|
174
183
|
_, error_msg = response_str.split(':', maxsplit=1)
|
175
|
-
logger.error(
|
184
|
+
logger.error(
|
176
185
|
f'Board {self.identity.board_type}:{self.identity.asset_tag} '
|
177
186
|
f'returned NACK on write command: {error_msg}'
|
178
|
-
)
|
187
|
+
)
|
179
188
|
raise RuntimeError(error_msg)
|
180
189
|
|
181
190
|
return response_str
|
@@ -185,6 +194,8 @@ class SerialWrapper:
|
|
185
194
|
Send a command to the board that does not require a response.
|
186
195
|
|
187
196
|
:param data: The data to write to the board.
|
197
|
+
:param endl: The endline character to add to outgoing data and remove from
|
198
|
+
received data
|
188
199
|
:raises RuntimeError: If the board returns a NACK response,
|
189
200
|
the firmware's error message is raised.
|
190
201
|
"""
|
@@ -202,15 +213,16 @@ class SerialWrapper:
|
|
202
213
|
"""
|
203
214
|
try:
|
204
215
|
self.serial.open()
|
205
|
-
|
206
|
-
|
207
|
-
|
216
|
+
if not IN_SIMULATOR:
|
217
|
+
# Wait for the board to be ready to receive data
|
218
|
+
# Certain boards will reset when the serial port is opened
|
219
|
+
time.sleep(self.delay_after_connect)
|
208
220
|
self.serial.reset_input_buffer()
|
209
221
|
except serial.SerialException:
|
210
|
-
logger.error(
|
222
|
+
logger.error(
|
211
223
|
'Failed to connect to board '
|
212
224
|
f'{self.identity.board_type}:{self.identity.asset_tag}'
|
213
|
-
)
|
225
|
+
)
|
214
226
|
return False
|
215
227
|
|
216
228
|
logger.info(
|
sr/robot3/servo_board.py
CHANGED
@@ -12,14 +12,15 @@ from .exceptions import IncorrectBoardError
|
|
12
12
|
from .logging import log_to_debug
|
13
13
|
from .serial_wrapper import SerialWrapper
|
14
14
|
from .utils import (
|
15
|
-
Board, BoardIdentity, float_bounds_check,
|
16
|
-
get_USB_identity, map_to_float, map_to_int,
|
15
|
+
IN_SIMULATOR, Board, BoardIdentity, float_bounds_check,
|
16
|
+
get_simulator_boards, get_USB_identity, map_to_float, map_to_int,
|
17
17
|
)
|
18
18
|
|
19
19
|
DUTY_MIN = 500
|
20
20
|
DUTY_MAX = 4000
|
21
21
|
START_DUTY_MIN = 1000
|
22
22
|
START_DUTY_MAX = 2000
|
23
|
+
NUM_SERVOS = 12
|
23
24
|
|
24
25
|
logger = logging.getLogger(__name__)
|
25
26
|
BAUDRATE = 115200 # Since the servo board is a USB device, this is ignored
|
@@ -77,7 +78,7 @@ class ServoBoard(Board):
|
|
77
78
|
self._serial = SerialWrapper(serial_port, BAUDRATE, identity=initial_identity)
|
78
79
|
|
79
80
|
self._servos = tuple(
|
80
|
-
Servo(self._serial, index) for index in range(
|
81
|
+
Servo(self._serial, index) for index in range(NUM_SERVOS)
|
81
82
|
)
|
82
83
|
|
83
84
|
self._identity = self.identify()
|
@@ -109,17 +110,46 @@ class ServoBoard(Board):
|
|
109
110
|
f"expected {err.expected_type!r}. Ignoring this device")
|
110
111
|
return None
|
111
112
|
except Exception:
|
112
|
-
if initial_identity is not None
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
113
|
+
if initial_identity is not None:
|
114
|
+
if initial_identity.board_type == 'manual':
|
115
|
+
logger.warning(
|
116
|
+
f"Manually specified servo board at port {serial_port!r}, "
|
117
|
+
"could not be identified. Ignoring this device")
|
118
|
+
elif initial_identity.manufacturer == 'sbot_simulator':
|
119
|
+
logger.warning(
|
120
|
+
f"Simulator specified servo board at port {serial_port!r}, "
|
121
|
+
"could not be identified. Ignoring this device")
|
122
|
+
return None
|
123
|
+
|
124
|
+
logger.warning(
|
125
|
+
f"Found servo board-like serial port at {serial_port!r}, "
|
126
|
+
"but it could not be identified. Ignoring this device")
|
120
127
|
return None
|
121
128
|
return board
|
122
129
|
|
130
|
+
@classmethod
|
131
|
+
def _get_simulator_boards(cls) -> MappingProxyType[str, ServoBoard]:
|
132
|
+
"""
|
133
|
+
Get the simulator boards.
|
134
|
+
|
135
|
+
:return: A mapping of board serial numbers to boards.
|
136
|
+
"""
|
137
|
+
boards = {}
|
138
|
+
# The filter here is the name of the emulated board in the simulator
|
139
|
+
for board_info in get_simulator_boards('ServoBoard'):
|
140
|
+
|
141
|
+
# Create board identity from the info given
|
142
|
+
initial_identity = BoardIdentity(
|
143
|
+
manufacturer='sbot_simulator',
|
144
|
+
board_type=board_info.type_str,
|
145
|
+
asset_tag=board_info.serial_number,
|
146
|
+
)
|
147
|
+
if (board := cls._get_valid_board(board_info.url, initial_identity)) is None:
|
148
|
+
continue
|
149
|
+
|
150
|
+
boards[board._identity.asset_tag] = board
|
151
|
+
return MappingProxyType(boards)
|
152
|
+
|
123
153
|
@classmethod
|
124
154
|
def _get_supported_boards(
|
125
155
|
cls, manual_boards: Optional[list[str]] = None,
|
@@ -133,6 +163,9 @@ class ServoBoard(Board):
|
|
133
163
|
to connect to, defaults to None
|
134
164
|
:return: A mapping of serial numbers to servo boards.
|
135
165
|
"""
|
166
|
+
if IN_SIMULATOR:
|
167
|
+
return cls._get_simulator_boards()
|
168
|
+
|
136
169
|
boards = {}
|
137
170
|
serial_ports = comports()
|
138
171
|
for port in serial_ports:
|
File without changes
|