mirror of
https://github.com/alexhopeoconnor/firmware.git
synced 2026-10-03 19:12:00 +10:00
160 lines
5.3 KiB
Python
160 lines
5.3 KiB
Python
"""Board / PlatformIO env enumeration.
|
|
|
|
Parses `pio project config --json-output` - a nested list of
|
|
`[section_name, [[key, value], ...]]` pairs - into a dict keyed by env name,
|
|
extracting the `custom_meshtastic_*` metadata the firmware variants expose.
|
|
|
|
The parsed config is cached and invalidated when `platformio.ini`'s mtime
|
|
changes, so subsequent calls don't pay the 1-2s pio startup cost.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import threading
|
|
from typing import Any
|
|
|
|
from . import config, pio
|
|
|
|
_CACHE_LOCK = threading.Lock()
|
|
_CACHE: dict[str, Any] = {"mtime": None, "envs": None}
|
|
|
|
|
|
def _parse_bool(value: Any) -> bool:
|
|
if isinstance(value, bool):
|
|
return value
|
|
if isinstance(value, str):
|
|
return value.strip().lower() in ("true", "yes", "1", "on")
|
|
return bool(value)
|
|
|
|
|
|
def _parse_int(value: Any) -> int | None:
|
|
try:
|
|
return int(value)
|
|
except (TypeError, ValueError):
|
|
return None
|
|
|
|
|
|
def _parse_tags(value: Any) -> list[str]:
|
|
if value is None:
|
|
return []
|
|
if isinstance(value, list):
|
|
return [str(v).strip() for v in value if str(v).strip()]
|
|
return [t.strip() for t in str(value).replace(",", " ").split() if t.strip()]
|
|
|
|
|
|
def _env_record(env_name: str, items: list[list[Any]]) -> dict[str, Any]:
|
|
"""Build a normalized dict for one env section."""
|
|
d = dict(items)
|
|
return {
|
|
"env": env_name,
|
|
"architecture": d.get("custom_meshtastic_architecture"),
|
|
"hw_model": _parse_int(d.get("custom_meshtastic_hw_model")),
|
|
"hw_model_slug": d.get("custom_meshtastic_hw_model_slug"),
|
|
"display_name": d.get("custom_meshtastic_display_name"),
|
|
"actively_supported": _parse_bool(
|
|
d.get("custom_meshtastic_actively_supported")
|
|
),
|
|
"support_level": _parse_int(d.get("custom_meshtastic_support_level")),
|
|
"board_level": d.get("board_level"), # "pr", "extra", or None
|
|
"tags": _parse_tags(d.get("custom_meshtastic_tags")),
|
|
"images": _parse_tags(d.get("custom_meshtastic_images")),
|
|
"board": d.get("board"),
|
|
"upload_speed": _parse_int(d.get("upload_speed")),
|
|
"upload_protocol": d.get("upload_protocol"),
|
|
"monitor_speed": _parse_int(d.get("monitor_speed")),
|
|
"monitor_filters": d.get("monitor_filters") or [],
|
|
"_raw": d, # Full dict for get_board
|
|
}
|
|
|
|
|
|
def _load_all() -> dict[str, dict[str, Any]]:
|
|
"""Parse `pio project config` into `{env_name: record}`."""
|
|
raw = pio.run_json(["project", "config"], timeout=pio.TIMEOUT_PROJECT_CONFIG)
|
|
result: dict[str, dict[str, Any]] = {}
|
|
for section_name, items in raw:
|
|
if not isinstance(section_name, str) or not section_name.startswith("env:"):
|
|
continue
|
|
env_name = section_name.split(":", 1)[1]
|
|
result[env_name] = _env_record(env_name, items)
|
|
return result
|
|
|
|
|
|
def _get_cached() -> dict[str, dict[str, Any]]:
|
|
root = config.firmware_root()
|
|
platformio_ini = root / "platformio.ini"
|
|
try:
|
|
mtime = platformio_ini.stat().st_mtime
|
|
except FileNotFoundError:
|
|
mtime = None
|
|
|
|
with _CACHE_LOCK:
|
|
if _CACHE["envs"] is not None and _CACHE["mtime"] == mtime:
|
|
return _CACHE["envs"]
|
|
envs = _load_all()
|
|
_CACHE["envs"] = envs
|
|
_CACHE["mtime"] = mtime
|
|
return envs
|
|
|
|
|
|
def invalidate_cache() -> None:
|
|
with _CACHE_LOCK:
|
|
_CACHE["envs"] = None
|
|
_CACHE["mtime"] = None
|
|
|
|
|
|
def _public_record(rec: dict[str, Any]) -> dict[str, Any]:
|
|
"""Strip the `_raw` field for list outputs."""
|
|
return {k: v for k, v in rec.items() if not k.startswith("_")}
|
|
|
|
|
|
def list_boards(
|
|
architecture: str | None = None,
|
|
actively_supported_only: bool = False,
|
|
query: str | None = None,
|
|
board_level: str | None = None, # "release" | "pr" | "extra"
|
|
) -> list[dict[str, Any]]:
|
|
"""Enumerate PlatformIO envs with Meshtastic metadata.
|
|
|
|
Filters are cumulative (AND). `board_level="release"` means envs with no
|
|
explicit `board_level` set (the default release targets).
|
|
"""
|
|
envs = _get_cached()
|
|
q = query.lower().strip() if query else None
|
|
|
|
out = []
|
|
for rec in envs.values():
|
|
if architecture and rec.get("architecture") != architecture:
|
|
continue
|
|
if actively_supported_only and not rec.get("actively_supported"):
|
|
continue
|
|
if board_level is not None:
|
|
rec_level = rec.get("board_level")
|
|
if board_level == "release":
|
|
if rec_level not in (None, ""):
|
|
continue
|
|
elif rec_level != board_level:
|
|
continue
|
|
if q:
|
|
display = (rec.get("display_name") or "").lower()
|
|
env_name = rec.get("env", "").lower()
|
|
slug = (rec.get("hw_model_slug") or "").lower()
|
|
if q not in display and q not in env_name and q not in slug:
|
|
continue
|
|
out.append(_public_record(rec))
|
|
|
|
out.sort(key=lambda r: (r.get("architecture") or "", r.get("env")))
|
|
return out
|
|
|
|
|
|
def get_board(env: str) -> dict[str, Any]:
|
|
"""Full metadata for one env, including the raw pio config dict."""
|
|
envs = _get_cached()
|
|
rec = envs.get(env)
|
|
if rec is None:
|
|
raise KeyError(
|
|
f"Unknown env: {env!r}. Use list_boards() to see available envs."
|
|
)
|
|
public = _public_record(rec)
|
|
public["raw_config"] = rec["_raw"]
|
|
return public
|