sr-robot3 2024.0.0rc2__tar.gz → 2025.0.0__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.
- {sr-robot3-2024.0.0rc2 → sr_robot3-2025.0.0}/.github/workflows/test_build.yml +11 -11
- {sr-robot3-2024.0.0rc2 → sr_robot3-2025.0.0}/PKG-INFO +7 -5
- {sr-robot3-2024.0.0rc2 → sr_robot3-2025.0.0}/pyproject.toml +6 -4
- sr_robot3-2025.0.0/sr/robot3/_version.py +16 -0
- {sr-robot3-2024.0.0rc2 → sr_robot3-2025.0.0}/sr/robot3/arduino.py +91 -25
- {sr-robot3-2024.0.0rc2 → sr_robot3-2025.0.0}/sr/robot3/astoria.py +7 -2
- {sr-robot3-2024.0.0rc2 → sr_robot3-2025.0.0}/sr/robot3/camera.py +92 -24
- sr_robot3-2025.0.0/sr/robot3/kch.py +414 -0
- {sr-robot3-2024.0.0rc2 → sr_robot3-2025.0.0}/sr/robot3/logging.py +1 -1
- {sr-robot3-2024.0.0rc2 → sr_robot3-2025.0.0}/sr/robot3/marker.py +8 -0
- {sr-robot3-2024.0.0rc2 → sr_robot3-2025.0.0}/sr/robot3/motor_board.py +42 -10
- {sr-robot3-2024.0.0rc2 → sr_robot3-2025.0.0}/sr/robot3/mqtt.py +23 -8
- {sr-robot3-2024.0.0rc2 → sr_robot3-2025.0.0}/sr/robot3/power_board.py +54 -13
- {sr-robot3-2024.0.0rc2 → sr_robot3-2025.0.0}/sr/robot3/raw_serial.py +71 -10
- {sr-robot3-2024.0.0rc2 → sr_robot3-2025.0.0}/sr/robot3/robot.py +100 -28
- {sr-robot3-2024.0.0rc2 → sr_robot3-2025.0.0}/sr/robot3/serial_wrapper.py +25 -13
- {sr-robot3-2024.0.0rc2 → sr_robot3-2025.0.0}/sr/robot3/servo_board.py +44 -11
- sr_robot3-2025.0.0/sr/robot3/simulator/camera.py +75 -0
- sr_robot3-2025.0.0/sr/robot3/simulator/time_server.py +94 -0
- {sr-robot3-2024.0.0rc2 → sr_robot3-2025.0.0}/sr/robot3/timeout.py +30 -4
- {sr-robot3-2024.0.0rc2 → sr_robot3-2025.0.0}/sr/robot3/utils.py +41 -1
- {sr-robot3-2024.0.0rc2 → sr_robot3-2025.0.0}/sr_robot3.egg-info/PKG-INFO +7 -5
- {sr-robot3-2024.0.0rc2 → sr_robot3-2025.0.0}/sr_robot3.egg-info/SOURCES.txt +10 -0
- sr_robot3-2025.0.0/sr_robot3.egg-info/requires.txt +28 -0
- {sr-robot3-2024.0.0rc2 → sr_robot3-2025.0.0}/tests/conftest.py +1 -1
- sr_robot3-2025.0.0/tests/test_data/empty/.gitkeep +0 -0
- sr_robot3-2025.0.0/tests/test_data/timeout_scripts/catch-base-exception.py +10 -0
- sr_robot3-2025.0.0/tests/test_data/timeout_scripts/catch-exception.py +10 -0
- sr_robot3-2025.0.0/tests/test_data/timeout_scripts/hot-loop.py +7 -0
- sr_robot3-2025.0.0/tests/test_data/timeout_scripts/sleep.py +6 -0
- sr_robot3-2025.0.0/tests/test_data/timeout_scripts/try-finally.py +11 -0
- sr_robot3-2025.0.0/tests/test_data/timeout_scripts_extra/early-exit.py +3 -0
- sr_robot3-2025.0.0/tests/test_data/timeout_scripts_extra/exception.py +5 -0
- {sr-robot3-2024.0.0rc2 → sr_robot3-2025.0.0}/tests/test_power_board.py +1 -1
- {sr-robot3-2024.0.0rc2 → sr_robot3-2025.0.0}/tests/test_raw_serial.py +14 -4
- sr_robot3-2025.0.0/tests/test_timeout.py +99 -0
- sr-robot3-2024.0.0rc2/sr/robot3/_version.py +0 -4
- sr-robot3-2024.0.0rc2/sr/robot3/kch.py +0 -188
- sr-robot3-2024.0.0rc2/sr_robot3.egg-info/requires.txt +0 -20
- sr-robot3-2024.0.0rc2/tests/test_timeout.py +0 -55
- {sr-robot3-2024.0.0rc2 → sr_robot3-2025.0.0}/.flake8 +0 -0
- {sr-robot3-2024.0.0rc2 → sr_robot3-2025.0.0}/.github/dependabot.yml +0 -0
- {sr-robot3-2024.0.0rc2 → sr_robot3-2025.0.0}/.gitignore +0 -0
- {sr-robot3-2024.0.0rc2 → sr_robot3-2025.0.0}/LICENSE +0 -0
- {sr-robot3-2024.0.0rc2 → sr_robot3-2025.0.0}/Makefile +0 -0
- {sr-robot3-2024.0.0rc2 → sr_robot3-2025.0.0}/README.md +0 -0
- {sr-robot3-2024.0.0rc2 → sr_robot3-2025.0.0}/setup.cfg +0 -0
- {sr-robot3-2024.0.0rc2 → sr_robot3-2025.0.0}/sr/__init__.py +0 -0
- {sr-robot3-2024.0.0rc2 → sr_robot3-2025.0.0}/sr/robot3/__init__.py +0 -0
- {sr-robot3-2024.0.0rc2 → sr_robot3-2025.0.0}/sr/robot3/calibrations/__init__.py +0 -0
- {sr-robot3-2024.0.0rc2 → sr_robot3-2025.0.0}/sr/robot3/exceptions.py +0 -0
- {sr-robot3-2024.0.0rc2 → sr_robot3-2025.0.0}/sr/robot3/game_specific.py +0 -0
- {sr-robot3-2024.0.0rc2 → sr_robot3-2025.0.0}/sr/robot3/py.typed +0 -0
- /sr-robot3-2024.0.0rc2/stubs/RPi/__init__.pyi → /sr_robot3-2025.0.0/sr/robot3/simulator/__init__.py +0 -0
- {sr-robot3-2024.0.0rc2 → sr_robot3-2025.0.0}/sr_robot3.egg-info/dependency_links.txt +0 -0
- {sr-robot3-2024.0.0rc2 → sr_robot3-2025.0.0}/sr_robot3.egg-info/top_level.txt +0 -0
- {sr-robot3-2024.0.0rc2 → sr_robot3-2025.0.0}/stubs/RPi/GPIO.pyi +0 -0
- /sr-robot3-2024.0.0rc2/tests/test_data/empty/.gitkeep → /sr_robot3-2025.0.0/stubs/RPi/__init__.pyi +0 -0
- {sr-robot3-2024.0.0rc2 → sr_robot3-2025.0.0}/tests/__init__.py +0 -0
- {sr-robot3-2024.0.0rc2 → sr_robot3-2025.0.0}/tests/test_arduino.py +0 -0
- {sr-robot3-2024.0.0rc2 → sr_robot3-2025.0.0}/tests/test_data/bad/metadata.json +0 -0
- {sr-robot3-2024.0.0rc2 → sr_robot3-2025.0.0}/tests/test_data/marker_detections.json +0 -0
- {sr-robot3-2024.0.0rc2 → sr_robot3-2025.0.0}/tests/test_data/marker_locations.csv +0 -0
- {sr-robot3-2024.0.0rc2 → sr_robot3-2025.0.0}/tests/test_data/missing_key/metadata.json +0 -0
- {sr-robot3-2024.0.0rc2 → sr_robot3-2025.0.0}/tests/test_data/nested/valid/metadata.json +0 -0
- {sr-robot3-2024.0.0rc2 → sr_robot3-2025.0.0}/tests/test_data/not_object/metadata.json +0 -0
- {sr-robot3-2024.0.0rc2 → sr_robot3-2025.0.0}/tests/test_data/valid/metadata.json +0 -0
- {sr-robot3-2024.0.0rc2 → sr_robot3-2025.0.0}/tests/test_logging.py +0 -0
- {sr-robot3-2024.0.0rc2 → sr_robot3-2025.0.0}/tests/test_markers.py +0 -0
- {sr-robot3-2024.0.0rc2 → sr_robot3-2025.0.0}/tests/test_motor_board.py +0 -0
- {sr-robot3-2024.0.0rc2 → sr_robot3-2025.0.0}/tests/test_serial_wrapper.py +0 -0
- {sr-robot3-2024.0.0rc2 → sr_robot3-2025.0.0}/tests/test_servo_board.py +0 -0
- {sr-robot3-2024.0.0rc2 → sr_robot3-2025.0.0}/tests/test_sr_robot.py +0 -0
@@ -1,6 +1,8 @@
|
|
1
1
|
name: Lint & build
|
2
2
|
|
3
|
-
on:
|
3
|
+
on:
|
4
|
+
push:
|
5
|
+
workflow_dispatch:
|
4
6
|
|
5
7
|
jobs:
|
6
8
|
test:
|
@@ -8,23 +10,23 @@ jobs:
|
|
8
10
|
fail-fast: false
|
9
11
|
matrix:
|
10
12
|
os: [ubuntu-latest]
|
11
|
-
py_version: ["3.8", "3.9", "3.10", "3.11"]
|
13
|
+
py_version: ["3.8", "3.9", "3.10", "3.11", "3.12"]
|
12
14
|
include:
|
13
15
|
- os: windows-latest
|
14
16
|
py_version: "3.8"
|
15
17
|
- os: windows-latest
|
16
|
-
py_version: "3.
|
18
|
+
py_version: "3.12"
|
17
19
|
- os: macos-latest
|
18
20
|
py_version: "3.8"
|
19
21
|
- os: macos-latest
|
20
|
-
py_version: "3.
|
22
|
+
py_version: "3.12"
|
21
23
|
runs-on: ${{ matrix.os }}
|
22
24
|
steps:
|
23
|
-
- uses: actions/checkout@
|
25
|
+
- uses: actions/checkout@v4
|
24
26
|
with:
|
25
27
|
fetch-depth: 0
|
26
28
|
- name: Set up Python
|
27
|
-
uses: actions/setup-python@
|
29
|
+
uses: actions/setup-python@v5
|
28
30
|
with:
|
29
31
|
python-version: ${{ matrix.py_version }}
|
30
32
|
- name: Install dependencies
|
@@ -50,11 +52,11 @@ jobs:
|
|
50
52
|
runs-on: ubuntu-latest
|
51
53
|
needs: test
|
52
54
|
steps:
|
53
|
-
- uses: actions/checkout@
|
55
|
+
- uses: actions/checkout@v4
|
54
56
|
with:
|
55
57
|
fetch-depth: 0
|
56
58
|
- name: Set up Python 3.8
|
57
|
-
uses: actions/setup-python@
|
59
|
+
uses: actions/setup-python@v5
|
58
60
|
with:
|
59
61
|
python-version: "3.8"
|
60
62
|
- name: Install dependencies
|
@@ -66,7 +68,7 @@ jobs:
|
|
66
68
|
run: |
|
67
69
|
make build
|
68
70
|
- name: Save built package
|
69
|
-
uses: actions/upload-artifact@
|
71
|
+
uses: actions/upload-artifact@v4
|
70
72
|
with:
|
71
73
|
name: package
|
72
74
|
path: |
|
@@ -74,5 +76,3 @@ jobs:
|
|
74
76
|
- name: Publish to PyPi
|
75
77
|
if: ${{ github.ref_type == 'tag' }}
|
76
78
|
uses: pypa/gh-action-pypi-publish@release/v1
|
77
|
-
with:
|
78
|
-
print_hash: true
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.1
|
2
2
|
Name: sr-robot3
|
3
|
-
Version:
|
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
|
@@ -38,19 +38,21 @@ Requires-Python: >=3.8
|
|
38
38
|
Description-Content-Type: text/markdown
|
39
39
|
License-File: LICENSE
|
40
40
|
Requires-Dist: pyserial<4,>=3
|
41
|
-
Requires-Dist: april_vision
|
42
|
-
Requires-Dist: paho-mqtt<2
|
41
|
+
Requires-Dist: april_vision==2.2.0
|
42
|
+
Requires-Dist: paho-mqtt<3,>=2
|
43
43
|
Requires-Dist: pydantic<2,>=1.9.1
|
44
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
47
|
Requires-Dist: flake8; extra == "dev"
|
47
48
|
Requires-Dist: isort; extra == "dev"
|
48
|
-
Requires-Dist: mypy; extra == "dev"
|
49
|
+
Requires-Dist: mypy==1.10.0; python_version < "3.9" and extra == "dev"
|
50
|
+
Requires-Dist: mypy<2,>=1.7; python_version >= "3.9" and extra == "dev"
|
49
51
|
Requires-Dist: build; extra == "dev"
|
50
52
|
Requires-Dist: types-pyserial; extra == "dev"
|
51
53
|
Requires-Dist: pytest; extra == "dev"
|
52
54
|
Requires-Dist: pytest-cov; extra == "dev"
|
53
|
-
Requires-Dist:
|
55
|
+
Requires-Dist: paho-mqtt<3,>=2; extra == "dev"
|
54
56
|
Provides-Extra: vision
|
55
57
|
Requires-Dist: opencv-python-headless<5,>=4; extra == "vision"
|
56
58
|
|
@@ -53,10 +53,11 @@ dynamic = ["version"]
|
|
53
53
|
requires-python = ">=3.8"
|
54
54
|
dependencies = [
|
55
55
|
"pyserial >=3,<4",
|
56
|
-
"april_vision
|
57
|
-
"paho-mqtt >=
|
56
|
+
"april_vision==2.2.0",
|
57
|
+
"paho-mqtt >=2,<3",
|
58
58
|
"pydantic >=1.9.1,<2",
|
59
59
|
"typing-extensions; python_version<'3.10'",
|
60
|
+
"tomli >=2.0.1,<3; python_version<'3.11'",
|
60
61
|
]
|
61
62
|
classifiers = [
|
62
63
|
"Topic :: Software Development :: Libraries :: Python Modules",
|
@@ -75,11 +76,12 @@ Documentation = "https://docs.studentrobotics.org"
|
|
75
76
|
dev = [
|
76
77
|
"flake8",
|
77
78
|
"isort",
|
78
|
-
"mypy",
|
79
|
+
"mypy==1.10.0; python_version<'3.9'",
|
80
|
+
"mypy>=1.7,<2; python_version>='3.9'",
|
79
81
|
"build",
|
80
82
|
"types-pyserial",
|
81
83
|
"pytest",
|
82
84
|
"pytest-cov",
|
83
|
-
"
|
85
|
+
"paho-mqtt >=2,<3"
|
84
86
|
]
|
85
87
|
vision = ["opencv-python-headless >=4,<5"]
|
@@ -0,0 +1,16 @@
|
|
1
|
+
# file generated by setuptools_scm
|
2
|
+
# don't change, don't track in version control
|
3
|
+
TYPE_CHECKING = False
|
4
|
+
if TYPE_CHECKING:
|
5
|
+
from typing import Tuple, Union
|
6
|
+
VERSION_TUPLE = Tuple[Union[int, str], ...]
|
7
|
+
else:
|
8
|
+
VERSION_TUPLE = object
|
9
|
+
|
10
|
+
version: str
|
11
|
+
__version__: str
|
12
|
+
__version_tuple__: VERSION_TUPLE
|
13
|
+
version_tuple: VERSION_TUPLE
|
14
|
+
|
15
|
+
__version__ = version = '2025.0.0'
|
16
|
+
__version_tuple__ = version_tuple = (2025, 0, 0)
|
@@ -11,10 +11,18 @@ from serial.tools.list_ports import comports
|
|
11
11
|
from .exceptions import IncorrectBoardError
|
12
12
|
from .logging import log_to_debug
|
13
13
|
from .serial_wrapper import SerialWrapper
|
14
|
-
from .utils import
|
14
|
+
from .utils import (
|
15
|
+
IN_SIMULATOR, Board, BoardIdentity,
|
16
|
+
get_simulator_boards, get_USB_identity, map_to_float,
|
17
|
+
)
|
15
18
|
|
16
19
|
logger = logging.getLogger(__name__)
|
17
20
|
BAUDRATE = 115200
|
21
|
+
if IN_SIMULATOR:
|
22
|
+
# Place each command on a new line in the simulator to simplify the implementation
|
23
|
+
ENDLINE = '\n'
|
24
|
+
else:
|
25
|
+
ENDLINE = ''
|
18
26
|
|
19
27
|
SUPPORTED_VID_PIDS = {
|
20
28
|
(0x2341, 0x0043), # Arduino Uno rev 3
|
@@ -58,7 +66,7 @@ class Arduino(Board):
|
|
58
66
|
"""
|
59
67
|
The Arduino board interface.
|
60
68
|
|
61
|
-
This is intended to be used with Arduino Uno boards running the
|
69
|
+
This is intended to be used with Arduino Uno boards running the SR firmware.
|
62
70
|
|
63
71
|
:param serial_port: The serial port to connect to.
|
64
72
|
:param initial_identity: The identity of the board, as reported by the USB descriptor.
|
@@ -127,17 +135,46 @@ class Arduino(Board):
|
|
127
135
|
f"expected {err.expected_type!r}. Ignoring this device")
|
128
136
|
return None
|
129
137
|
except Exception:
|
130
|
-
if initial_identity is not None
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
+
if initial_identity is not None:
|
139
|
+
if initial_identity.board_type == 'manual':
|
140
|
+
logger.warning(
|
141
|
+
f"Manually specified Arduino at port {serial_port!r} "
|
142
|
+
"could not be identified. Ignoring this device")
|
143
|
+
elif initial_identity.manufacturer == 'sbot_simulator':
|
144
|
+
logger.warning(
|
145
|
+
f"Simulator specified arduino at port {serial_port!r} "
|
146
|
+
"could not be identified. Ignoring this device")
|
147
|
+
return None
|
148
|
+
|
149
|
+
logger.warning(
|
150
|
+
f"Found Arduino-like serial port at {serial_port!r}, "
|
151
|
+
"but it could not be identified. Ignoring this device")
|
138
152
|
return None
|
139
153
|
return board
|
140
154
|
|
155
|
+
@classmethod
|
156
|
+
def _get_simulator_boards(cls) -> MappingProxyType[str, Arduino]:
|
157
|
+
"""
|
158
|
+
Get the simulator boards.
|
159
|
+
|
160
|
+
:return: A mapping of board serial numbers to Arduinos
|
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('Arduino'):
|
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
|
+
|
141
178
|
@classmethod
|
142
179
|
def _get_supported_boards(
|
143
180
|
cls,
|
@@ -149,8 +186,12 @@ class Arduino(Board):
|
|
149
186
|
|
150
187
|
:param manual_boards: A list of manually specified board port strings,
|
151
188
|
defaults to None
|
189
|
+
:param ignored_serials: A list of serial number to ignore during board discovery
|
152
190
|
:return: A mapping of board serial numbers to Arduinos
|
153
191
|
"""
|
192
|
+
if IN_SIMULATOR:
|
193
|
+
return cls._get_simulator_boards()
|
194
|
+
|
154
195
|
boards = {}
|
155
196
|
if ignored_serials is None:
|
156
197
|
ignored_serials = []
|
@@ -191,7 +232,7 @@ class Arduino(Board):
|
|
191
232
|
|
192
233
|
:return: The identity of the board.
|
193
234
|
"""
|
194
|
-
response = self._serial.query('v', endl=
|
235
|
+
response = self._serial.query('v', endl=ENDLINE)
|
195
236
|
response_fields = response.split(':')
|
196
237
|
|
197
238
|
# The arduino firmware cannot access the serial number reported in the USB descriptor
|
@@ -223,13 +264,16 @@ class Arduino(Board):
|
|
223
264
|
:param command: The command to send to the board.
|
224
265
|
:return: The response from the board.
|
225
266
|
"""
|
226
|
-
|
267
|
+
if IN_SIMULATOR:
|
268
|
+
logger.warning("The command method is not fully supported in the simulator")
|
269
|
+
return self._serial.query(command, endl=ENDLINE)
|
227
270
|
|
228
271
|
def map_pin_number(self, pin_number: int) -> str:
|
229
272
|
"""
|
230
273
|
Map the pin number to the the serial format.
|
231
274
|
Pin numbers are sent as printable ASCII characters, with 0 being 'a'.
|
232
275
|
|
276
|
+
:param pin_number: The pin number to encode.
|
233
277
|
:return: The pin number in the serial format.
|
234
278
|
:raises ValueError: If the pin number is invalid.
|
235
279
|
"""
|
@@ -239,6 +283,34 @@ class Arduino(Board):
|
|
239
283
|
raise ValueError("Invalid pin provided") from None
|
240
284
|
return chr(pin_number + ord('a'))
|
241
285
|
|
286
|
+
@log_to_debug
|
287
|
+
def ultrasound_measure(
|
288
|
+
self,
|
289
|
+
pulse_pin: int,
|
290
|
+
echo_pin: int,
|
291
|
+
) -> int:
|
292
|
+
"""
|
293
|
+
Measure the distance to an object using an ultrasound sensor.
|
294
|
+
|
295
|
+
The sensor can only measure distances up to 4m.
|
296
|
+
|
297
|
+
:param pulse_pin: The pin to send the ultrasound pulse from.
|
298
|
+
:param echo_pin: The pin to read the ultrasound echo from.
|
299
|
+
:raises ValueError: If either of the pins are invalid
|
300
|
+
:return: The distance measured by the ultrasound sensor in mm.
|
301
|
+
"""
|
302
|
+
try: # bounds check
|
303
|
+
pulse_id = self.map_pin_number(pulse_pin)
|
304
|
+
except ValueError:
|
305
|
+
raise ValueError("Invalid pulse pin provided") from None
|
306
|
+
try:
|
307
|
+
echo_id = self.map_pin_number(echo_pin)
|
308
|
+
except ValueError:
|
309
|
+
raise ValueError("Invalid echo pin provided") from None
|
310
|
+
|
311
|
+
response = self._serial.query(f'u{pulse_id}{echo_id}', endl=ENDLINE)
|
312
|
+
return int(response)
|
313
|
+
|
242
314
|
def __repr__(self) -> str:
|
243
315
|
return f"<{self.__class__.__qualname__}: {self._serial}>"
|
244
316
|
|
@@ -250,6 +322,7 @@ class Pin:
|
|
250
322
|
:param serial: The serial wrapper to use to communicate with the board.
|
251
323
|
:param index: The index of the pin.
|
252
324
|
:param supports_analog: Whether the pin supports analog reads.
|
325
|
+
:param disabled: Whether the pin can be controlled.
|
253
326
|
"""
|
254
327
|
__slots__ = ('_serial', '_index', '_supports_analog', '_disabled', '_mode')
|
255
328
|
|
@@ -300,7 +373,7 @@ class Pin:
|
|
300
373
|
mode_char = MODE_CHAR_MAP.get(value)
|
301
374
|
if mode_char is None:
|
302
375
|
raise IOError(f'Pin mode {value} is not supported')
|
303
|
-
self._serial.write(self._build_command(mode_char), endl=
|
376
|
+
self._serial.write(self._build_command(mode_char), endl=ENDLINE)
|
304
377
|
self._mode = value
|
305
378
|
|
306
379
|
@log_to_debug
|
@@ -315,7 +388,7 @@ class Pin:
|
|
315
388
|
self._check_if_disabled()
|
316
389
|
if self.mode not in DIGITAL_READ_MODES:
|
317
390
|
raise IOError(f'Digital read is not supported in {self.mode}')
|
318
|
-
response = self._serial.query(self._build_command('r'), endl=
|
391
|
+
response = self._serial.query(self._build_command('r'), endl=ENDLINE)
|
319
392
|
return response == 'h'
|
320
393
|
|
321
394
|
@log_to_debug
|
@@ -331,9 +404,9 @@ class Pin:
|
|
331
404
|
if self.mode not in DIGITAL_WRITE_MODES:
|
332
405
|
raise IOError(f'Digital write is not supported in {self.mode}')
|
333
406
|
if value:
|
334
|
-
self._serial.write(self._build_command('h'), endl=
|
407
|
+
self._serial.write(self._build_command('h'), endl=ENDLINE)
|
335
408
|
else:
|
336
|
-
self._serial.write(self._build_command('l'), endl=
|
409
|
+
self._serial.write(self._build_command('l'), endl=ENDLINE)
|
337
410
|
|
338
411
|
@log_to_debug
|
339
412
|
def analog_read(self) -> float:
|
@@ -354,7 +427,7 @@ class Pin:
|
|
354
427
|
raise IOError(f'Analog read is not supported in {self.mode}')
|
355
428
|
if not self._supports_analog:
|
356
429
|
raise IOError('Pin does not support analog read')
|
357
|
-
response = self._serial.query(self._build_command('a'), endl=
|
430
|
+
response = self._serial.query(self._build_command('a'), endl=ENDLINE)
|
358
431
|
# map the response from the ADC range to the voltage range
|
359
432
|
return map_to_float(int(response), ADC_MIN, ADC_MAX, 0.0, 5.0)
|
360
433
|
|
@@ -380,7 +453,7 @@ class Pin:
|
|
380
453
|
"""
|
381
454
|
Generate the command to send to the board.
|
382
455
|
|
383
|
-
:param
|
456
|
+
:param cmd_char: The command character to send.
|
384
457
|
:return: The command string.
|
385
458
|
"""
|
386
459
|
return f'{cmd_char}{self._map_pin_number()}'
|
@@ -393,13 +466,6 @@ class Pin:
|
|
393
466
|
)
|
394
467
|
|
395
468
|
|
396
|
-
# PIN:<n>:MODE:GET?
|
397
|
-
# PIN:<n>:MODE:SET:<value>
|
398
|
-
# PIN:<n>:DIGITAL:GET?
|
399
|
-
# PIN:<n>:DIGITAL:SET:<1/0>
|
400
|
-
# PIN:<n>:ANALOG:GET?
|
401
|
-
# ULTRASOUND:<pulse>:<echo>:MEASURE?
|
402
|
-
|
403
469
|
if __name__ == '__main__': # pragma: no cover
|
404
470
|
arduinos = Arduino._get_supported_boards()
|
405
471
|
for serial_num, board in arduinos.items():
|
@@ -9,8 +9,9 @@ from threading import Event, Lock
|
|
9
9
|
from time import sleep
|
10
10
|
from typing import Any, ClassVar, NewType, Optional, Tuple
|
11
11
|
|
12
|
+
import paho.mqtt.client as mqtt
|
12
13
|
from paho.mqtt.client import Client as MQTT
|
13
|
-
from paho.mqtt.client import MQTTMessage
|
14
|
+
from paho.mqtt.client import MQTTMessage
|
14
15
|
from pydantic import BaseModel, ValidationError
|
15
16
|
|
16
17
|
from .mqtt import MQTTClient
|
@@ -278,7 +279,11 @@ def init_mqtt(config: AstoriaConfig, client_name: str = 'sr-robot3') -> 'MQTTCli
|
|
278
279
|
host=config.mqtt.host,
|
279
280
|
port=config.mqtt.port,
|
280
281
|
client_name=client_name,
|
281
|
-
mqtt_version=
|
282
|
+
mqtt_version=(
|
283
|
+
mqtt.MQTTProtocolVersion.MQTTv311
|
284
|
+
if config.mqtt.force_protocol_version_3_1 else
|
285
|
+
mqtt.MQTTProtocolVersion.MQTTv5
|
286
|
+
),
|
282
287
|
topic_prefix=config.mqtt.topic_prefix,
|
283
288
|
)
|
284
289
|
return client
|
@@ -1,9 +1,11 @@
|
|
1
1
|
"""An implementation of a camera board using the april_vision library."""
|
2
|
+
from __future__ import annotations
|
3
|
+
|
2
4
|
import logging
|
3
5
|
from pathlib import Path
|
4
6
|
from typing import Callable, Dict, Iterable, List, Optional, Union
|
5
7
|
|
6
|
-
from april_vision import CalibratedCamera, Frame
|
8
|
+
from april_vision import CalibratedCamera, Frame, FrameSource
|
7
9
|
from april_vision import Marker as AprilMarker
|
8
10
|
from april_vision import (
|
9
11
|
Processor, USBCamera, __version__, calibrations,
|
@@ -13,8 +15,11 @@ from april_vision.helpers import Base64Sender
|
|
13
15
|
from numpy.typing import NDArray
|
14
16
|
|
15
17
|
from .marker import Marker
|
16
|
-
from .utils import
|
18
|
+
from .utils import (
|
19
|
+
IN_SIMULATOR, Board, BoardIdentity, BoardInfo, get_simulator_boards,
|
20
|
+
)
|
17
21
|
|
22
|
+
PathLike = Union[Path, str]
|
18
23
|
LOGGER = logging.getLogger(__name__)
|
19
24
|
|
20
25
|
robot_calibrations = calibrations.copy()
|
@@ -30,9 +35,11 @@ class AprilCamera(Board):
|
|
30
35
|
in order to determine the spatial positon and orientation of the markers
|
31
36
|
that it has detected.
|
32
37
|
|
33
|
-
:param
|
34
|
-
:param
|
38
|
+
:param camera_source: The source of the camera frames.
|
39
|
+
:param calibration: The intrinsic calibration of the camera.
|
35
40
|
:param serial_num: The serial number of the camera.
|
41
|
+
:param name: The name of the camera.
|
42
|
+
:param vidpid: The VID:PID of the camera.
|
36
43
|
"""
|
37
44
|
__slots__ = ('_serial_num', '_cam')
|
38
45
|
|
@@ -56,28 +63,82 @@ class AprilCamera(Board):
|
|
56
63
|
|
57
64
|
:return: A dict of cameras, keyed by their name and index.
|
58
65
|
"""
|
66
|
+
if IN_SIMULATOR:
|
67
|
+
return {
|
68
|
+
camera_info.serial_number: cls.from_webots_camera(camera_info)
|
69
|
+
for camera_info in get_simulator_boards('CameraBoard')
|
70
|
+
}
|
71
|
+
|
59
72
|
return {
|
60
73
|
(serial := f"{camera_data.name} - {camera_data.index}"):
|
61
|
-
cls(camera_data.index, camera_data=camera_data, serial_num=serial)
|
74
|
+
cls.from_id(camera_data.index, camera_data=camera_data, serial_num=serial)
|
62
75
|
for camera_data in find_cameras(robot_calibrations)
|
63
76
|
}
|
64
77
|
|
65
|
-
def __init__(
|
78
|
+
def __init__(
|
79
|
+
self, camera_source: FrameSource,
|
80
|
+
calibration: tuple[float, float, float, float] | None,
|
81
|
+
serial_num: str,
|
82
|
+
name: str,
|
83
|
+
vidpid: str = "",
|
84
|
+
) -> None:
|
85
|
+
# The processor handles the detection and pose estimation
|
86
|
+
self._cam = Processor(
|
87
|
+
camera_source,
|
88
|
+
calibration=calibration,
|
89
|
+
name=name,
|
90
|
+
vidpid=vidpid,
|
91
|
+
mask_unknown_size_tags=True,
|
92
|
+
)
|
93
|
+
self._serial_num = serial_num
|
94
|
+
|
95
|
+
@classmethod
|
96
|
+
def from_webots_camera(cls, camera_info: BoardInfo) -> 'AprilCamera':
|
97
|
+
"""
|
98
|
+
Create a camera from a webots camera.
|
99
|
+
|
100
|
+
:param camera_info: The information about the virtual camera,
|
101
|
+
including the url to connect to.
|
102
|
+
:return: The camera object.
|
103
|
+
"""
|
104
|
+
from .simulator.camera import WebotsRemoteCameraSource
|
105
|
+
|
106
|
+
camera_source = WebotsRemoteCameraSource(camera_info)
|
107
|
+
return cls(
|
108
|
+
camera_source,
|
109
|
+
calibration=camera_source.calibration,
|
110
|
+
serial_num=camera_info.serial_number,
|
111
|
+
name=camera_info.serial_number,
|
112
|
+
)
|
113
|
+
|
114
|
+
@classmethod
|
115
|
+
def from_id(
|
116
|
+
cls,
|
117
|
+
camera_id: int,
|
118
|
+
camera_data: CalibratedCamera,
|
119
|
+
serial_num: str,
|
120
|
+
) -> 'AprilCamera':
|
121
|
+
"""
|
122
|
+
Create a camera from an ID.
|
123
|
+
|
124
|
+
:param camera_id: The ID of the camera to create.
|
125
|
+
:param camera_data: The calibration data for the camera.
|
126
|
+
:param serial_num: The serial number of the camera.
|
127
|
+
:return: The camera object.
|
128
|
+
"""
|
66
129
|
# The camera source handles the connection between the camera and the processor
|
67
130
|
camera_source = USBCamera.from_calibration_file(
|
68
131
|
camera_id,
|
69
132
|
calibration_file=camera_data.calibration,
|
70
133
|
vidpid=camera_data.vidpid,
|
71
134
|
)
|
72
|
-
|
73
|
-
self._cam = Processor(
|
135
|
+
return cls(
|
74
136
|
camera_source,
|
75
137
|
calibration=camera_source.calibration,
|
138
|
+
serial_num=serial_num,
|
76
139
|
name=camera_data.name,
|
77
140
|
vidpid=camera_data.vidpid,
|
78
|
-
mask_unknown_size_tags=True,
|
79
141
|
)
|
80
|
-
self._serial_num = serial_num
|
81
142
|
|
82
143
|
def identify(self) -> BoardIdentity:
|
83
144
|
"""
|
@@ -103,34 +164,41 @@ class AprilCamera(Board):
|
|
103
164
|
"""
|
104
165
|
self._cam.close()
|
105
166
|
|
106
|
-
def see(
|
167
|
+
def see(
|
168
|
+
self,
|
169
|
+
*,
|
170
|
+
frame: Optional[NDArray] = None,
|
171
|
+
save: Optional[PathLike] = None,
|
172
|
+
) -> List[Marker]:
|
107
173
|
"""
|
108
174
|
Capture an image and identify fiducial markers.
|
109
175
|
|
110
176
|
:param frame: An image to detect markers in, instead of capturing a new one,
|
177
|
+
:param save: If given, save the annotated frame to the path.
|
178
|
+
This is given a JPEG extension if none is provided.
|
111
179
|
:returns: list of markers that the camera could see.
|
112
180
|
"""
|
181
|
+
if frame is None:
|
182
|
+
frame = self._cam.capture()
|
183
|
+
|
113
184
|
markers = self._cam.see(frame=frame)
|
185
|
+
|
186
|
+
if save:
|
187
|
+
self._cam.save(save, frame=frame, detections=markers)
|
114
188
|
return [Marker.from_april_vision_marker(marker) for marker in markers]
|
115
189
|
|
116
|
-
def capture(self) -> NDArray:
|
190
|
+
def capture(self, *, save: Optional[PathLike] = None) -> NDArray:
|
117
191
|
"""
|
118
192
|
Get the raw image data from the camera.
|
119
193
|
|
194
|
+
:param save: If given, save the annotated frame to the path.
|
195
|
+
This is given a JPEG extension if none is provided.
|
120
196
|
:returns: Camera pixel data
|
121
197
|
"""
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
Save an annotated image to a path.
|
127
|
-
|
128
|
-
:param path: The path to save the image to,
|
129
|
-
this is given a JPEG extension if none is provided.
|
130
|
-
:param frame: An image to annotate and save, instead of capturing a new one,
|
131
|
-
defaults to None
|
132
|
-
"""
|
133
|
-
self._cam.save(path, frame=frame)
|
198
|
+
raw_frame = self._cam.capture()
|
199
|
+
if save:
|
200
|
+
self._cam.save(save, frame=raw_frame, annotated=False)
|
201
|
+
return raw_frame
|
134
202
|
|
135
203
|
def _set_marker_sizes(
|
136
204
|
self,
|