Skip to content

Commit 21857aa

Browse files
authored
Merge pull request #35 from tekktrik/dev/allow-dashes
Allow dashes in board name
2 parents 422022f + 27b900d commit 21857aa

File tree

6 files changed

+125
-60
lines changed

6 files changed

+125
-60
lines changed

circfirm/backend.py

Lines changed: 30 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
import enum
1111
import os
1212
import pathlib
13+
import re
1314
from typing import Dict, List, Optional, Set, Tuple
1415

1516
import packaging.version
@@ -45,6 +46,18 @@ class Language(enum.Enum):
4546
MANDARIN_LATIN_PINYIN = "zh_Latn_pinyin"
4647

4748

49+
_ALL_LANGAGES = [language.value for language in Language]
50+
_ALL_LANGUAGES_REGEX = "|".join(_ALL_LANGAGES)
51+
FIRMWARE_REGEX = "-".join(
52+
[
53+
r"adafruit-circuitpython-(.*)",
54+
f"({_ALL_LANGUAGES_REGEX})",
55+
r"(\d+\.\d+\.\d+(?:-(?:\balpha\b|\bbeta\b)\.\d+)*)\.uf2",
56+
]
57+
)
58+
BOARD_ID_REGEX = r"Board ID:\s*(.*)"
59+
60+
4861
def _find_device(filename: str) -> Optional[str]:
4962
"""Find a specific connected device."""
5063
for partition in psutil.disk_partitions():
@@ -69,19 +82,20 @@ def find_bootloader() -> Optional[str]:
6982

7083
def get_board_name(device_path: str) -> str:
7184
"""Get the attached CircuitPython board's name."""
72-
uf2info_file = pathlib.Path(device_path) / circfirm.UF2INFO_FILE
73-
with open(uf2info_file, encoding="utf-8") as infofile:
85+
bootout_file = pathlib.Path(device_path) / circfirm.BOOTOUT_FILE
86+
with open(bootout_file, encoding="utf-8") as infofile:
7487
contents = infofile.read()
75-
model_line = [line.strip() for line in contents.split("\n")][1]
76-
return [comp.strip() for comp in model_line.split(":")][1]
88+
board_match = re.search(BOARD_ID_REGEX, contents)
89+
if not board_match:
90+
raise ValueError("Could not parse the board name from the boot out file")
91+
return board_match[1]
7792

7893

7994
def download_uf2(board: str, version: str, language: str = "en_US") -> None:
8095
"""Download a version of CircuitPython for a specific board."""
8196
file = get_uf2_filename(board, version, language=language)
82-
board_name = board.replace(" ", "_").lower()
8397
uf2_file = get_uf2_filepath(board, version, language=language, ensure=True)
84-
url = f"https://downloads.circuitpython.org/bin/{board_name}/{language}/{file}"
98+
url = f"https://downloads.circuitpython.org/bin/{board}/{language}/{file}"
8599
response = requests.get(url)
86100

87101
SUCCESS = 200
@@ -105,31 +119,31 @@ def get_uf2_filepath(
105119
) -> pathlib.Path:
106120
"""Get the path to a downloaded UF2 file."""
107121
file = get_uf2_filename(board, version, language)
108-
board_name = board.replace(" ", "_").lower()
109-
uf2_folder = pathlib.Path(circfirm.UF2_ARCHIVE) / board_name
122+
uf2_folder = pathlib.Path(circfirm.UF2_ARCHIVE) / board
110123
if ensure:
111124
circfirm.startup.ensure_dir(uf2_folder)
112125
return pathlib.Path(uf2_folder) / file
113126

114127

115128
def get_uf2_filename(board: str, version: str, language: str = "en_US") -> str:
116129
"""Get the structured name for a specific board/version CircuitPython."""
117-
board_name = board.replace(" ", "_").lower()
118-
return f"adafruit-circuitpython-{board_name}-{language}-{version}.uf2"
130+
return f"adafruit-circuitpython-{board}-{language}-{version}.uf2"
119131

120132

121133
def get_board_folder(board: str) -> pathlib.Path:
122134
"""Get the board folder path."""
123-
board_name = board.replace(" ", "_").lower()
124-
return pathlib.Path(circfirm.UF2_ARCHIVE) / board_name
135+
return pathlib.Path(circfirm.UF2_ARCHIVE) / board
125136

126137

