Skip to content

Commit 078432b

Browse files
committed
pybricksdev.connections.pybricks: add support for Pybricks Profile v1.3.0
This adds support for changes in the firmware for Pybricks Profile v1.3.0.
1 parent 28ec0f6 commit 078432b

File tree

4 files changed

+73
-26
lines changed

4 files changed

+73
-26
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
66

77
## [Unreleased]
88

9+
### Added
10+
- Added support for Pybricks Profile v1.3.0.
911
## [1.0.0-alpha.42] - 2023-04-12
1012

1113
### Fixed

pybricksdev/ble/pybricks.py

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,16 @@ class Command(IntEnum):
124124
.. availability:: Since Pybricks protocol v1.2.0.
125125
"""
126126

127+
WRITE_STDIN = 6
128+
"""
129+
Requests to write to stdin on the hub.
130+
131+
Parameters:
132+
payload: Variable number of bytes to write (0 to ``max_char_size`` bytes).
133+
134+
.. availability:: Since Pybricks protocol v1.3.0.
135+
"""
136+
127137

128138
class CommandError(IntEnum):
129139
"""
@@ -170,14 +180,23 @@ class Event(IntEnum):
170180
"""
171181

172182
STATUS_REPORT = 0
173-
"""Status report.
183+
"""Status report event.
174184
175185
The payload is a 32-bit little-endian unsigned integer containing
176186
:class:`StatusFlag` flags.
177187
178188
.. availability:: Since Pybricks protocol v1.0.0.
179189
"""
180190

191+
WRITE_STDOUT = 1
192+
"""Hub write to stdout event.
193+
194+
The payload is a variable number of bytes that was written to stdout by
195+
the hub.
196+
197+
.. availability:: Since Pybricks protocol v1.3.0.
198+
"""
199+
181200

182201
class StatusFlag(IntFlag):
183202
"""Hub status indicators."""

pybricksdev/connections/lego.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -161,7 +161,7 @@ async def exec_line(self, line, wait=True):
161161
while not self.is_idle():
162162
await asyncio.sleep(0.1)
163163

164-
line_handler = PybricksHub.line_handler
164+
line_handler = PybricksHub._line_handler
165165

166166
async def exec_paste_mode(self, code, wait=True, print_output=True):
167167
"""Executes commands via paste mode."""

pybricksdev/connections/pybricks.py

Lines changed: 50 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -81,14 +81,21 @@ def __init__(self):
8181
self.connection_state_observable = BehaviorSubject(ConnectionState.DISCONNECTED)
8282
self.status_observable = BehaviorSubject(StatusFlag(0))
8383
self.nus_observable = Subject()
84-
self.stream_buf = bytearray()
85-
self.output = []
8684
self.print_output = True
8785
self.fw_version = None
8886
self._mpy_abi_version = 0
8987
self._capability_flags = HubCapabilityFlag(0)
9088
self._max_user_program_size = 0
9189

90+
# buffered stdout from the hub for splitting into lines
91+
self._stdout_buf = bytearray()
92+
93+
# REVISIT: this can potentially waste a lot of RAM if not drained
94+
self._stdout_line_queue = asyncio.Queue()
95+
96+
# prior to Pybricks Profile v1.3.0, NUS was used for stdio
97+
self._legacy_stdio = False
98+
9299
# indicates is we are currently downloading a program over NUS (legacy download)
93100
self._downloading_via_nus = False
94101

@@ -98,13 +105,13 @@ def __init__(self):
98105
# File handle for logging
99106
self.log_file = None
100107

