Skip to content

Commit

Permalink
Merge pull request #35 from tekktrik/dev/allow-dashes
Browse files Browse the repository at this point in the history
Allow dashes in board name
  • Loading branch information
tekktrik authored Feb 23, 2024
2 parents 422022f + 27b900d commit 21857aa
Show file tree
Hide file tree
Showing 6 changed files with 125 additions and 60 deletions.
46 changes: 30 additions & 16 deletions circfirm/backend.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
import enum
import os
import pathlib
import re
from typing import Dict, List, Optional, Set, Tuple

import packaging.version
Expand Down Expand Up @@ -45,6 +46,18 @@ class Language(enum.Enum):
MANDARIN_LATIN_PINYIN = "zh_Latn_pinyin"


_ALL_LANGAGES = [language.value for language in Language]
_ALL_LANGUAGES_REGEX = "|".join(_ALL_LANGAGES)
FIRMWARE_REGEX = "-".join(
[
r"adafruit-circuitpython-(.*)",
f"({_ALL_LANGUAGES_REGEX})",
r"(\d+\.\d+\.\d+(?:-(?:\balpha\b|\bbeta\b)\.\d+)*)\.uf2",
]
)
BOARD_ID_REGEX = r"Board ID:\s*(.*)"


def _find_device(filename: str) -> Optional[str]:
"""Find a specific connected device."""
for partition in psutil.disk_partitions():
Expand All @@ -69,19 +82,20 @@ def find_bootloader() -> Optional[str]:

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


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

SUCCESS = 200
Expand All @@ -105,31 +119,31 @@ def get_uf2_filepath(
) -> pathlib.Path:
"""Get the path to a downloaded UF2 file."""
file = get_uf2_filename(board, version, language)
board_name = board.replace(" ", "_").lower()
uf2_folder = pathlib.Path(circfirm.UF2_ARCHIVE) / board_name
uf2_folder = pathlib.Path(circfirm.UF2_ARCHIVE) / board
if ensure:
circfirm.startup.ensure_dir(uf2_folder)
return pathlib.Path(uf2_folder) / file


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


def get_board_folder(board: str) -> pathlib.Path:
"""Get the board folder path."""
board_name = board.replace(" ", "_").lower()
return pathlib.Path(circfirm.UF2_ARCHIVE) / board_name
return pathlib.Path(circfirm.UF2_ARCHIVE) / board


def get_firmware_info(uf2_filename: str) -> Tuple[str, str]:
"""Get firmware info."""
filename_parts = uf2_filename.split("-")
language = filename_parts[3]
version_extension = "-".join(filename_parts[4:])
version = version_extension[:-4]
regex_match = re.match(FIRMWARE_REGEX, uf2_filename)
if regex_match is None:
raise ValueError(
"Firmware information could not be determined from the filename"
)
version = regex_match[3]
language = regex_match[2]
return version, language


Expand Down
30 changes: 20 additions & 10 deletions circfirm/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
import pathlib
import shutil
import sys
import time
from typing import Optional

import click
Expand All @@ -30,8 +31,23 @@ def cli() -> None:
@cli.command()
@click.argument("version")
@click.option("-l", "--language", default="en_US", help="CircuitPython language/locale")
def install(version: str, language: str) -> None:
@click.option("-b", "--board", default=None, help="Assume the given board name")
def install(version: str, language: str, board: Optional[str]) -> None:
"""Install the specified version of CircuitPython."""
if not board:
circuitpy = circfirm.backend.find_circuitpy()
if not circuitpy and circfirm.backend.find_bootloader():
click.echo("CircuitPython device found, but it is in bootloader mode!")
click.echo(
"Please put the device out of bootloader mode, or use the --board option."
)
sys.exit(3)
board = circfirm.backend.get_board_name(circuitpy)

click.echo("Board name detected, please switch the device to bootloader mode.")
while not circfirm.backend.find_bootloader():
time.sleep(1)

mount_path = circfirm.backend.find_bootloader()
if not mount_path:
circuitpy = circfirm.backend.find_circuitpy()
Expand All @@ -44,8 +60,6 @@ def install(version: str, language: str) -> None:
click.echo("Check that the device is connected and mounted.")
sys.exit(1)

board = circfirm.backend.get_board_name(mount_path)