127138
def get_firmware_info(uf2_filename: str) -> Tuple[str, str]:
128139
"""Get firmware info."""
129-
filename_parts = uf2_filename.split("-")
130-
language = filename_parts[3]
131-
version_extension = "-".join(filename_parts[4:])
132-
version = version_extension[:-4]
140+
regex_match = re.match(FIRMWARE_REGEX, uf2_filename)
141+
if regex_match is None:
142+
raise ValueError(
143+
"Firmware information could not be determined from the filename"
144+
)
145+
version = regex_match[3]
146+
language = regex_match[2]
133147
return version, language
134148

135149

circfirm/cli.py

Lines changed: 20 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
import pathlib
1212
import shutil
1313
import sys
14+
import time
1415
from typing import Optional
1516

1617
import click
@@ -30,8 +31,23 @@ def cli() -> None:
3031
@cli.command()
3132
@click.argument("version")
3233
@click.option("-l", "--language", default="en_US", help="CircuitPython language/locale")
33-
def install(version: str, language: str) -> None:
34+
@click.option("-b", "--board", default=None, help="Assume the given board name")
35+
def install(version: str, language: str, board: Optional[str]) -> None:
3436
"""Install the specified version of CircuitPython."""
37+
if not board:
38+
circuitpy = circfirm.backend.find_circuitpy()
39+
if not circuitpy and circfirm.backend.find_bootloader():
40+
click.echo("CircuitPython device found, but it is in bootloader mode!")
41+
click.echo(
42+
"Please put the device out of bootloader mode, or use the --board option."
43+
)
44+
sys.exit(3)
45+
board = circfirm.backend.get_board_name(circuitpy)
46+
47+
click.echo("Board name detected, please switch the device to bootloader mode.")
48+
while not circfirm.backend.find_bootloader():
49+
time.sleep(1)
50+
3551
mount_path = circfirm.backend.find_bootloader()
3652
if not mount_path:
3753
circuitpy = circfirm.backend.find_circuitpy()
@@ -44,8 +60,6 @@ def install(version: str, language: str) -> None:
4460
click.echo("Check that the device is connected and mounted.")
4561
sys.exit(1)
4662

