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.
@@ -0,0 +1,75 @@
1
+ import struct
2
+
3
+ import cv2
4
+ import numpy as np
5
+ from april_vision import FrameSource
6
+ from numpy.typing import NDArray
7
+ from serial import serial_for_url
8
+
9
+ from ..utils import BoardInfo
10
+
11
+ HEADER_SIZE = 5 # 1 byte for the type, 4 bytes for the length
12
+ IMAGE_TAG_ID = 0
13
+
14
+
15
+ class WebotsRemoteCameraSource(FrameSource):
16
+ # Webots cameras include an alpha channel, this informs april_vision of how to handle it
17
+ COLOURSPACE = cv2.COLOR_BGRA2GRAY
18
+
19
+ def __init__(self, camera_info: BoardInfo) -> None:
20
+ self.calibration = (0.0, 0.0, 0.0, 0.0)
21
+ # Use pyserial to give a nicer interface for connecting to the camera socket
22
+ self._serial = serial_for_url(camera_info.url, baudrate=115200, timeout=None)
23
+
24
+ # Check the camera is connected
25
+ response = self._make_request("*IDN?")
26
+ if not response.split(b":")[1].lower().startswith(b"cam"):
27
+ raise RuntimeError(f"Camera not connected to a camera, returned: {response!r}")
28
+
29
+ # Get the calibration data for this camera
30
+ response = self._make_request("CAM:CALIBRATION?")
31
+
32
+ # The calibration data is returned as a string of floats separated by colons
33
+ new_calibration = tuple(map(float, response.split(b":")))
34
+ assert len(new_calibration) == 4, f"Invalid calibration data: {new_calibration}"
35
+ self.calibration = new_calibration
36
+
37
+ # Get the image size for this camera
38
+ response = self._make_request("CAM:RESOLUTION?")
39
+ self.image_size = tuple(map(int, response.split(b":")))
40
+ assert len(self.image_size) == 2, f"Invalid image dimensions: {self.image_size}"
41
+
42
+ def read(self, fresh: bool = True) -> NDArray:
43
+ """
44
+ The method for getting a new frame.
45
+
46
+ :param fresh: Whether to flush the device's buffer before capturing
47
+ the frame, unused.
48
+ """
49
+ self._serial.write(b"CAM:FRAME!\n")
50
+ # The image is encoded as a TLV (Type, Length, Value) packet
51
+ # so we need to read the header to get the type and length of the image
52
+ header = self._serial.read(HEADER_SIZE)
53
+ assert len(header) == HEADER_SIZE, f"Invalid header length: {len(header)}"
54
+ img_tag, img_len = struct.unpack('>BI', header)
55
+ assert img_tag == IMAGE_TAG_ID, f"Invalid image tag: {img_tag}"
56
+
57
+ # Get the image data now we know the length
58
+ img_data = self._serial.read(img_len)
59
+ assert len(img_data) == img_len, f"Invalid image data length: {len(img_data)}"
60
+
61
+ rgb_frame_raw: NDArray[np.uint8] = np.frombuffer(img_data, np.uint8)
62
+
63
+ # Height is first, then width, then channels
64
+ return rgb_frame_raw.reshape((self.image_size[1], self.image_size[0], 4))
65
+
66
+ def close(self) -> None:
67
+ """Close the underlying socket on exit."""
68
+ self._serial.close()
69
+
70
+ def _make_request(self, command: str) -> bytes:
71
+ self._serial.write(command.encode() + b"\n")
72
+ response = self._serial.readline()
73
+ if not response.endswith(b"\n") or response.startswith(b"NACK:"):
74
+ raise RuntimeError(f"Failed to communicate with camera, returned: {response!r}")
75
+ return response
@@ -0,0 +1,94 @@
1
+ from __future__ import annotations
2
+
3
+ import logging
4
+ from datetime import datetime
5
+
6
+ from ..exceptions import BoardDisconnectionError, IncorrectBoardError
7
+ from ..serial_wrapper import SerialWrapper
8
+ from ..utils import BoardIdentity, get_simulator_boards
9
+
10
+ logger = logging.getLogger(__name__)
11
+
12
+ BAUDRATE = 115200
13
+
14
+
15
+ class TimeServer:
16
+ @staticmethod
17
+ def get_board_type() -> str:
18
+ """
19
+ Return the type of the board.
20
+
21
+ :return: The literal string 'TimeServer'.
22
+ """
23
+ return 'TimeServer'
24
+
25
+ def __init__(
26
+ self,
27
+ serial_port: str,
28
+ initial_identity: BoardIdentity | None = None,
29
+ ) -> None:
30
+ if initial_identity is None:
31
+ initial_identity = BoardIdentity()
32
+ self._serial = SerialWrapper(
33
+ serial_port,
34
+ BAUDRATE,
35
+ identity=initial_identity,
36
+ # Disable the timeout so sleep works properly
37
+ timeout=None,
38
+ )
39
+
40
+ self._identity = self.identify()
41
+ if self._identity.board_type != self.get_board_type():
42
+ raise IncorrectBoardError(self._identity.board_type, self.get_board_type())
43
+ self._serial.set_identity(self._identity)
44
+
45
+ @classmethod
46
+ def initialise(cls) -> 'TimeServer' | None:
47
+ # The filter here is the name of the emulated board in the simulator
48
+ boards = get_simulator_boards('TimeServer')
49
+
50
+ if not boards:
51
+ return None
52
+
53
+ board_info = boards[0]
54
+
55
+ # Create board identity from the info given
56
+ initial_identity = BoardIdentity(
57
+ manufacturer='sbot_simulator',
58
+ board_type=board_info.type_str,
59
+ asset_tag=board_info.serial_number,
60
+ )
61
+
62
+ try:
63
+ board = cls(board_info.url, initial_identity)
64
+ except BoardDisconnectionError:
65
+ logger.warning(
66
+ f"Simulator specified time server at port {board_info.url!r}, "
67
+ "could not be identified. Ignoring this device")
68
+ return None
69
+ except IncorrectBoardError as err:
70
+ logger.warning(
71
+ f"Board returned type {err.returned_type!r}, "
72
+ f"expected {err.expected_type!r}. Ignoring this device")
73
+ return None
74
+
75
+ return board
76
+
77
+ def identify(self) -> BoardIdentity:
78
+ """
79
+ Get the identity of the board.
80
+
81
+ :return: The identity of the board.
82
+ """
83
+ response = self._serial.query('*IDN?')
84
+ return BoardIdentity(*response.split(':'))
85
+
86
+ def get_time(self) -> float:
87
+ time_str = self._serial.query('TIME?')
88
+ return datetime.fromisoformat(time_str).timestamp()
89
+
90
+ def sleep(self, duration: float) -> None:
91
+ if duration < 0:
92
+ raise ValueError("sleep length must be non-negative")
93
+
94
+ self._serial.query(f'SLEEP:{int(duration * 1000)}')
sr/robot3/timeout.py CHANGED
@@ -1,4 +1,5 @@
1
1
  """Functions for killing the robot after a certain amount of time."""
