mirror of
https://github.com/alexhopeoconnor/firmware.git
synced 2026-10-15 07:51:19 +10:00
* Start of MCP server and test suite * Add MCP server for interacting with meshtastic devices and testing framework / TUI * Update mcp-server/README.md Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * fix mcp-server review feedback from thread Agent-Logs-Url: https://github.com/meshtastic/firmware/sessions/91dc128a-ed50-4d07-8bb2-3dc6623a05f7 Co-authored-by: thebentern <9000580+thebentern@users.noreply.github.com> * Enhance StreamAPI and PhoneAPI for improved log record handling and concurrency control * Semgrep fixes * Trunk and semgrep fixes * optimize pio streaming tee file writes Agent-Logs-Url: https://github.com/meshtastic/firmware/sessions/04e26c6b-6a2b-45be-bbeb-79ae4d0be633 Co-authored-by: thebentern <9000580+thebentern@users.noreply.github.com> * chore: remove redundant log handle assignment Agent-Logs-Url: https://github.com/meshtastic/firmware/sessions/04e26c6b-6a2b-45be-bbeb-79ae4d0be633 Co-authored-by: thebentern <9000580+thebentern@users.noreply.github.com> * Consolidate type imports and remove placeholder test files * Add tests for config persistence and more exchange messages * Refactor position test to validate on-demand request/reply behavior * Remove position request/reply test and update README for telemetry behavior * Fix transmit history file to get removed on factory reset --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
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
|