47-
board = circfirm.backend.get_board_name(mount_path)
48-
4963
if not circfirm.backend.is_downloaded(board, version, language):
5064
click.echo("Downloading UF2...")
5165
circfirm.backend.download_uf2(board, version, language)
@@ -76,8 +90,6 @@ def clear(
7690
click.echo("Cache cleared!")
7791
return
7892

79-
board = board.replace(" ", "_").lower()
80-
8193
glob_pattern = "*-*" if board is None else f"*-{board}"
8294
language_pattern = "-*" if language is None else f"-{language}"
8395
glob_pattern += language_pattern
@@ -99,19 +111,17 @@ def clear(
99111
@click.option("-b", "--board", default=None, help="CircuitPython board name")
100112
def cache_list(board: Optional[str]) -> None:
101113
"""List all the boards/versions cached."""
102-
if board is not None:
103-
board_name = board.replace(" ", "_").lower()
104114
board_list = os.listdir(circfirm.UF2_ARCHIVE)
105115

106116
if not board_list:
107117
click.echo("Versions have not been cached yet for any boards.")
108118
sys.exit(0)
109119

110-
if board is not None and board_name not in board_list:
111-
click.echo(f"No versions for board '{board_name}' are not cached.")
120+
if board is not None and board not in board_list:
121+
click.echo(f"No versions for board '{board}' are not cached.")
112122
sys.exit(0)
113123

114-
specified_board = board_name if board is not None else None
124+
specified_board = board if board is not None else None
115125
boards = circfirm.backend.get_sorted_boards(specified_board)
116126

117127
for rec_boardname, rec_boardvers in boards.items():

tests/assets/boot_out.txt

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
1-
Adafruit CircuitPython 8.2.9 on 2023-12-06; Adafruit Feather STM32F405 Express with STM32F405RG
2-
Board ID:feather_stm32f405_express
3-
UID:250026001050304235343220
1+
Adafruit CircuitPython 8.0.0-beta.6 on 2022-12-21; Adafruit Feather M4 Express with samd51j19
2+
Board ID:feather_m4_express
3+
UID:C4391B2B0D942955

tests/helpers.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,12 @@ def get_mount_node(path: str, must_exist: bool = False) -> str:
4242
return node_location
4343

4444

45+
def delete_mount_node(path: str, missing_okay: bool = False) -> None:
46+
"""Delete a file on the mounted druve."""
47+
node_file = get_mount_node(path)
48+
pathlib.Path(node_file).unlink(missing_ok=missing_okay)
49+
50+
4551
def touch_mount_node(path: str, exist_ok: bool = False) -> str:
4652
"""Touch a file on the mounted drive."""
4753
node_location = get_mount_node(path)

tests/test_backend.py

Lines changed: 30 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -50,17 +50,33 @@ def test_find_bootloader() -> None:
5050

5151
def test_get_board_name() -> None:
5252
"""Tests getting the board name from the UF2 info file."""
53+
# Setup
54+
tests.helpers.delete_mount_node(circfirm.UF2INFO_FILE)
55+
tests.helpers.copy_boot_out()
56+
57+
# Test successful parsing
5358
mount_location = tests.helpers.get_mount()
5459
board_name = circfirm.backend.get_board_name(mount_location)
55-
assert board_name == "PyGamer"
60+
assert board_name == "feather_m4_express"
61+
62+
# Test unsuccessful parsing
63+
with open(
64+
tests.helpers.get_mount_node(circfirm.BOOTOUT_FILE), mode="w", encoding="utf-8"
65+
) as bootfile:
66+
bootfile.write("junktext")
67+
with pytest.raises(ValueError):
68+
circfirm.backend.get_board_name(mount_location)
69+
70+
# Clean up
71+
tests.helpers.delete_mount_node(circfirm.BOOTOUT_FILE)
72+
tests.helpers.copy_uf2_info()
5673

5774

5875
def test_get_board_folder() -> None:
5976
"""Tests getting UF2 information."""
60-
board_name = "Feather M4 Express"
61-
formatted_board_name = board_name.replace(" ", "_").lower()
77+
board_name = "feather_m4_express"
6278
board_path = circfirm.backend.get_board_folder(board_name)
63-
expected_path = pathlib.Path(circfirm.UF2_ARCHIVE) / formatted_board_name
79+
expected_path = pathlib.Path(circfirm.UF2_ARCHIVE) / board_name
6480
assert board_path.resolve() == expected_path.resolve()
6581

6682

@@ -71,7 +87,7 @@ def test_get_uf2_filepath() -> None:
7187
version = "7.0.0"
7288

7389
created_path = circfirm.backend.get_uf2_filepath(
74-
"Feather M4 Express", "7.0.0", "en_US", ensure=True
90+
"feather_m4_express", "7.0.0", "en_US", ensure=True
7591
)
7692
expected_path = (
7793
pathlib.Path(circfirm.UF2_ARCHIVE)
@@ -83,16 +99,14 @@ def test_get_uf2_filepath() -> None:
8399

84100
def test_download_uf2() -> None:
85101
"""Tests the UF2 download functionality."""
86-
board_name = "Feather M4 Express"
102+
board_name = "feather_m4_express"
87103
language = "en_US"
88104
version = "junktext"
89105

90-
formatted_board_name = board_name.replace(" ", "_").lower()
91-
92106
# Test bad download candidate
93107
expected_path = (
94108
circfirm.backend.get_board_folder(board_name)
95-
/ f"adafruit-circuitpython-{formatted_board_name}-{language}-{version}.uf2"
109+
/ f"adafruit-circuitpython-{board_name}-{language}-{version}.uf2"
96110
)
97111
with pytest.raises(ConnectionError):
98112
circfirm.backend.download_uf2(board_name, version, language)
@@ -105,7 +119,7 @@ def test_download_uf2() -> None:
105119
circfirm.backend.download_uf2(board_name, version, language)
106120
expected_path = (
107121
circfirm.backend.get_board_folder(board_name)
108-
/ f"adafruit-circuitpython-{formatted_board_name}-{language}-{version}.uf2"
122+
/ f"adafruit-circuitpython-{board_name}-{language}-{version}.uf2"
109123
)
110124
assert expected_path.exists()
111125
assert circfirm.backend.is_downloaded(board_name, version)
@@ -116,9 +130,10 @@ def test_download_uf2() -> None:
116130

117131
def test_get_firmware_info() -> None:
118132
"""Tests the ability to get firmware information."""
119-
board_name = "Feather M4 Express"
133+
board_name = "feather_m4_express"
120134
language = "en_US"
121135

136+
# Test successful parsing
122137
for version in ("8.0.0", "9.0.0-beta.2"):
123138
try:
124139
board_folder = circfirm.backend.get_board_folder(board_name)
@@ -133,3 +148,7 @@ def test_get_firmware_info() -> None:
133148
finally:
134149
# Clean up post tests
135150
shutil.rmtree(board_folder)
151+
152+
# Test failed parsing
153+
with pytest.raises(ValueError):
154+
circfirm.backend.get_firmware_info("cannotparse")

0 commit comments

Comments
 (0)