2
+ import atexit
2
3
  import logging
3
4
  import os
4
5
  import signal
@@ -9,6 +10,9 @@ from typing import Optional
9
10
 
10
11
  logger = logging.getLogger(__name__)
11
12
 
13
+ TIMEOUT_MESSAGE = "Timeout expired: Game Over!"
14
+ EXIT_ATTEMPTS = 0
15
+
12
16
 
13
17
  def timeout_handler(signal_type: int, stack_frame: Optional[FrameType]) -> None:
14
18
  """
@@ -17,13 +21,34 @@ def timeout_handler(signal_type: int, stack_frame: Optional[FrameType]) -> None:
17
21
  This function is called when the timeout expires and will stop the robot's main process.
18
22
  In order for this to work, any threads that are created must be daemons.
19
23
 
24
+ If the process doesn't terminate clearly (perhaps because the exception was caught),
25
+ exit less cleanly.
26
+
20
27
  NOTE: This function is not called on Windows.
21
28
 
22
- :param signal_type: The sginal that triggered this handler
29
+ :param signal_type: The signal that triggered this handler
23
30
  :param stack_frame: The stack frame at the time of the signal
24
31
  """
25
- logger.info("Timeout expired: Game Over!")
26
- exit(0)
32
+ global EXIT_ATTEMPTS
33
+ EXIT_ATTEMPTS += 1
34
+
35
+ if sys.platform == "win32":
36
+ raise AssertionError("This function should not be called on Windows")
37
+
38
+ if EXIT_ATTEMPTS == 1:
39
+ # Allow 2 seconds for the process to exit cleanly before killing it
40
+ signal.alarm(2)
41
+ logger.info(TIMEOUT_MESSAGE)
42
+ # Exit cleanly
43
+ exit(0)
44
+ else:
45
+ # The process didn't exit cleanly, so first call the cleanup handlers
46
+ # and use an unhanded alarm to force python to exit with a core dump
47
+ signal.signal(signal.SIGALRM, signal.SIG_DFL)
48
+ signal.alarm(2) # Allow 2 seconds for cleanup
49
+
50
+ atexit._run_exitfuncs()
51
+ exit(0)
27
52
 
