Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Update towards 2.2.0 #63

Merged
merged 22 commits into from
Mar 7, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 25 additions & 3 deletions circfirm/backend.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,9 @@ class Language(enum.Enum):
)

BOARD_ID_REGEX = r"Board ID:\s*(.*)"
BOARD_VER_REGEX = (
r"Adafruit CircuitPython (\d+\.\d+\.\d+(?:-(?:\balpha\b|\bbeta\b)\.\d+)*)"
)

S3_CONFIG = botocore.client.Config(signature_version=botocore.UNSIGNED)
S3_RESOURCE: S3ServiceResource = boto3.resource("s3", config=S3_CONFIG)
Expand Down Expand Up @@ -127,15 +130,18 @@ def find_bootloader() -> Optional[str]:
return _find_device(circfirm.UF2INFO_FILE)


def get_board_name(device_path: str) -> str:
"""Get the attached CircuitPython board's name."""
def get_board_info(device_path: str) -> Tuple[str, str]:
"""Get the attached CircuitPytho board's name and version."""
bootout_file = pathlib.Path(device_path) / circfirm.BOOTOUT_FILE
with open(bootout_file, encoding="utf-8") as infofile:
contents = infofile.read()
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]
version_match = re.search(BOARD_VER_REGEX, contents)
if not version_match:
raise ValueError("Could not parse the firmware version from the boot out file")
return board_match[1], version_match[1]