101-
def line_handler(self, line):
102-
"""Handles new incoming lines. Handle special actions if needed,
108+
def _line_handler(self, line: bytes) -> None:
109+
"""
110+
Handles new incoming lines. Handle special actions if needed,
103111
otherwise just print it as regular lines.
104112
105113
Arguments:
106-
line (bytearray):
107-
Line to process.
114+
line: Line to process.
108115
"""
109116

110117
# The line tells us to open a log file, so do it.
@@ -134,45 +141,60 @@ def line_handler(self, line):
134141
self.log_file = None
135142
return
136143

144+
line_str = line.decode()
145+
137146
# If we are processing datalog, save current line to the open file.
138147
if self.log_file is not None:
139-
print(line.decode(), file=self.log_file)
148+
print(line_str, file=self.log_file)
140149
return
141150

142-
# If there is nothing special about this line, print it if requested.
143-
self.output.append(line)
144151
if self.print_output:
145-
print(line.decode())
152+
print(line_str)
153+
return
146154

147-
def nus_handler(self, sender, data):
148-
self.nus_observable.on_next(data)
155+
self._stdout_line_queue.put_nowait(line_str)
149156

150-
# Store incoming data
151-
if not self._downloading_via_nus:
152-
self.stream_buf += data
153-
logger.debug("NUS DATA: {0}".format(data))
157+
def _handle_line_data(self, data: bytes) -> None:
158+
self._stdout_buf.extend(data)
154159

155160
# Break up data into lines and take those out of the buffer
156161
lines = []
157162
while True:
158163
# Find and split at end of line
159-
index = self.stream_buf.find(self.EOL)
164+
index = self._stdout_buf.find(self.EOL)
160165
# If no more line end is found, we are done
161166
if index < 0:
162167
break
163168
# If we found a line, save it, and take it from the buffer
164-
lines.append(self.stream_buf[0:index])
165-
del self.stream_buf[0 : index + len(self.EOL)]
169+
lines.append(self._stdout_buf[:index])
170+
del self._stdout_buf[: index + len(self.EOL)]
166171

167172
# Call handler for each line that we found
168173
for line in lines:
169-
self.line_handler(line)
174+
self._line_handler(line)
170175

171-
def pybricks_service_handler(self, _: int, data: bytes) -> None:
176+
def _nus_handler(self, sender, data: bytearray) -> None:
177+
self.nus_observable.on_next(data)
178+
179+
# legacy firmware may use NUS for download and run, in which case
180+
# we need to ignore the incoming data
181+
if self._downloading_via_nus:
182+
return
183+
184+
logger.debug("NUS DATA: %r", data)
185+
186+
# support legacy firmware where the Nordic UART service
187+
# was used for stdio
188+
if self._legacy_stdio:
189+
self._handle_line_data(data)
190+
191+
def _pybricks_service_handler(self, _: int, data: bytes) -> None:
172192
if data[0] == Event.STATUS_REPORT:
173193
# decode the payload
174194
(flags,) = struct.unpack_from("<I", data, 1)
175195
self.status_observable.on_next(StatusFlag(flags))
196+
elif data[0] == Event.WRITE_STDOUT:
197+
self._handle_line_data(data[1:])
176198

177199
async def connect(self, device: BLEDevice):
178200
"""Connects to a device that was discovered with :meth:`pybricksdev.ble.find_device`
@@ -242,9 +264,12 @@ def handle_disconnect(_: BleakClient):
242264
6 if self.fw_version >= Version("3.2.0b2") else 5
243265
)
244266

245-
await self.client.start_notify(NUS_TX_UUID, self.nus_handler)
267+
if protocol_version < "1.3.0":
268+
self._legacy_stdio = True
269+
270+
await self.client.start_notify(NUS_TX_UUID, self._nus_handler)
246271
await self.client.start_notify(
247-
PYBRICKS_COMMAND_EVENT_UUID, self.pybricks_service_handler
272+
PYBRICKS_COMMAND_EVENT_UUID, self._pybricks_service_handler
248273
)
249274

250275
self.connection_state_observable.on_next(ConnectionState.CONNECTED)
@@ -327,7 +352,8 @@ async def run(
327352

328353
# Reset output buffer
329354
self.log_file = None
330-
self.output = []
355+
self._stdout_buf.clear()
356+
self._stdout_line_queue = asyncio.Queue()
331357
self.print_output = print_output
332358
self.script_dir, _ = os.path.split(py_path)
333359

0 commit comments

Comments
 (0)