28
53
 
29
54
  def win_timeout_handler() -> None:
@@ -35,7 +60,7 @@ def win_timeout_handler() -> None:
35
60
 
36
61
  NOTE: This function is only called on Windows.
37
62
  """
38
- logger.info("Timeout expired: Game Over!")
63
+ logger.info(TIMEOUT_MESSAGE)
39
64
  os.kill(os.getpid(), signal.SIGTERM)
40
65
 
41
66
 
@@ -52,6 +77,7 @@ def kill_after_delay(timeout_seconds: int) -> None:
52
77
  # Windows doesn't have SIGALRM,
53
78
  # so we approximate its functionality using a delayed SIGTERM
54
79
  timer = Timer(timeout_seconds, win_timeout_handler)
80
+ timer.daemon = True
55
81
  timer.start()
56
82
  else:
57
83
  signal.signal(signal.SIGALRM, timeout_handler)
sr/robot3/utils.py CHANGED
@@ -2,6 +2,7 @@
2
2
  from __future__ import annotations
3
3
 
4
4
  import logging
5
+ import os
5
6
  import signal
6
7
  import socket
7
8
  from abc import ABC, abstractmethod
@@ -14,6 +15,8 @@ from serial.tools.list_ports_common import ListPortInfo
14
15
  T = TypeVar('T')
15
16
  logger = logging.getLogger(__name__)
16
17
 
18
+ IN_SIMULATOR = os.environ.get('WEBOTS_SIMULATOR', '') == '1'
19
+
17
20
 
18
21
  class BoardIdentity(NamedTuple):
19
22
  """
@@ -34,6 +37,13 @@ class BoardIdentity(NamedTuple):
34
37
  sw_version: str = ""
35
38
 
36
39
 
40
+ class BoardInfo(NamedTuple):
41
+ """A container for the information about a board connection."""
42
+ url: str
43
+ serial_number: str
44
+ type_str: str
45
+
46
+
37
47
  class Board(ABC):
38
48
  """
39
49
  This is the base class for all boards.
@@ -123,7 +133,7 @@ def singular(container: Mapping[str, T]) -> T:
123
133
 
124
134
  :param container: A mapping of connected boards of a type
125
135
  :raises RuntimeError: If there is not exactly one of this type of board connected
126
- :return: _description_
136
+ :return: The connected board of the type
127
137
  """
128
138
  length = len(container)
129
139
 
@@ -228,6 +238,36 @@ def ensure_atexit_on_term() -> None:
228
238
  signal.signal(signal.SIGTERM, handle_signal)
229
239
 
230
240
 
241
+ def get_simulator_boards(board_filter: str = '') -> list[BoardInfo]:
242
+ """
243
+ Get a list of all boards configured in the simulator.
244
+
245
+ This is used to support discovery of boards in the simulator environment.
246
+
247
+ :param board_filter: A filter to only return boards of a certain type
248
+ :return: A list of board connection information
249
+ """
250
+ if 'WEBOTS_ROBOT' not in os.environ:
251
+ return []
252
+
253
+ simulator_data = os.environ['WEBOTS_ROBOT'].splitlines()
254
+ simulator_boards = []
255
+
256
+ for board_data in simulator_data:
257
+ board_data = board_data.rstrip('/')
258
+ board_fragment, serial_number = board_data.rsplit('/', 1)
259
+ board_url, board_type = board_fragment.rsplit('/', 1)
260
+
261
+ board_info = BoardInfo(url=board_url, serial_number=serial_number, type_str=board_type)
262
+
263
+ if board_filter and board_info.type_str != board_filter:
264
+ continue
265
+
266
+ simulator_boards.append(board_info)
267
+
268
+ return simulator_boards
269
+
270
+
231
271
  def list_ports() -> None:
232
272
  """
233
273
  Print a list of all connected USB serial ports.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: sr-robot3
3
- Version: 2024.0.0rc2
3
+ Version: 2025.0.0
4
4
  Summary: Student Robotics API for Python 3
5
5
  Author-email: Student Robotics <kit-team@studentrobotics.org>
6
6
  License: MIT License
@@ -37,22 +37,24 @@ Classifier: Topic :: Education
37
37
  Requires-Python: >=3.8
38
38
  Description-Content-Type: text/markdown
39
39
  License-File: LICENSE
40
- Requires-Dist: pyserial <4,>=3
41
- Requires-Dist: april-vision <3,>=2.1
42
- Requires-Dist: paho-mqtt <2,>=1.6
43
- Requires-Dist: pydantic <2,>=1.9.1
44
- Requires-Dist: typing-extensions ; python_version < "3.10"
40
+ Requires-Dist: pyserial<4,>=3
41
+ Requires-Dist: april-vision==2.2.0
42
+ Requires-Dist: paho-mqtt<3,>=2
43
+ Requires-Dist: pydantic<2,>=1.9.1
44
+ Requires-Dist: typing-extensions; python_version < "3.10"
45
+ Requires-Dist: tomli<3,>=2.0.1; python_version < "3.11"
45
46
  Provides-Extra: dev
46
- Requires-Dist: flake8 ; extra == 'dev'
47
- Requires-Dist: isort ; extra == 'dev'
48
- Requires-Dist: mypy ; extra == 'dev'
49
- Requires-Dist: build ; extra == 'dev'
50
- Requires-Dist: types-pyserial ; extra == 'dev'
51
- Requires-Dist: pytest ; extra == 'dev'
52
- Requires-Dist: pytest-cov ; extra == 'dev'
53
- Requires-Dist: types-paho-mqtt ; extra == 'dev'
47
+ Requires-Dist: flake8; extra == "dev"
48
+ Requires-Dist: isort; extra == "dev"
49
+ Requires-Dist: build; extra == "dev"
50
+ Requires-Dist: types-pyserial; extra == "dev"
51
+ Requires-Dist: pytest; extra == "dev"
52
+ Requires-Dist: pytest-cov; extra == "dev"
53
+ Requires-Dist: paho-mqtt<3,>=2; extra == "dev"
54
+ Requires-Dist: mypy==1.10.0; python_version < "3.9" and extra == "dev"
55
+ Requires-Dist: mypy<2,>=1.7; python_version >= "3.9" and extra == "dev"
54
56
  Provides-Extra: vision
55
- Requires-Dist: opencv-python-headless <5,>=4 ; extra == 'vision'
57
+ Requires-Dist: opencv-python-headless<5,>=4; extra == "vision"
56
58
 
57
59
  # sr-robot3
58
60
 
@@ -0,0 +1,29 @@
1
+ sr/robot3/__init__.py,sha256=lW7j65ruMEkRNJjZzp8929TefPMaMhqXzpsGPJqMkRs,1476
2
+ sr/robot3/_version.py,sha256=-X88o2Nur4PzeUeRdAC4dP3gTVlb9JWw08awOdH4TYg,417
3
+ sr/robot3/arduino.py,sha256=9rGsvGDb1JoHB4c7W61R16Y3eb0-UZ9TE49Qu7BJIZE,16934
4
+ sr/robot3/astoria.py,sha256=0EIXoxvs1glkGuzLDYG7HptKucc8q7sdWAQiu96NG24,8547
5
+ sr/robot3/camera.py,sha256=b2r8-ttQgLqbMv9BI6qWb5sHjeg4Zb5VrGo2uWMYGd8,8686
6
+ sr/robot3/exceptions.py,sha256=A8cpuvFaIFazWSrp9QNts9ikINWymgoGALv4JAL-SU8,1381
7
+ sr/robot3/game_specific.py,sha256=X8vLUbTRdNPZEggC6LxZzaNb9oqMly8-3PyQoD3B1ho,282
8
+ sr/robot3/kch.py,sha256=CxMbRztCNz75Uikv3KaViV6zYlXsJuBbI6qg2AsQW4Q,12500
9
+ sr/robot3/logging.py,sha256=1KO1yW2RP9qSUHr-i1NOMM68szxML5qJyDKtdt34uyg,3464
10
+ sr/robot3/marker.py,sha256=6LzT59xjEFroljcDsejfcUeU-5dlDAEWbPepp-25fOY,5403
11
+ sr/robot3/motor_board.py,sha256=6yYhz0WSLzPvAzREIYUmCLeCtc6Qwkm7hdTOPFgRZ70,11356
12
+ sr/robot3/mqtt.py,sha256=1xKQ0uwkupURWFXrvsD75TE-GlltfbrpVNqoabgmsrc,6679
13
+ sr/robot3/power_board.py,sha256=3Skbc7kmG1pr2K6n94POkMOXT4jzbOiYCz2cb60CWv8,17734
14
+ sr/robot3/py.typed,sha256=dXL7b_5DnfZ_09rOcxu7D-0rT56DRm26uGUohPJOPQc,94
15
+ sr/robot3/raw_serial.py,sha256=rdh0lyS9tJDyHbAZKyJSO8lKvdJ3-_iZzvmUSOdbo08,7368
16
+ sr/robot3/robot.py,sha256=cqUrPFver_yAjdTPBLqPjsoliSuNdiHn7RGrpXLsuMA,15888
17
+ sr/robot3/serial_wrapper.py,sha256=CDRVFbIILf4ahiToIHrVf02hz_GXpFeazDHdw3rGgX0,9327
18
+ sr/robot3/servo_board.py,sha256=1GppUoqdx9wUA551bO95dEbtCGdzS10twv8OZ3S6t8U,12505
19
+ sr/robot3/timeout.py,sha256=qBPCOwnOW9h0fLnL-peraO9RsaKWc92TcTYNgyTzo_g,2678
20
+ sr/robot3/utils.py,sha256=dA2WyB73hnw41jLkOpk6ail7mfrW3oxKU0KrpQuHv1Y,9146
21
+ sr/robot3/calibrations/__init__.py,sha256=TTL-lkU5tj3EszSEInRTftNvX5rsMyRUSHf12RlJ4QY,130
22
+ sr/robot3/simulator/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
23
+ sr/robot3/simulator/camera.py,sha256=thenTnlyHzHrosUg9juUSfYCs2U4dfkS7vFoto1b3_k,3138
24
+ sr/robot3/simulator/time_server.py,sha256=MwFJM7p3vqf4balmGwIMXcCLBVJQgnq2X4gY_agalwc,2859
25
+ sr_robot3-2025.0.0.dist-info/LICENSE,sha256=d9EjkBp2jG1JOJ3zKfKRwBtPe1nhh1VByrw4iH3A9WA,1101
26
+ sr_robot3-2025.0.0.dist-info/METADATA,sha256=iQFR8uEwpNe7S497QqT56Pash4wDEbw2Ud0V7K6ZjJY,5322
27
+ sr_robot3-2025.0.0.dist-info/WHEEL,sha256=UvcQYKBHoFqaQd6LKyqHw9fxEolWLQnlzP0h_LgJAfI,91
28
+ sr_robot3-2025.0.0.dist-info/top_level.txt,sha256=XFzWuC7umCVdV-O6IuC9Rl0qehvEC-U34M9WJ2-2h3E,3
29
+ sr_robot3-2025.0.0.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: bdist_wheel (0.41.2)
2
+ Generator: setuptools (74.0.0)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5
 
@@ -1,26 +0,0 @@
1
- sr/robot3/__init__.py,sha256=lW7j65ruMEkRNJjZzp8929TefPMaMhqXzpsGPJqMkRs,1476
2
- sr/robot3/_version.py,sha256=SXjMGAhD6YctTnTcwOWelhbWC-EouEhUcdYCxGBqZ-s,169
3
- sr/robot3/arduino.py,sha256=sz-9AAg649Spp70sit3GwrwvDhsMfAM7yv94Dgg3gfM,14362
4
- sr/robot3/astoria.py,sha256=dyrK_vMFHl9F-E9sXgDGNbuW3SLcTCQzNgu_-d7uB0Q,8435
5
- sr/robot3/camera.py,sha256=UKvJFLGVptZLquNpGne34AaEnqw07CpH8C-gaxPdUrQ,6621
6
- sr/robot3/exceptions.py,sha256=A8cpuvFaIFazWSrp9QNts9ikINWymgoGALv4JAL-SU8,1381
7
- sr/robot3/game_specific.py,sha256=X8vLUbTRdNPZEggC6LxZzaNb9oqMly8-3PyQoD3B1ho,282
8
- sr/robot3/kch.py,sha256=uPdzxdbFxCS0F7UdkUsIuh7xXyKs8Jw4QpHBeVw_k-U,5413
9
- sr/robot3/logging.py,sha256=QHvdxWieSyQcu2pKpjG6ur3A8BWGwDia4h3lOjBKDZ4,3447
10
- sr/robot3/marker.py,sha256=FDRWl-tNN7xeRnlQLRitjumuligJmVxa2Mr49n1YurE,4886
11
- sr/robot3/motor_board.py,sha256=qfwbchUdfdZoSfeTwNuG24GR2cCmo_5vdFSkQtPIQxk,10109
12
- sr/robot3/mqtt.py,sha256=6JQdPlx1LpVE1g_n2hbQ2vAOUKwWrtNcZlH-F_q-0Lc,6300
13
- sr/robot3/power_board.py,sha256=tKBwuqDqqc_N0jdOxBstHe_eoZ1glOTv1eEuV_1287k,16213
14
- sr/robot3/py.typed,sha256=dXL7b_5DnfZ_09rOcxu7D-0rT56DRm26uGUohPJOPQc,94
15
- sr/robot3/raw_serial.py,sha256=nXNdaPfPL7l9dznkuaniRjb8EYnE4_wsQPi7dESxUXE,4876
16
- sr/robot3/robot.py,sha256=iPvbvFFamZe1x0ebgUqsTHmlKUEr3MZoGHuzAD4Pqdo,13239
17
- sr/robot3/serial_wrapper.py,sha256=iBN9GvOQdxPSz3LO3Mqxoskm7wltN7YeiORg_yIV0jQ,8832
18
- sr/robot3/servo_board.py,sha256=EUTxCXq79wGWK2CAmc3nzviGBeJpNq1F_wz-CX2FKmY,11234
19
- sr/robot3/timeout.py,sha256=QA-Rer05wl1wnt4hKVXwlMnMAsyaqTvcLTU7dmrr6IE,1843
20
- sr/robot3/utils.py,sha256=tpLVRVEC_6nBdiBxecSDgLmExy6_bLhrJKkMPrN2vew,7936
21
- sr/robot3/calibrations/__init__.py,sha256=TTL-lkU5tj3EszSEInRTftNvX5rsMyRUSHf12RlJ4QY,130
22
- sr_robot3-2024.0.0rc2.dist-info/LICENSE,sha256=d9EjkBp2jG1JOJ3zKfKRwBtPe1nhh1VByrw4iH3A9WA,1101
23
- sr_robot3-2024.0.0rc2.dist-info/METADATA,sha256=GhMKUBWNH2otMOa7laLSTndBVeX3J9OITfv2QWkJJxY,5180
24
- sr_robot3-2024.0.0rc2.dist-info/WHEEL,sha256=yQN5g4mg4AybRjkgi-9yy4iQEFibGQmlz78Pik5Or-A,92
25
- sr_robot3-2024.0.0rc2.dist-info/top_level.txt,sha256=XFzWuC7umCVdV-O6IuC9Rl0qehvEC-U34M9WJ2-2h3E,3
26
- sr_robot3-2024.0.0rc2.dist-info/RECORD,,