if not circfirm.backend.is_downloaded(board, version, language):
click.echo("Downloading UF2...")
circfirm.backend.download_uf2(board, version, language)
Expand Down Expand Up @@ -76,8 +90,6 @@ def clear(
click.echo("Cache cleared!")
return

board = board.replace(" ", "_").lower()

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

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

if board is not None and board_name not in board_list:
click.echo(f"No versions for board '{board_name}' are not cached.")
if board is not None and board not in board_list:
click.echo(f"No versions for board '{board}' are not cached.")
sys.exit(0)

specified_board = board_name if board is not None else None
specified_board = board if board is not None else None
boards = circfirm.backend.get_sorted_boards(specified_board)

for rec_boardname, rec_boardvers in boards.items():
Expand Down
6 changes: 3 additions & 3 deletions tests/assets/boot_out.txt
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
Adafruit CircuitPython 8.2.9 on 2023-12-06; Adafruit Feather STM32F405 Express with STM32F405RG
Board ID:feather_stm32f405_express
UID:250026001050304235343220
Adafruit CircuitPython 8.0.0-beta.6 on 2022-12-21; Adafruit Feather M4 Express with samd51j19
Board ID:feather_m4_express
UID:C4391B2B0D942955
6 changes: 6 additions & 0 deletions tests/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,12 @@ def get_mount_node(path: str, must_exist: bool = False) -> str:
return node_location


def delete_mount_node(path: str, missing_okay: bool = False) -> None:
"""Delete a file on the mounted druve."""
node_file = get_mount_node(path)
pathlib.Path(node_file).unlink(missing_ok=missing_okay)


def touch_mount_node(path: str, exist_ok: bool = False) -> str:
"""Touch a file on the mounted drive."""
node_location = get_mount_node(path)
Expand Down
41 changes: 30 additions & 11 deletions tests/test_backend.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,17 +50,33 @@ def test_find_bootloader() -> None:

def test_get_board_name() -> None:
"""Tests getting the board name from the UF2 info file."""
# Setup
tests.helpers.delete_mount_node(circfirm.UF2INFO_FILE)
tests.helpers.copy_boot_out()

# Test successful parsing
mount_location = tests.helpers.get_mount()
board_name = circfirm.backend.get_board_name(mount_location)
assert board_name == "PyGamer"
assert board_name == "feather_m4_express"

# Test unsuccessful parsing
with open(
tests.helpers.get_mount_node(circfirm.BOOTOUT_FILE), mode="w", encoding="utf-8"
) as bootfile:
bootfile.write("junktext")
with pytest.raises(ValueError):
circfirm.backend.get_board_name(mount_location)

# Clean up
tests.helpers.delete_mount_node(circfirm.BOOTOUT_FILE)
tests.helpers.copy_uf2_info()


def test_get_board_folder() -> None:
"""Tests getting UF2 information."""
board_name = "Feather M4 Express"
formatted_board_name = board_name.replace(" ", "_").lower()
board_name = "feather_m4_express"
board_path = circfirm.backend.get_board_folder(board_name)
expected_path = pathlib.Path(circfirm.UF2_ARCHIVE) / formatted_board_name
expected_path = pathlib.Path(circfirm.UF2_ARCHIVE) / board_name
assert board_path.resolve() == expected_path.resolve()


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

created_path = circfirm.backend.get_uf2_filepath(
"Feather M4 Express", "7.0.0", "en_US", ensure=True
"feather_m4_express", "7.0.0", "en_US", ensure=True
)
expected_path = (
pathlib.Path(circfirm.UF2_ARCHIVE)
Expand All @@ -83,16 +99,14 @@ def test_get_uf2_filepath() -> None:

def test_download_uf2() -> None:
"""Tests the UF2 download functionality."""
board_name = "Feather M4 Express"
board_name = "feather_m4_express"
language = "en_US"
version = "junktext"

formatted_board_name = board_name.replace(" ", "_").lower()

# Test bad download candidate
expected_path = (
circfirm.backend.get_board_folder(board_name)
/ f"adafruit-circuitpython-{formatted_board_name}-{language}-{version}.uf2"
/ f"adafruit-circuitpython-{board_name}-{language}-{version}.uf2"
)
with pytest.raises(ConnectionError):
circfirm.backend.download_uf2(board_name, version, language)
Expand All @@ -105,7 +119,7 @@ def test_download_uf2() -> None:
circfirm.backend.download_uf2(board_name, version, language)
expected_path = (
circfirm.backend.get_board_folder(board_name)
/ f"adafruit-circuitpython-{formatted_board_name}-{language}-{version}.uf2"
/ f"adafruit-circuitpython-{board_name}-{language}-{version}.uf2"
)
assert expected_path.exists()
assert circfirm.backend.is_downloaded(board_name, version)
Expand All @@ -116,9 +130,10 @@ def test_download_uf2() -> None:

def test_get_firmware_info() -> None:
"""Tests the ability to get firmware information."""
board_name = "Feather M4 Express"
board_name = "feather_m4_express"
language = "en_US"

# Test successful parsing
for version in ("8.0.0", "9.0.0-beta.2"):
try:
board_folder = circfirm.backend.get_board_folder(board_name)
Expand All @@ -133,3 +148,7 @@ def test_get_firmware_info() -> None:
finally:
# Clean up post tests
shutil.rmtree(board_folder)

# Test failed parsing
with pytest.raises(ValueError):
circfirm.backend.get_firmware_info("cannotparse")
Loading

0 comments on commit 21857aa

Please sign in to comment.