Skip to content
Draft
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
8 changes: 4 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -340,10 +340,10 @@ python scripts/tdm_airtime_smoke.py --port /dev/ttyACM0
It drives `net`, `airtime`, and `rf`, asserts the 60/40, 50/50, and 40/60 dwell
splits, checks that the TDM switch counter advances, and fails clearly if the
flashed firmware still has MeshCore gated.
downloads the matching successful `Firmware CI` artifact with `gh`. It refuses
to use an older run unless `--allow-latest-success` is passed, and verifies the
downloaded `FLASH_MANIFEST.txt` before printing the local flash command: artifact
SHA, Actions run id, and T-Deck budget status must match.
The fetch helper refuses to use an older run unless `--allow-latest-success` is
passed, and verifies the downloaded `FLASH_MANIFEST.txt` before printing the
local flash command: artifact SHA, Actions run id, environment, and T-Deck
budget status must match.

CI runs the native simulator build, native codec selftest, deterministic simulator
scenario, screenshot generation, T-Deck firmware build, and T-Deck size report
Expand Down
14 changes: 14 additions & 0 deletions docs/tdeck-flashing-recovery.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,12 @@ Download the artifact for the exact commit:
python scripts/fetch_tdeck_artifact.py --repo ItsLimitlezz/LimitlezzOS --branch <branch> --commit <sha> --out .pio/ci-artifacts/tdeck
```

For the MeshCore-enabled Phase 3 validation artifact:

```sh
python scripts/fetch_tdeck_artifact.py --env tdeck-meshcore --repo ItsLimitlezz/LimitlezzOS --branch <branch> --commit <sha>
```

Check `FLASH_MANIFEST.txt` before flashing:

```powershell
Expand All @@ -63,6 +69,14 @@ Flash and run the standard serial smoke:
python scripts/tdeck_smoke.py --port COM8 --no-stub-upload --skip-build --artifact-dir .pio/ci-artifacts/tdeck --open-timeout 60 --boot-timeout 60 --timeout 30
```

For a MeshCore-enabled TDM run, use the matching environment and artifact
directory:

```sh
python scripts/tdeck_smoke.py --port COM8 --env tdeck-meshcore --no-stub-upload --skip-build --artifact-dir .pio/ci-artifacts/tdeck-meshcore --open-timeout 60 --boot-timeout 60 --timeout 30
python scripts/tdm_airtime_smoke.py --port COM8 --open-timeout 60 --boot-timeout 60 --timeout 30
```

Use `/dev/ttyACM0` or `/dev/ttyUSB0` instead of `COM8` on non-Windows hosts.

## Local Build Flash Path
Expand Down
22 changes: 20 additions & 2 deletions docs/tdeck-release-artifacts.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,17 +11,18 @@ Release binaries must come from the `Firmware CI` workflow for the exact commit
being released. Do not attach locally built firmware as the release binary
unless GitHub Actions is unavailable and the release notes explicitly say so.

The workflow uploads two artifact families:
The workflow uploads three artifact families:

- `tdeck-firmware-<sha>`
- `tdeck-meshcore-firmware-<sha>` for opt-in Phase 3 split-airtime validation
- `native-screenshots-<sha>`

For pull requests, `<sha>` is the PR head SHA. The workflow also records the
GitHub pull-request merge SHA in `FLASH_MANIFEST.txt` as `github_sha`.

## Firmware Bundle Contents

The T-Deck firmware artifact must include:
The T-Deck firmware artifacts must include:

- `bootloader.bin`
- `boot_app0.bin`
Expand Down Expand Up @@ -49,6 +50,9 @@ The T-Deck firmware artifact must include:
- static RAM size
- budget status

The MeshCore-enabled artifact manifest must also include `env=tdeck-meshcore`
and `meshcore_enabled=1`.

## Release Candidate Checklist

Before attaching binaries to a release:
Expand Down Expand Up @@ -80,6 +84,12 @@ For an explicit release candidate:
python scripts/fetch_tdeck_artifact.py --repo ItsLimitlezz/LimitlezzOS --branch <branch> --commit <sha> --out .pio/ci-artifacts/<release>
```

