Skip to content

Commit 5700e11

Browse files
committed
pybricksdev.cli: add support for flashing NXT bricks
This adds support to the `pybricksdev flash` command for flashing firmware to LEGO MINDSTORMS NXT bricks. Support for firmware metadata v2.1.0 is added to enable this.
1 parent ad6cffa commit 5700e11

File tree

4 files changed

+92
-9
lines changed

4 files changed

+92
-9
lines changed

CHANGELOG.md

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

99
### Added
1010
- Added `pybricksdev.ble.pybricks.Command.PBIO_PYBRICKS_COMMAND_REBOOT_TO_UPDATE_MODE`.
11+
- Added support for Pybricks firmware metadata v2.1.0.
12+
- Added support for flashing firmware to LEGO MINDSTORMS NXT bricks.
1113

1214
### Fixed
1315
- Fixed reboot in update mode for Pybricks Profile >= 1.2.0 in `pybricksdev flash` CLI.

pybricksdev/ble/lwp3/bytecodes.py

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
# SPDX-License-Identifier: MIT
2-
# Copyright (c) 2021 The Pybricks Authors
2+
# Copyright (c) 2021-2023 The Pybricks Authors
33
# Some portions of the documentation:
44
# Copyright (c) 2018 LEGO System A/S
55

@@ -162,6 +162,9 @@ class SystemKind(IntEnum):
162162
TECHNIC = 4 << 5
163163
"""LEGO Technic"""
164164

165+
LEGACY = 7 << 5
166+
"""Pre-Powered Up (unofficial Pybricks addition)"""
167+
165168

166169
@unique
167170
class HubKind(IntEnum):
@@ -201,6 +204,15 @@ class HubKind(IntEnum):
201204
TECHNIC_SMALL = 0x83
202205
"""LEGO SPIKE Essential Hub."""
203206

207+
RCX = 0xE0
208+
"""LEGO MINDSTORMS RCX brick. (unofficial Pybricks addition)"""
209+
210+
NXT = 0xE1
211+
"""LEGO MINDSTORMS NXT brick. (unofficial Pybricks addition)"""
212+
213+
EV3 = 0xE2
214+
"""LEGO MINDSTORMS EV3 brick. (unofficial Pybricks addition)"""
215+
204216
@property
205217
def system(self) -> SystemKind:
206218
"""

pybricksdev/cli/flash.py

Lines changed: 47 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
# SPDX-License-Identifier: MIT
2-
# Copyright (c) 2019-2022 The Pybricks Authors
2+
# Copyright (c) 2019-2023 The Pybricks Authors
33

44
import asyncio
55
import hashlib
@@ -312,9 +312,49 @@ async def flash_ble(hub_kind: HubKind, firmware: bytes, metadata: dict):
312312
await updater.flash(firmware, metadata)
313313

314314

315+
async def flash_nxt(firmware: bytes) -> None:
316+
"""
317+
Flashes firmware to NXT using the Samba bootloader.
318+
319+
Args:
320+
firmware:
321+
A firmware blob with the NxOS header appended to the end.
322+
"""
323+
from .._vendored.pynxt.samba import SambaBrick, SambaOpenError
324+
from .._vendored.pynxt.firmware import Firmware
325+
from .._vendored.pynxt.flash import FlashController
326+
327+
# parse the header
328+
info = Firmware(firmware)
329+
330+
if info.samba:
331+
raise ValueError("Firmware is not suitable for flashing.")
332+
333+
s = SambaBrick()
334+
335+
try:
336+
print("Looking for the NXT in SAM-BA mode...")
337+
s.open(timeout=5)
338+
print("Brick found!")
339+
except SambaOpenError as e:
340+
print(e)
341+
sys.exit(1)
342+
343+
print("Flashing firmware...")
344+
f = FlashController(s)
345+
f.flash(firmware)
346+
347+
print("Flashing complete, jumping to 0x100000...")
348+
f._wait_for_flash()
349+
s.jump(0x100000)
350+
351+
print("Firmware started.")
352+
s.close()
353+
354+
315355
async def flash_firmware(firmware_zip: BinaryIO, new_name: Optional[str]) -> None:
316356
"""
317-
Command line tool for flasing firmware.
357+
Command line tool for flashing firmware.
318358
319359
Args:
320360
firmware_zip: The path to the ``firmware.zip`` file.
@@ -364,5 +404,9 @@ async def flash_firmware(firmware_zip: BinaryIO, new_name: Optional[str]) -> Non
364404
except OSError:
365405
print("Could not find hub in standard firmware mode. Trying DFU.")
366406
flash_dfu(firmware, metadata)
367-
else:
407+
elif hub_kind in [HubKind.BOOST, HubKind.CITY, HubKind.TECHNIC]:
368408
await flash_ble(hub_kind, firmware, metadata)
409+
elif hub_kind == HubKind.NXT:
410+
await flash_nxt(firmware)
411+
else:
412+
raise ValueError(f"unsupported hub kind: {hub_kind}")

