Files
c8dac10348 Add MCP server for interacting with meshtastic devices and testing framework / TUI (#10194)
* 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>
2026-04-18 08:17:44 -05:00

160 lines
5.3 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""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