Skip to content

Commit 3e8e08c

Browse files
committed
connections: Add USB file upload.
This is used for installing Pybricks on SPIKE hubs without DFU. If no hub in serial mode is detected, it will still proceed with DFU as it did before.
1 parent f51face commit 3e8e08c

File tree

2 files changed

+119
-5
lines changed

2 files changed

+119
-5
lines changed

pybricksdev/cli/__init__.py

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
from tempfile import NamedTemporaryFile
1313
from typing import ContextManager, TextIO
1414
import validators
15+
import zipfile
1516

1617
from abc import ABC, abstractmethod
1718
from os import PathLike, path
@@ -229,8 +230,39 @@ async def run(self, args: argparse.Namespace):
229230

230231
if metadata["device-id"] in (HubKind.TECHNIC_SMALL, HubKind.TECHNIC_LARGE):
231232
from ..dfu import flash_dfu
233+
from ..connections import REPLHub
232234

233-
flash_dfu(firmware, metadata)
235+
try:
236+
# Connect to the hub and exit the runtime.
237+
hub = REPLHub()
238+
await hub.connect()
239+
await hub.reset_hub()
240+
241+
# Upload installation script.
242+
archive = zipfile.ZipFile(args.firmware)
243+
await hub.exec_line("import uos; uos.mkdir('_firmware')")
244+
await hub.upload_file(
245+
"_firmware/install_pybricks.py",
246+
bytearray(archive.open("install_pybricks.py").read()),
247+
)
248+
249+
# Upload metadata.
250+
await hub.upload_file(
251+
"_firmware/firmware.metadata.json",
252+
bytearray(archive.open("firmware.metadata.json").read()),
253+
)
254+
255+
# Upload Pybricks firmware
256+
await hub.upload_file("_firmware/firmware.bin", firmware)
257+
258+
# Run installation script
259+
print("Installing firmware")
260+
await hub.exec_line("from _firmware.install_pybricks import install")
261+
await hub.exec_paste_mode("install()")
262+
263+
except OSError:
264+
print("Could not find hub in standard firmware mode. Trying DFU.")
265+
flash_dfu(firmware, metadata)
234266
else:
235267
from ..ble import find_device
236268
from ..flash import BootloaderConnection

pybricksdev/connections.py

Lines changed: 86 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -381,6 +381,47 @@ async def run(self, py_path, wait=True, print_output=True):
381381
await asyncio.sleep(0.3)
382382

383383

384+
FILE_PACKET_SIZE = 1024
385+
FILE_TRANSFER_SCRIPT = f"""
386+
import sys
387+
import micropython
388+
import utime
389+
390+
PACKETSIZE = {FILE_PACKET_SIZE}
391+
392+
def receive_file(filename, filesize):
393+
394+
micropython.kbd_intr(-1)
395+
396+
with open(filename, "wb") as f:
397+
398+
# Initialize buffers
399+
done = 0
400+
buf = bytearray(PACKETSIZE)
401+
sys.stdin.buffer.read(1)
402+
403+
while done < filesize:
404+
405+
# Size of last package
406+
if filesize - done < PACKETSIZE:
407+
buf = bytearray(filesize - done)
408+
409+
# Read one packet from standard in.
410+
time_now = utime.ticks_ms()
411+
bytes_read = sys.stdin.buffer.readinto(buf)
412+
413+
# If transmission took a long time, something bad happened.
414+
if utime.ticks_ms() - time_now > 5000:
415+
print("transfer timed out")
416+
return
417+
418+
# Write the data and say we're ready for more.
419+
f.write(buf)
420+
done += bytes_read
421+
print("ACK")
422+
"""
423+
424+
384425
class REPLHub:
385426
"""Run scripts on generic MicroPython boards with a REPL over USB."""
386427

@@ -455,6 +496,12 @@ async def reset_hub(self):
455496
# Clear all buffers
456497
self.reset_buffers()
457498

499+
# Load file transfer function
500+
await self.exec_paste_mode(FILE_TRANSFER_SCRIPT, print_output=False)
501+
self.reset_buffers()
502+
503+
print("Hub is ready.")
504+
458505
async def exec_line(self, line, wait=True):
459506
"""Executes one line on the REPL."""
460507

@@ -467,10 +514,6 @@ async def exec_line(self, line, wait=True):
467514
echo = encoded + b"\r\n"
468515
self.serial.write(echo)
469516

470-
# We are done if we don't want to wait for the result.
471-
if not wait:
472-
return
473-
474517
# Wait until the echo has been read.
475518
while len(self.buffer) < start_len + len(echo):
476519
await asyncio.sleep(0.05)
@@ -480,6 +523,10 @@ async def exec_line(self, line, wait=True):
480523
print(start_len, self.buffer, self.buffer[start_len - 1 :], echo)
481524
raise ValueError("Failed to execute line: {0}.".format(line))
482525

526+
# We are done if we don't want to wait for the result.
527+
if not wait:
528+
return
529+
483530
# Wait for MicroPython to execute the command.
484531
while not self.is_idle():
485532
await asyncio.sleep(0.1)
@@ -556,3 +603,38 @@ async def run(self, py_path, wait=True, print_output=True):
556603
self.script_dir, _ = os.path.split(py_path)
557604
await self.reset_hub()
558605
await self.exec_paste_mode(script, wait, print_output)
606+
607+
async def upload_file(self, destination, contents):
608+
"""Uploads a file to the hub."""
609+
610+
# Print upload info.
611+
size = len(contents)
612+
print(f"Uploading {destination} ({size} bytes)")
613+
self.reset_buffers()
614+
615+
# Prepare hub to receive file
616+
await self.exec_line(f"receive_file('{destination}', {size})", wait=False)
617+
618+
ACK = b"ACK" + self.EOL
619+
progress = 0
620+
621+
# Write file chunk by chunk.
622+
for data in chunk(contents, FILE_PACKET_SIZE):
623+
624+
# Send a chunk and wait for acknowledgement of receipt
625+
buffer_now = len(self.buffer)
626+
progress += self.serial.write(data)
627+
while len(self.buffer) < buffer_now + len(ACK):
628+
await asyncio.sleep(0.01)
629+
self.parse_input()
630+
631+
# Raise error if we didn't get acknowledgement
632+
if self.buffer[buffer_now : buffer_now + len(ACK)] != ACK:
633+
print(self.buffer[buffer_now:])
634+
raise ValueError("Did not get expected response from the hub.")
635+
636+
# Print progress
637+
print(f"Progress: {int(progress / size * 100)}%", end="\r")
638+
639+
# Get REPL back in normal state
640+
await self.exec_line("# File transfer complete")

0 commit comments

Comments
 (0)