pybricksdev/firmware.py

Lines changed: 30 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
# SPDX-License-Identifier: MIT
2-
# Copyright (c) 2022 The Pybricks Authors
2+
# Copyright (c) 2022-2023 The Pybricks Authors
33

44
"""
55
Utilities for working with Pybricks ``firmware.zip`` files.
@@ -15,9 +15,9 @@
1515

1616

1717
if sys.version_info < (3, 10):
18-
from typing_extensions import TypeGuard, TypeAlias
18+
from typing_extensions import TypeGuard
1919
else:
20-
from typing import TypeGuard, TypeAlias
20+
from typing import TypeGuard
2121

2222
import semver
2323

@@ -69,7 +69,7 @@ class FirmwareMetadataV200(
6969
{
7070
"metadata-version": Literal["2.0.0"],
7171
"firmware-version": str,
72-
"device-id": Literal[0x40, 0x41, 0x80, 0x81, 83],
72+
"device-id": Literal[0x40, 0x41, 0x80, 0x81, 0x83],
7373
"checksum-type": Literal["sum", "crc32"],
7474
"checksum-size": int,
7575
"hub-name-offset": int,
@@ -82,12 +82,29 @@ class FirmwareMetadataV200(
8282
"""
8383

8484

85+
class FirmwareMetadataV210(
86+
FirmwareMetadataV200,
87+
TypedDict(
88+
"V210",
89+
{
90+
# changed
91+
"metadata-version": Literal["2.1.0"],
92+
"device-id": Literal[0x40, 0x41, 0x80, 0x81, 0x83, 0xE0, 0xE1, 0xE2],
93+
"checksum-type": Literal["sum", "crc32", "none"],
94+
},
95+
),
96+
):
97+
"""
98+
Type for data contained in v2.1.0 ``firmware.metadata.json`` files.
99+
"""
100+
101+
85102
AnyFirmwareV1Metadata = Union[FirmwareMetadataV100, FirmwareMetadataV110]
86103
"""
87104
Type for data contained in ``firmware.metadata.json`` files of any 1.x version.
88105
"""
89106

90-
AnyFirmwareV2Metadata: TypeAlias = FirmwareMetadataV200
107+
AnyFirmwareV2Metadata = Union[FirmwareMetadataV200, FirmwareMetadataV210]
91108
"""
92109
Type for data contained in ``firmware.metadata.json`` files of any 2.x version.
93110
"""
@@ -180,6 +197,9 @@ async def _create_firmware_v2(
180197

181198
# Update hub name if given
182199
if name:
200+
if not metadata["hub-name-offset"]:
201+
raise ValueError("this firmware does not support changing the hub name")
202+
183203
name = name.encode() + b"\0"
184204

185205
max_size = metadata["hub-name-size"]
@@ -197,6 +217,11 @@ async def _create_firmware_v2(
197217
checksum = sum_complement(io.BytesIO(firmware), metadata["checksum-size"])
198218
elif metadata["checksum-type"] == "crc32":
199219
checksum = crc32_checksum(io.BytesIO(firmware), metadata["checksum-size"])
220+
elif (
221+
semver.compare(metadata["metadata-version"], "2.1.0") >= 0
222+
and metadata["checksum-type"] == "none"
223+
):
224+
return firmware
200225
else:
201226
raise ValueError(f"unsupported checksum type: {metadata['checksum-type']}")
202227

0 commit comments

Comments
 (0)