For the opt-in MeshCore TDM validation bundle:

```sh
python scripts/fetch_tdeck_artifact.py --env tdeck-meshcore --repo ItsLimitlezz/LimitlezzOS --branch <branch> --commit <sha>
```

To inspect the manifest:

```sh
Expand Down Expand Up @@ -136,6 +146,14 @@ For the maintainer Windows host, flash the release artifact on `COM8`:
python scripts/tdeck_smoke.py --port COM8 --no-stub-upload --skip-build --artifact-dir .pio/ci-artifacts/<release>
```

For Phase 3 MeshCore TDM validation, flash the opt-in artifact and then run
the split-airtime probe:

```sh
python scripts/tdeck_smoke.py --port COM8 --env tdeck-meshcore --no-stub-upload --skip-build --artifact-dir .pio/ci-artifacts/tdeck-meshcore
python scripts/tdm_airtime_smoke.py --port COM8
```

If the first serial attach times out after a verified flash, retry without
reflashing:

Expand Down
14 changes: 14 additions & 0 deletions docs/tdeck-release-checklist.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,12 @@ gh run watch --repo ItsLimitlezz/LimitlezzOS <run-id> --exit-status --interval 1
python scripts/fetch_tdeck_artifact.py --repo ItsLimitlezz/LimitlezzOS --branch <branch> --commit <sha> --out .pio/ci-artifacts/tdeck
```

For Phase 3 MeshCore TDM validation, fetch the opt-in build instead:

```sh
python scripts/fetch_tdeck_artifact.py --env tdeck-meshcore --repo ItsLimitlezz/LimitlezzOS --branch <branch> --commit <sha>
```

Confirm `FLASH_MANIFEST.txt` contains the expected `repo=`, `sha=`,
`workflow=`, `run_id=`, `flash_offsets=`, and size-budget lines.

Expand All @@ -66,6 +72,14 @@ Flash the downloaded Actions artifact:
python scripts/tdeck_smoke.py --port COM8 --no-stub-upload --skip-build --artifact-dir .pio/ci-artifacts/tdeck --open-timeout 60 --boot-timeout 60 --timeout 30
```

For the MeshCore-enabled artifact, pass the matching environment and then run
the TDM probe:

```sh
python scripts/tdeck_smoke.py --port COM8 --env tdeck-meshcore --no-stub-upload --skip-build --artifact-dir .pio/ci-artifacts/tdeck-meshcore --open-timeout 60 --boot-timeout 60 --timeout 30
python scripts/tdm_airtime_smoke.py --port COM8 --open-timeout 60 --boot-timeout 60 --timeout 30
```

Capture:

- ESP32-S3 revision and MAC.
Expand Down
8 changes: 8 additions & 0 deletions docs/tdeck-troubleshooting.md
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,14 @@ python scripts/fetch_tdeck_artifact.py --repo ItsLimitlezz/LimitlezzOS --branch
python scripts/tdeck_smoke.py --port COM8 --no-stub-upload --skip-build --artifact-dir .pio/ci-artifacts/tdeck
```

For the MeshCore-enabled Phase 3 bundle, use the artifact environment so the
downloaded manifest is checked against `env=tdeck-meshcore`:

```sh
python scripts/fetch_tdeck_artifact.py --env tdeck-meshcore --repo ItsLimitlezz/LimitlezzOS --branch <branch> --commit <sha>
python scripts/tdeck_smoke.py --port COM8 --env tdeck-meshcore --no-stub-upload --skip-build --artifact-dir .pio/ci-artifacts/tdeck-meshcore
```

## Flash Succeeds But Smoke Times Out

USB CDC may disconnect briefly during boot. If flashing completed and all hashes
Expand Down
8 changes: 8 additions & 0 deletions docs/tdeck-upgrade-path.md
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,14 @@ python scripts/fetch_tdeck_artifact.py --repo ItsLimitlezz/LimitlezzOS --branch
python scripts/tdeck_smoke.py --port COM8 --no-stub-upload --skip-build --artifact-dir .pio/ci-artifacts/<name>
```

