Skip to content

Commit 81f310d

Browse files
committed
connections.ev3: Improve firmware update.
- Allow faster flashing of smaller firmwares by erasing only as much as needed. - Work around USB3.0 issues with the EV3 bootloader.
1 parent 5e2a361 commit 81f310d

File tree

2 files changed

+52
-30
lines changed

2 files changed

+52
-30
lines changed

pybricksdev/cli/flash.py

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -382,24 +382,28 @@ async def flash_ev3(firmware: bytes) -> None:
382382
fw, hw = await bootloader.get_version()
383383
print(f"hwid: {hw}")
384384

385-
ERASE_TICKS = 60
386-
387385
# Erasing doesn't have any feedback so we just use time for the progress
388386
# bar. The operation runs on the EV3, so the time is the same for everyone.
389387
async def tick(callback):
390-
for _ in range(ERASE_TICKS):
391-
await asyncio.sleep(1)
392-
callback(1)
388+
CHUNK = 8000
389+
SPEED = 256000
390+
for _ in range(len(firmware) // CHUNK):
391+
await asyncio.sleep(CHUNK / SPEED)
392+
callback(CHUNK)
393393

394-
print("Erasing memory...")
395-
with logging_redirect_tqdm(), tqdm(total=ERASE_TICKS) as pbar:
396-
await asyncio.gather(bootloader.erase_chip(), tick(pbar.update))
394+
print("Erasing memory and preparing firmware download...")
395+
with logging_redirect_tqdm(), tqdm(
396+
total=len(firmware), unit="B", unit_scale=True
397+
) as pbar:
398+
await asyncio.gather(
399+
bootloader.erase_and_begin_download(len(firmware)), tick(pbar.update)
400+
)
397401

398402
print("Downloading firmware...")
399403
with logging_redirect_tqdm(), tqdm(
400404
total=len(firmware), unit="B", unit_scale=True
401405
) as pbar:
402-
await bootloader.download(0, firmware, pbar.update)
406+
await bootloader.download(firmware, pbar.update)
403407

404408
print("Verifying...", end="", flush=True)
405409
checksum = await bootloader.get_checksum(0, len(firmware))

pybricksdev/connections/ev3.py

Lines changed: 39 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -106,13 +106,20 @@ def _send_command(self, command: Command, payload: Optional[bytes] = None) -> in
106106

107107
return message_number
108108

109-
def _receive_reply(self, command: Command, message_number: int) -> bytes:
109+
def _receive_reply(
110+
self, command: Command, message_number: int, force_length: int = 0
111+
) -> bytes:
110112
"""
111113
Receive a reply from the EV3 bootloader.
112114
113115
Args:
114116
command: The command that was sent.
115117
message_number: The return value of :meth:`_send_command`.
118+
force_length: Expected length, used only when it fails to unpack
119+
normally. Some replies on USB 3.0 hosts contain
120+
the original command written over the reply. This
121+
means the header is bad, but the payload may be in
122+
tact if you know what data to expect.
116123
117124
Returns:
118125
The payload of the reply.
@@ -131,36 +138,41 @@ def _receive_reply(self, command: Command, message_number: int) -> bytes:
131138
raise ReplyError(status)
132139

133140
if message_type != MessageType.SYSTEM_REPLY:
134-
raise RuntimeError("unexpected message type: {message_type}")
141+
if force_length:
142+
return reply[7 : force_length + 2]
143+
raise RuntimeError(f"unexpected message type: {message_type}")
135144

136145
if reply_command != command:
137-
raise RuntimeError("command mismatch: {reply_command} != {command}")
146+
raise RuntimeError(f"command mismatch: {reply_command} != {command}")
138147

139148
return reply[7 : length + 2]
140149

141150
def download_sync(
142151
self,
143-
address: int,
144152
data: bytes,
145153
progress: Optional[Callable[[int], None]] = None,
146154
) -> None:
147155
"""
148156
Blocking version of :meth:`download`.
149157
"""
150-
param_data = struct.pack("<II", address, len(data))
151-
num = self._send_command(Command.BEGIN_DOWNLOAD, param_data)
152-
self._receive_reply(Command.BEGIN_DOWNLOAD, num)
153158

159+
completed = 0
154160
for c in chunk(data, self._MAX_DATA_SIZE):
155161
num = self._send_command(Command.DOWNLOAD_DATA, c)
156-
self._receive_reply(Command.DOWNLOAD_DATA, num)
162+
try:
163+
completed += len(c)
164+
self._receive_reply(Command.DOWNLOAD_DATA, num)
165+
except RuntimeError as e:
166+
# Allow exception only on the final chunk.
167+
if completed != len(data):
168+
raise e
169+
print(e, ". Proceeding anyway.")
157170

158171
if progress:
159172
progress(len(c))
160173

161174
async def download(
162175
self,
163-
address: int,
164176
data: bytes,
165177
progress: Optional[Callable[[int], None]] = None,
166178
) -> None:
@@ -170,30 +182,31 @@ async def download(
170182
This operation takes about 60 seconds for a full 16MB firmware file.
171183
172184
Args:
173-
address: The starting address of where to write the data.
174185
data: The data to write.
175186
progress: Optional callback for indicating progress.
176187
"""
177188
return await asyncio.get_running_loop().run_in_executor(
178-
None, self.download_sync, address, data, progress
189+
None, self.download_sync, data, progress
179190
)
180191

181-
def erase_chip_sync(self) -> None:
192+
def erase_and_begin_download_sync(self, size) -> None:
182193
"""
183-
Blocking version of :meth:`erase_chip`.
194+
Blocking version of :meth:`erase_and_begin_download`.
184195
"""
185-
num = self._send_command(Command.CHIP_ERASE)
186-
self._receive_reply(Command.CHIP_ERASE, num)
196+
param_data = struct.pack("<II", 0, size)
197+
num = self._send_command(Command.BEGIN_DOWNLOAD_WITH_ERASE, param_data)
198+
self._receive_reply(Command.BEGIN_DOWNLOAD_WITH_ERASE, num)
187199

188-
async def erase_chip(self) -> None:
200+
async def erase_and_begin_download(self, size) -> None:
189201
"""
190-
Erases the external flash memory chip.
202+
Erases the external flash memory chip by the amount required to
203+
flash the new firmware. Also prepares firmware download.
191204
192-
This operation takes about 60 seconds.
205+
Args:
206+
size: How much to erase.
193207
"""
194208
return await asyncio.get_running_loop().run_in_executor(
195-
None,
196-
self.erase_chip_sync,
209+
None, self.erase_and_begin_download_sync, size
197210
)
198211

199212
def start_app_sync(self) -> None:
@@ -241,7 +254,12 @@ def get_version_sync(self) -> Tuple[int, int]:
241254
Blocking version of :meth:`get_version`.
242255
"""
243256
num = self._send_command(Command.GET_VERSION)
244-
payload = self._receive_reply(Command.GET_VERSION, num)
257+
# On certain USB 3.0 systems, the brick reply contains the command
258+
# we just sent written over it. This means we don't get the correct
259+
# header and length info. Since the command here is smaller than the
260+
# reply, the paypload does not get overwritten, so we can still get
261+
# the version info since we know the expected reply size.
262+
payload = self._receive_reply(Command.GET_VERSION, num, force_length=13)
245263
return struct.unpack("<II", payload)
246264

247265
async def get_version(self) -> Tuple[int, int]:

0 commit comments

Comments
 (0)