mirror of
https://github.com/BigBodyCobain/Shadowbroker.git
synced 2026-05-27 01:22:27 +02:00
b86a258535
Ship the v0.9.79 runtime refresh with transport lane isolation, Infonet secure-message address management, MeshChat MQTT controls, selected asset trail behavior, telemetry panel refinements, onboarding updates, and desktop/package metadata alignment. Also ignore local graphify work products so analysis folders do not leak into future commits.
207 lines
5.8 KiB
Python
207 lines
5.8 KiB
Python
"""Helpers for Meshtastic MQTT roots, topic parsing, and subscriptions."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import re
|
|
from typing import Iterable
|
|
|
|
# Default subscription roots — US-only to avoid flooding the public broker.
|
|
# Users can opt into additional regions via MESH_MQTT_EXTRA_ROOTS.
|
|
DEFAULT_ROOTS: tuple[str, ...] = ("US",)
|
|
DEFAULT_CHANNEL = "LongFast"
|
|
|
|
# Every known official region root (for UI dropdowns / manual opt-in).
|
|
ALL_OFFICIAL_ROOTS: tuple[str, ...] = (
|
|
"US",
|
|
"EU_868",
|
|
"EU_433",
|
|
"CN",
|
|
"JP",
|
|
"KR",
|
|
"TW",
|
|
"RU",
|
|
"IN",
|
|
"ANZ",
|
|
"ANZ_433",
|
|
"NZ_865",
|
|
"TH",
|
|
"UA_868",
|
|
"UA_433",
|
|
"MY_433",
|
|
"MY_919",
|
|
"SG_923",
|
|
"LORA_24",
|
|
)
|
|
|
|
# Legacy/community roots still seen in the wild on public/community brokers.
|
|
COMMUNITY_ROOTS: tuple[str, ...] = (
|
|
"EU",
|
|
"AU",
|
|
"UA",
|
|
"BR",
|
|
"AF",
|
|
"ME",
|
|
"SEA",
|
|
"SA",
|
|
"PL",
|
|
)
|
|
|
|
_ROOT_SEGMENT_RE = re.compile(r"^[A-Za-z0-9_+\-]+$")
|
|
_TOPIC_SEGMENT_RE = re.compile(r"^[A-Za-z0-9_+\-#]+$")
|
|
|
|
|
|
def _dedupe(values: Iterable[str]) -> list[str]:
|
|
out: list[str] = []
|
|
seen: set[str] = set()
|
|
for value in values:
|
|
if value not in seen:
|
|
out.append(value)
|
|
seen.add(value)
|
|
return out
|
|
|
|
|
|
def _split_config_values(raw: str) -> list[str]:
|
|
if not raw:
|
|
return []
|
|
normalized = raw.replace("\n", ",").replace(";", ",")
|
|
return [item.strip() for item in normalized.split(",") if item.strip()]
|
|
|
|
|
|
def normalize_root(value: str) -> str | None:
|
|
"""Normalize a Meshtastic root like `PL` or `US/rob/snd`."""
|
|
|
|
raw = str(value or "").strip()
|
|
if not raw:
|
|
return None
|
|
if raw.startswith("msh/"):
|
|
raw = raw[4:]
|
|
raw = raw.strip("/")
|
|
if raw.endswith("/#"):
|
|
raw = raw[:-2].rstrip("/")
|
|
if not raw:
|
|
return None
|
|
parts = [part for part in raw.split("/") if part]
|
|
if not parts:
|
|
return None
|
|
if any(part in {"+", "#"} for part in parts):
|
|
return None
|
|
if any(not _ROOT_SEGMENT_RE.match(part) for part in parts):
|
|
return None
|
|
return "/".join(parts)
|
|
|
|
|
|
def normalize_topic_filter(value: str) -> str | None:
|
|
"""Normalize a full MQTT subscription filter."""
|
|
|
|
raw = str(value or "").strip()
|
|
if not raw:
|
|
return None
|
|
if not raw.startswith("msh/"):
|
|
root = normalize_root(raw)
|
|
return f"msh/{root}/#" if root else None
|
|
raw = raw.strip("/")
|
|
parts = [part for part in raw.split("/") if part]
|
|
if not parts or parts[0] != "msh":
|
|
return None
|
|
if any(part != "+" and not _TOPIC_SEGMENT_RE.match(part) for part in parts[1:]):
|
|
return None
|
|
return "/".join(parts)
|
|
|
|
|
|
def _default_topics_for_root(root: str) -> list[str]:
|
|
"""Return the default LongFast subscriptions for a region root.
|
|
|
|
The public broker carries protobuf/encrypted traffic under ``/e/`` and
|
|
companion decoded JSON traffic under ``/json/``. Positions often arrive on
|
|
the protobuf path, while public text is commonly easiest to observe on the
|
|
JSON path.
|
|
"""
|
|
return [
|
|
f"msh/{root}/2/e/{DEFAULT_CHANNEL}/#",
|
|
f"msh/{root}/2/json/{DEFAULT_CHANNEL}/#",
|
|
]
|
|
|
|
|
|
def build_subscription_topics(
|
|
extra_roots: str = "",
|
|
extra_topics: str = "",
|
|
include_defaults: bool = True,
|
|
) -> list[str]:
|
|
roots: list[str] = []
|
|
if include_defaults:
|
|
roots.extend(DEFAULT_ROOTS)
|
|
# Community roots are no longer subscribed by default — users opt in
|
|
# via MESH_MQTT_EXTRA_ROOTS to avoid flooding the public broker.
|
|
roots.extend(root for root in (normalize_root(item) for item in _split_config_values(extra_roots)) if root)
|
|
|
|
topics = [
|
|
topic
|
|
for root in _dedupe(roots)
|
|
for topic in _default_topics_for_root(root)
|
|
]
|
|
topics.extend(
|
|
topic
|
|
for topic in (
|
|
normalize_topic_filter(item) for item in _split_config_values(extra_topics)
|
|
)
|
|
if topic
|
|
)
|
|
return _dedupe(topics)
|
|
|
|
|
|
def known_roots(extra_roots: str = "", include_defaults: bool = True) -> list[str]:
|
|
"""Return the roots we are *currently subscribed* to."""
|
|
topics = build_subscription_topics(extra_roots=extra_roots, include_defaults=include_defaults)
|
|
roots: list[str] = []
|
|
for topic in topics:
|
|
if not topic.startswith("msh/") or not topic.endswith("/#"):
|
|
continue
|
|
root = normalize_root(parse_topic_metadata(topic)["root"])
|
|
if root:
|
|
roots.append(root)
|
|
return _dedupe(roots)
|
|
|
|
|
|
def all_available_roots() -> list[str]:
|
|
"""Return every region the UI should list (for dropdowns), regardless of subscription state."""
|
|
return _dedupe(list(ALL_OFFICIAL_ROOTS) + list(COMMUNITY_ROOTS))
|
|
|
|
|
|
def parse_topic_metadata(topic: str) -> dict[str, str]:
|
|
"""Extract region/root/channel metadata from a Meshtastic MQTT topic."""
|
|
|
|
parts = [part for part in str(topic or "").strip("/").split("/") if part]
|
|
if not parts or parts[0] != "msh":
|
|
return {"region": "?", "root": "?", "channel": "LongFast", "mode": "", "version": ""}
|
|
|
|
mode_idx = -1
|
|
for idx in range(1, len(parts)):
|
|
if parts[idx] in {"e", "c", "json"}:
|
|
mode_idx = idx
|
|
break
|
|
|
|
version = ""
|
|
root_parts = parts[1:]
|
|
channel = "LongFast"
|
|
mode = ""
|
|
if mode_idx != -1:
|
|
mode = parts[mode_idx]
|
|
maybe_version_idx = mode_idx - 1
|
|
if maybe_version_idx >= 1 and parts[maybe_version_idx].isdigit():
|
|
version = parts[maybe_version_idx]
|
|
root_parts = parts[1:maybe_version_idx]
|
|
else:
|
|
root_parts = parts[1:mode_idx]
|
|
if len(parts) > mode_idx + 1:
|
|
channel = parts[mode_idx + 1]
|
|
|
|
root = "/".join(root_parts) if root_parts else "?"
|
|
region = root_parts[0] if root_parts else "?"
|
|
return {
|
|
"region": region,
|
|
"root": root,
|
|
"channel": channel or "LongFast",
|
|
"mode": mode,
|
|
"version": version,
|
|
}
|