For the opt-in MeshCore TDM validation image:

```sh
python scripts/fetch_tdeck_artifact.py --env tdeck-meshcore --repo ItsLimitlezz/LimitlezzOS --branch <branch> --commit <sha>
python scripts/tdeck_smoke.py --port COM8 --env tdeck-meshcore --no-stub-upload --skip-build --artifact-dir .pio/ci-artifacts/tdeck-meshcore
python scripts/tdm_airtime_smoke.py --port COM8
```

The artifact fetch is strict by default. It refuses to download an artifact when
there is no successful `Firmware CI` run for the requested commit.

Expand Down
71 changes: 59 additions & 12 deletions scripts/fetch_tdeck_artifact.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,22 @@
from pathlib import Path


ARTIFACT_BUNDLE_FILES = (
"bootloader.bin",
"boot_app0.bin",
"firmware.bin",
"firmware.elf",
"firmware.map",
"partitions.bin",
"FLASH_MANIFEST.txt",
"SIZE_BUDGET.md",
"SIZE_BUDGET.txt",
"size-budget.json",
"tdeck-build.txt",
"tdeck-size.txt",
)


def run_text(cmd: list[str], cwd: Path) -> str:
try:
return subprocess.check_output(cmd, cwd=cwd, text=True, stderr=subprocess.STDOUT).strip()
Expand Down Expand Up @@ -106,6 +122,26 @@ def load_artifacts(project_dir: Path, repo: str, run_id: int) -> list[dict]:
return json.loads(data).get("artifacts", [])


def artifact_prefix(env_name: str) -> str:
if env_name == "tdeck":
return "tdeck-firmware"
if env_name == "tdeck-meshcore":
return "tdeck-meshcore-firmware"
raise SystemExit(f"Unsupported artifact environment: {env_name}")


def default_out_dir(env_name: str) -> Path:
leaf = "tdeck-meshcore" if env_name == "tdeck-meshcore" else "tdeck"
return Path(".pio") / "ci-artifacts" / leaf


def clear_existing_bundle(out_dir: Path) -> None:
for name in ARTIFACT_BUNDLE_FILES:
path = out_dir / name
if path.is_file():
path.unlink()


def choose_run(runs: list[dict], commit: str, allow_latest_success: bool) -> dict:
successful = [r for r in runs if r.get("status") == "completed" and r.get("conclusion") == "success"]
for run in successful:
Expand Down Expand Up @@ -157,10 +193,11 @@ def load_flash_manifest(bundle: Path) -> dict[str, str]:
return values