def download_uf2(board: str, version: str, language: str = "en_US") -> None:
Expand Down Expand Up @@ -279,3 +285,19 @@ def get_board_versions(
except packaging.version.InvalidVersion:
pass
return sorted(versions, key=packaging.version.Version, reverse=True)


def get_latest_board_version(
board: str, language: str, pre_release: bool
) -> Optional[str]:
"""Get the latest version for a board in a given language."""
versions = get_board_versions(board, language)
if not pre_release:
versions = [
version
for version in versions
if not packaging.version.Version(version).is_prerelease
]
if versions:
return versions[0]
return None
125 changes: 70 additions & 55 deletions circfirm/cli/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
import shutil
import sys
import time
from typing import Any, Callable, Dict, Iterable, Optional, TypeVar
from typing import Any, Callable, Dict, Iterable, Optional, Tuple, TypeVar

import click
import click_spinner
Expand Down Expand Up @@ -41,6 +41,75 @@ def maybe_support(msg: str) -> None:
click.echo(msg)


def get_board_name(
circuitpy: Optional[str], bootloader: Optional[str], board: Optional[str]
) -> Tuple[str, str]:
"""Get the board name of a device via CLI."""
if not board:
if not circuitpy and 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_info(circuitpy)[0]

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


def get_connection_status() -> Tuple[Optional[str], Optional[str]]:
"""Get the status of a connectted CircuitPython device as a CIRCUITPY and bootloader location."""
circuitpy = circfirm.backend.find_circuitpy()
bootloader = circfirm.backend.find_bootloader()
if not circuitpy and not bootloader:
click.echo("CircuitPython device not found!")
click.echo("Check that the device is connected and mounted.")
sys.exit(1)
return circuitpy, bootloader


def ensure_bootloader_mode(bootloader: Optional[str]) -> None:
"""Ensure the connected device is in bootloader mode."""
if not bootloader:
if circfirm.backend.find_circuitpy():
click.echo("CircuitPython device found, but is not in bootloader mode!")
click.echo("Please put the device in bootloader mode.")
sys.exit(2)


def download_if_needed(board: str, version: str, language: str) -> None:
"""Download the firmware for a given board, version, and language via CLI."""
if not circfirm.backend.is_downloaded(board, version, language):
try:
announce_and_await(
"Downloading UF2",
circfirm.backend.download_uf2,
args=(board, version, language),
)
except ConnectionError as err:
click.echo(" failed") # Mark as failed
click.echo(f"Error: {err.args[0]}")
sys.exit(4)
else:
click.echo("Using cached firmware file")


def copy_cache_firmware(
board: str, version: str, language: str, bootloader: str
) -> None:
"""Copy the cached firmware for a given board, version, and language to the bootloader via CLI."""
uf2file = circfirm.backend.get_uf2_filepath(board, version, language)
uf2filename = os.path.basename(uf2file)
uf2_path = os.path.join(bootloader, uf2filename)
announce_and_await(
f"Copying UF2 to {board}", shutil.copyfile, args=(uf2file, uf2_path)
)
click.echo("Device should reboot momentarily.")


def announce_and_await(
msg: str,
func: Callable[..., _T],
Expand Down Expand Up @@ -101,60 +170,6 @@ def load_subcmd_folder(path: str, super_import_name: str) -> None:
cli.add_command(subcmd, subcmd_name)


@cli.command()
@click.argument("version")
@click.option("-l", "--language", default="en_US", help="CircuitPython language/locale")
@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."""
circuitpy = circfirm.backend.find_circuitpy()
bootloader = circfirm.backend.find_bootloader()
if not circuitpy and not bootloader:
click.echo("CircuitPython device not found!")
click.echo("Check that the device is connected and mounted.")
sys.exit(1)

if not board:
if not circuitpy and 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 (bootloader := circfirm.backend.find_bootloader()):
time.sleep(1)

if not bootloader:
if circfirm.backend.find_circuitpy():
click.echo("CircuitPython device found, but is not in bootloader mode!")
click.echo("Please put the device in bootloader mode.")
sys.exit(2)

if not circfirm.backend.is_downloaded(board, version, language):
try:
announce_and_await(
"Downloading UF2",
circfirm.backend.download_uf2,
args=(board, version, language),
)
except ConnectionError as err:
click.echo(f"Error: {err.args[0]}")
sys.exit(4)
else:
click.echo("Using cached firmware file")

uf2file = circfirm.backend.get_uf2_filepath(board, version, language)
uf2filename = os.path.basename(uf2file)
uf2_path = os.path.join(bootloader, uf2filename)
announce_and_await(
f"Copying UF2 to {board}", shutil.copyfile, args=(uf2file, uf2_path)
)
click.echo("Device should reboot momentarily.")


# Load extra commands from the rest of the circfirm.cli subpackage
cli_pkg_path = os.path.dirname(os.path.abspath(__file__))
cli_pkg_name = "circfirm.cli"
Expand Down
42 changes: 42 additions & 0 deletions circfirm/cli/current.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
# SPDX-FileCopyrightText: 2024 Alec Delaney, for Adafruit Industries
#
# SPDX-License-Identifier: MIT

"""CLI functionality for the current subcommand.

Author(s): Alec Delaney
"""

from typing import Tuple

import click

import circfirm.backend
import circfirm.cli


def get_board_info() -> Tuple[str, str]:
"""Get board info via the CLI."""
circuitpy, _ = circfirm.cli.get_connection_status()
if not circuitpy:
raise click.ClickException(
"Board must be in CIRCUITPY mode in order to detect board information"
)
return circfirm.backend.get_board_info(circuitpy)


@click.group()
def cli() -> None:
"""Check the information about the currently connected board."""


@cli.command(name="name")
def current_name() -> None:
"""Get the board name of the currently connected board."""
click.echo(get_board_info()[0])


@cli.command(name="version")
def current_version() -> None:
"""Get the CircuitPython version of the currently connected board."""
click.echo(get_board_info()[1])
33 changes: 33 additions & 0 deletions circfirm/cli/install.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# SPDX-FileCopyrightText: 2024 Alec Delaney, for Adafruit Industries
#
# SPDX-License-Identifier: MIT

"""CLI functionality for the install subcommand.

Author(s): Alec Delaney
"""

from typing import Optional

import click

import circfirm.backend
import circfirm.cli


@click.command()
@click.argument("version")
@click.option("-l", "--language", default="en_US", help="CircuitPython language/locale")
@click.option(
"-b",
"--board",
default=None,
help="Assume the given board name (and connect in bootloader mode)",
)
def cli(version: str, language: str, board: Optional[str]) -> None:
"""Install the specified version of CircuitPython."""
circuitpy, bootloader = circfirm.cli.get_connection_status()
bootloader, board = circfirm.cli.get_board_name(circuitpy, bootloader, board)
circfirm.cli.ensure_bootloader_mode(bootloader)
circfirm.cli.download_if_needed(board, version, language)
circfirm.cli.copy_cache_firmware(board, version, language, bootloader)
12 changes: 3 additions & 9 deletions circfirm/cli/query.py
Original file line number Diff line number Diff line change
Expand Up @@ -84,12 +84,6 @@ def query_versions(board: str, language: str, regex: str) -> None:
)
def query_latest(board: str, language: str, pre_release: bool) -> None:
"""Query the latest CircuitPython versions available."""
versions = circfirm.backend.get_board_versions(board, language)
if not pre_release:
versions = [
version
for version in versions
if not packaging.version.Version(version).is_prerelease
]
if versions:
click.echo(versions[0])
version = circfirm.backend.get_latest_board_version(board, language, pre_release)
if version:
click.echo(version)
62 changes: 62 additions & 0 deletions circfirm/cli/update.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
# SPDX-FileCopyrightText: 2024 Alec Delaney, for Adafruit Industries
#
# SPDX-License-Identifier: MIT

"""CLI functionality for the update subcommand.

Author(s): Alec Delaney
"""

from typing import Optional

import click
import packaging.version

import circfirm.backend
import circfirm.cli.install


@click.command()
@click.option(
"-b",
"--board",
default=None,
help="Assume the given board name (and connect in bootloader mode)",
)
@click.option("-l", "--language", default="en_US", help="CircuitPython langauge/locale")
@click.option(
"-p",
"--pre-release",
is_flag=True,
default=False,
help="Whether pre-release versions should be considered",
)
def cli(board: Optional[str], language: str, pre_release: bool) -> None:
"""Update a connected board to the latest CircuitPython version."""
circuitpy, bootloader = circfirm.cli.get_connection_status()
if circuitpy:
_, current_version = circfirm.backend.get_board_info(circuitpy)
else:
click.echo(
"Bootloader mode detected - cannot check the currently installed version"
)
click.echo(
"The latest version will be installed regardless of the currently installed version."
)
current_version = "0.0.0"
bootloader, board = circfirm.cli.get_board_name(circuitpy, bootloader, board)

new_version = circfirm.backend.get_latest_board_version(
board, language, pre_release
)
if packaging.version.Version(current_version) >= packaging.version.Version(
new_version
):
click.echo(
f"Current version ({current_version}) is at or higher than proposed new update ({new_version})"
)
return

circfirm.cli.ensure_bootloader_mode(bootloader)
circfirm.cli.download_if_needed(board, new_version, language)
circfirm.cli.copy_cache_firmware(board, new_version, language, bootloader)
6 changes: 3 additions & 3 deletions tests/cli/test_cli_about.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,11 @@

from circfirm.cli import cli

RUNNER = CliRunner()


def test_about() -> None:
"""Tests the about command."""
runner = CliRunner()

result = runner.invoke(cli, ["about"])
result = RUNNER.invoke(cli, ["about"])
assert result.exit_code == 0
assert result.output == "Written by Alec Delaney, licensed under MIT License.\n"
Loading