Skip to content

Commit ca94b4c

Browse files
committed
connections: fix ble download and run
This fixes several race conditions caused by firmware changes.
1 parent 3f48d34 commit ca94b4c

File tree

3 files changed

+56
-44
lines changed

3 files changed

+56
-44
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,10 @@ All notable changes to this project will be documented in this file.
44
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
55
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
66

7+
## [Unreleased]
8+
### Fixed
9+
- Fix running programs via Bluetooth Low Energy.
10+
711
## [1.0.0-alpha.1] - 2021-04-07
812
### Added
913
- This changelog.

pybricksdev/cli/__init__.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -150,8 +150,10 @@ async def run(self, args: argparse.Namespace):
150150

151151
# Connect to the address and run the script
152152
await hub.connect(device_or_address)
153-
await hub.run(script_path, args.wait == 'True')
154-
await hub.disconnect()
153+
try:
154+
await hub.run(script_path, args.wait == 'True')
155+
finally:
156+
await hub.disconnect()
155157

156158

157159
class Flash(Tool):

pybricksdev/connections.py

Lines changed: 48 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -559,7 +559,8 @@ async def get(self, remote_path, local_path=None):
559559
NUS_TX_UUID = '6e400003-b5a3-f393-e0a9-e50e24dcca9e'
560560

561561

562-
class PybricksHub():
562+
class PybricksHub:
563+
EOL = b"\r\n" # MicroPython EOL
563564

564565
def __init__(self):
565566
self.logger = logging.getLogger('Pybricks Hub')
@@ -571,26 +572,33 @@ def __init__(self):
571572
self.logger.addHandler(handler)
572573
self.logger.setLevel(logging.WARNING)
573574

574-
self.EOL = b"\r\n"
575575
self.stream_buf = bytearray()
576576
self.output = []
577577
self.print_output = True
578578

579+
# indicates that the hub is currently connected via BLE
580+
self.connected = False
581+
582+
# stores the next expected checksum or -1 when not expecting a checksum
579583
self.expected_checksum = -1
584+
# indicates when a valid checksum was received
580585
self.checksum_ready = asyncio.Event()
581586

587+
# indicates is we are currently downloading a program
588+
self.loading = False
589+
# indicates that the user program is running
582590
self.program_running = False
583-
self.program_state_changed = asyncio.Event()
591+
# used to notify when the user program has ended
592+
self.user_program_stopped = asyncio.Event()
584593

585594
def line_handler(self, line):
586595
self.output.append(line)
587596
if self.print_output:
588597
print(line.decode())
589598

590599
def nus_handler(self, sender, data):
591-
592-
# If no program is running, read checksum bytes.
593-
if not self.program_running and self.expected_checksum >= 0:
600+
# If we are currently downloading a program, treat incoming data as checksum.
601+
if self.loading:
594602
if data[0] == self.expected_checksum:
595603
self.checksum_ready.set()
596604
self.logger.debug("Correct checksum: {0}".format(data[0]))
@@ -621,20 +629,25 @@ def nus_handler(self, sender, data):
621629
for line in lines:
622630
self.line_handler(line)
623631

624-
def pybricks_service_handler(self, sender, data):
632+
def pybricks_service_handler(self, _: int, data: bytearray):
625633
if data[0] == 0:
626634

627635
# Get new state
628636
msg = data[1]
629637

630638
# Get new program state
631639
program_running_now = bool(msg & (1 << 6))
632-
self.logger.info("Program running: " + str(program_running_now))
633640

634-
# If program state changed, notifiy
635-
if self.program_running != program_running_now:
636-
self.program_state_changed.set()
637-
self.program_running = program_running_now
641+
# If we are currently downloading a program, we must ignore user
642+
# program running state changes, otherwise the checksum will be
643+
# sent to the terminal instead of being handled by the download
644+
# algorithm
645+
if not self.loading:
646+
if self.program_running != program_running_now:
647+
self.logger.info(f"Program running: {program_running_now}")
648+
self.program_running = program_running_now
649+
if not program_running_now:
650+
self.user_program_stopped.set()
638651

639652
def disconnected_handler(self, client: BleakClient):
640653
self.logger.info("Disconnected!")
@@ -649,11 +662,12 @@ async def connect(self, device):
649662
self.connected = True
650663

651664
async def disconnect(self):
652-
await self.client.stop_notify(NUS_TX_UUID)
653-
await self.client.stop_notify(PYBRICKS_UUID)
654665
if self.connected:
655666
self.logger.info("Disconnecting...")
656667
await self.client.disconnect()
668+
self.connected = False
669+
else:
670+
self.logger.debug("already disconnected")
657671

658672
def get_checksum(self, block):
659673
checksum = 0
@@ -664,13 +678,11 @@ def get_checksum(self, block):
664678
async def send_block(self, data):
665679
self.checksum_ready.clear()
666680
self.expected_checksum = self.get_checksum(data)
667-
await self.client.write_gatt_char(NUS_RX_UUID, bytearray(data), False)
668681
try:
682+
await self.client.write_gatt_char(NUS_RX_UUID, bytearray(data), False)
669683
await asyncio.wait_for(self.checksum_ready.wait(), timeout=0.5)
670-
except asyncio.TimeoutError:
671-
self.logger.warning("Error during program download.")
672-
return
673-
self.expected_checksum = -1
684+
finally:
685+
self.expected_checksum = -1
674686

675687
async def run(self, py_path, wait=True, print_output=True):
676688

@@ -681,31 +693,25 @@ async def run(self, py_path, wait=True, print_output=True):
681693
# Compile the script to mpy format
682694
mpy = await compile_file(py_path)
683695

684-
# Get length of file and send it as bytes to hub
685-
length = len(mpy).to_bytes(4, byteorder='little')
686-
await self.send_block(length)
696+
try:
697+
self.loading = True
698+
self.user_program_stopped.clear()
687699

688-
# Divide script in chunks of bytes
689-
n = 100
690-
chunks = [mpy[i: i + n] for i in range(0, len(mpy), n)]
700+
# Get length of file and send it as bytes to hub
701+
length = len(mpy).to_bytes(4, byteorder='little')
702+
await self.send_block(length)
691703

692-
# Send the data chunk by chunk
693-
print("Downloading {0} bytes in {1} steps.".format(len(mpy), len(chunks)))
694-
for i, chunk in enumerate(chunks):
695-
print("Progress: {0}%".format(
696-
round((i + 1) / len(chunks) * 100))
697-
)
698-
await self.send_block(chunk)
704+
# Divide script in chunks of bytes
705+
n = 100
706+
chunks = [mpy[i: i + n] for i in range(0, len(mpy), n)]
699707

700-
# Wait for program to start.
701-
try:
702-
await asyncio.wait_for(self.program_state_changed.wait(), timeout=0.5)
703-
self.program_state_changed.clear()
704-
except asyncio.TimeoutError:
705-
self.logger.warning("Unable to start program.")
706-
return
708+
# Send the data chunk by chunk
709+
print(f"Downloading {len(mpy)} bytes in {len(chunks)} steps.")
710+
for i, chunk in enumerate(chunks):
711+
print(f"Progress: {round((i + 1) / len(chunks) * 100)}%")
712+
await self.send_block(chunk)
713+
finally:
714+
self.loading = False
707715

708716
if wait:
709-
# Wait for program to stop
710-
await self.program_state_changed.wait()
711-
self.program_state_changed.clear()
717+
await self.user_program_stopped.wait()

0 commit comments

Comments
 (0)