diff --git a/dotbot/cli/device.py b/dotbot/cli/device.py index 7b9aed5f..6448d40b 100644 --- a/dotbot/cli/device.py +++ b/dotbot/cli/device.py @@ -114,6 +114,19 @@ def _fw_version_option(f): )(f) +def _local_root_option(f): + return click.option( + "--local-root", + type=click.Path(path_type=Path, file_okay=False, dir_okay=True), + default=None, + help=( + "With -f local: root of the build tree to flash from (e.g. a " + "worktree). Re-links the local artifacts before flashing, so this " + "is `dotbot fw fetch -f local --local-root ` in one step." + ), + )(f) + + @cmd.command(name="flash-swarmit-sandbox") @click.option( "--swarm-id", @@ -128,9 +141,12 @@ def _fw_version_option(f): help="Optional LH2 calibration file to bake into the config page.", ) @_fw_version_option +@_local_root_option @_probe_option @click.pass_context -def flash_swarmit_sandbox(ctx, swarm_id, calibration_path, fw_version, probe): +def flash_swarmit_sandbox( + ctx, swarm_id, calibration_path, fw_version, local_root, probe +): """Turn a DotBot v3 into a swarm sandbox host (was `provision -d dotbot-v3`). Flashes the SwarmIT bootloader (app core) + netcore + writes the @@ -161,6 +177,7 @@ def flash_swarmit_sandbox(ctx, swarm_id, calibration_path, fw_version, probe): calibration_path=calibration_path, bin_dir=artifacts_dir(), sn_starting_digits=probe, + local_root=local_root, ) @@ -171,9 +188,10 @@ def flash_swarmit_sandbox(ctx, swarm_id, calibration_path, fw_version, probe): help="16-bit hex swarm id (e.g. 0100); defaults to your config's swarm_id.", ) @_fw_version_option +@_local_root_option @_probe_option @click.pass_context -def flash_mari_gateway(ctx, swarm_id, fw_version, probe): +def flash_mari_gateway(ctx, swarm_id, fw_version, local_root, probe): """Turn an nRF5340-DK into the swarm gateway (was `provision -d gateway`). Flashes the Mari gateway firmware (both cores) + writes the network @@ -203,6 +221,7 @@ def flash_mari_gateway(ctx, swarm_id, fw_version, probe): fw_version=fw_version, bin_dir=artifacts_dir(), sn_starting_digits=probe, + local_root=local_root, ) diff --git a/dotbot/firmware/flash.py b/dotbot/firmware/flash.py index 17e2e67b..1e8019b3 100644 --- a/dotbot/firmware/flash.py +++ b/dotbot/firmware/flash.py @@ -297,6 +297,23 @@ def manifest_matches( ) +def describe_image(path: Path) -> str: + """``path``, plus the symlink target and its build time when it is a link. + + ``-f local`` resolves to a directory of symlinks into a build tree, so the + path alone does not say which tree (or how stale a build) is about to be + flashed. Resolving it here means every flash reports its own provenance. + """ + if not path.is_symlink(): + return str(path) + target = Path(os.readlink(path)) + try: + built = time.strftime("%Y-%m-%d %H:%M", time.localtime(target.stat().st_mtime)) + except OSError: + return f"{path} -> {target} (BROKEN LINK)" + return f"{path}\n -> {target} (built {built})" + + def flash_role( role: str, *, @@ -306,6 +323,7 @@ def flash_role( bin_dir: Path = DEFAULT_BIN_DIR, sn_starting_digits: str | None = None, default_app_name: str | None = None, + local_root: Path | None = None, ) -> None: """Flash a device's role: system firmware bundle (app+net cores) + config. @@ -316,10 +334,19 @@ def flash_role( net_id/device_id (never raises on readback failure). If the role's images are absent from ``bin_dir//``, fetches the release first (the "run fetch under the hood" behaviour). + + ``local_root`` (only with ``fw_version="local"``) re-points the local + symlinks at that build tree before flashing, so a one-liner can target a + worktree instead of whichever tree a previous `dotbot fw fetch` linked. """ assets = DEVICE_ASSETS[role] net_id_val, net_id_hex = net_id + # Checked before the J-Link probe so a bad flag combination fails + # instantly rather than after hardware selection. + if local_root is not None and fw_version != "local": + raise click.ClickException("--local-root requires --fw-version local.") + if sn_starting_digits: snr = pick_matching_jlink_snr(sn_starting_digits) else: @@ -370,7 +397,9 @@ def flash_role( # swarmit release into bin_dir/swarmit-/ before flashing. pre_app = fw_root / assets["app"] pre_net = fw_root / assets["net"] - if fw_version != "local" and not (pre_app.exists() and pre_net.exists()): + if fw_version == "local" and local_root is not None: + fetch_assets("swarmit", "local", bin_dir, local_root) + elif fw_version != "local" and not (pre_app.exists() and pre_net.exists()): click.echo(f"[INFO] firmware {fw_version} not found in {fw_root}; fetching...") fetch_assets("swarmit", fw_version, bin_dir) if not fw_root.exists(): @@ -447,8 +476,8 @@ def flash_role( click.echo(f"[INFO] device: {device}") click.echo(f"[INFO] fw_version: {fw_version}") click.echo(f"[INFO] network_id: 0x{net_id_hex}") - click.echo(f"[INFO] app hex: {app_hex}") - click.echo(f"[INFO] net hex: {net_hex}") + click.echo(f"[INFO] app hex: {describe_image(app_hex)}") + click.echo(f"[INFO] net hex: {describe_image(net_hex)}") click.echo(f"[INFO] config hex: {config_hex}") if not config_hex.exists():