def validate_flash_manifest(bundle: Path, expected_sha: str, expected_run_id: int) -> dict[str, str]:
def validate_flash_manifest(bundle: Path, expected_sha: str, expected_run_id: int, expected_env: str) -> dict[str, str]:
values = load_flash_manifest(bundle)
actual_sha = values.get("sha", "")
actual_run_id = values.get("run_id", "")
actual_env = values.get("env") or "tdeck"
if actual_sha.lower() != expected_sha.lower():
raise SystemExit(
f"Downloaded artifact SHA mismatch: manifest has {actual_sha or '<missing>'}, "
Expand All @@ -176,27 +213,35 @@ def validate_flash_manifest(bundle: Path, expected_sha: str, expected_run_id: in
f"Downloaded artifact did not pass the T-Deck budget gate: "
f"{values.get('budget_status', '<missing>')}"
)
if actual_env != expected_env:
raise SystemExit(
f"Downloaded artifact environment mismatch: manifest has {actual_env}, "
f"expected {expected_env}."
)
return values


def main() -> int:
parser = argparse.ArgumentParser(description="Download the current branch T-Deck firmware artifact.")
parser.add_argument("--project-dir", default=Path(__file__).resolve().parents[1])
parser.add_argument("--env", default="tdeck", choices=("tdeck", "tdeck-meshcore"),
help="Firmware environment artifact to download.")
parser.add_argument("--repo", help="GitHub repo to read Actions artifacts from, e.g. ItsLimitlezz/LimitlezzOS.")
parser.add_argument("--workflow", default="Firmware CI")
parser.add_argument("--branch", help="Branch name. Defaults to the current git branch.")
parser.add_argument("--commit", help="Commit SHA. Defaults to HEAD.")
parser.add_argument("--run-id", type=int, help="Download a specific run instead of searching.")
parser.add_argument("--artifact-name", help="Artifact name. Defaults to tdeck-firmware-<sha>.")
parser.add_argument("--out", default=Path(".pio") / "ci-artifacts" / "tdeck")
parser.add_argument("--artifact-name", help="Artifact name. Defaults from --env and SHA.")
parser.add_argument("--out", help="Output directory. Defaults from --env.")
parser.add_argument("--allow-latest-success", action="store_true", help="Allow newest successful run if HEAD has none.")
args = parser.parse_args()

project_dir = Path(args.project_dir).resolve()
repo = args.repo or default_repo(project_dir)
branch = args.branch or current_branch(project_dir)
commit = args.commit or current_commit(project_dir)
out_dir = (project_dir / args.out).resolve() if not Path(args.out).is_absolute() else Path(args.out)
out_arg = Path(args.out) if args.out else default_out_dir(args.env)
out_dir = (project_dir / out_arg).resolve() if not out_arg.is_absolute() else out_arg

if args.run_id is not None:
run_id = args.run_id
Expand All @@ -209,25 +254,27 @@ def main() -> int:
artifact_sha = chosen["headSha"]
run_url = chosen["url"]

artifact_name = args.artifact_name or f"tdeck-firmware-{artifact_sha}"
prefix = artifact_prefix(args.env)
artifact_name = args.artifact_name or f"{prefix}-{artifact_sha}"
if args.artifact_name is None:
artifacts = load_artifacts(project_dir, repo, run_id)
names = [a.get("name", "") for a in artifacts]
if artifact_name not in names:
tdeck_names = [name for name in names if name.startswith("tdeck-firmware-")]
if len(tdeck_names) == 1:
env_names = [name for name in names if name.startswith(f"{prefix}-")]
if len(env_names) == 1:
print(
f"[artifact] expected {artifact_name}, using run artifact {tdeck_names[0]}",
f"[artifact] expected {artifact_name}, using run artifact {env_names[0]}",
file=sys.stderr,
)
artifact_name = tdeck_names[0]
artifact_name = env_names[0]
else:
available = "\n".join(f" {name}" for name in names)
raise SystemExit(
f"Artifact {artifact_name} was not found in run {run_id}.\n"
f"Available artifacts:\n{available}"
)
out_dir.mkdir(parents=True, exist_ok=True)
clear_existing_bundle(out_dir)

run_text(
[
Expand All @@ -246,16 +293,16 @@ def main() -> int:
)

bundle = bundle_dir(out_dir)
manifest = validate_flash_manifest(bundle, artifact_sha, run_id)
manifest = validate_flash_manifest(bundle, artifact_sha, run_id, args.env)
print(f"[artifact] run: {run_url}")
print(f"[artifact] name: {artifact_name}")
print(f"[artifact] dir: {bundle}")
print(
f"[artifact] manifest: sha={manifest.get('sha')} run_id={manifest.get('run_id')} "
f"budget={manifest.get('budget_status')}"
f"env={manifest.get('env') or 'tdeck'} budget={manifest.get('budget_status')}"
)
print("[artifact] flash:")
print(f" python scripts/tdeck_smoke.py --port COM8 --no-stub-upload --skip-build --artifact-dir {bundle}")
print(f" python scripts/tdeck_smoke.py --port COM8 --env {args.env} --no-stub-upload --skip-build --artifact-dir {bundle}")
return 0


Expand Down
Loading