rigs/features.py
Rama main · réplica de lectura del repositorio original.
#!/usr/bin/env python3
"""
features.py — centralny rejestr funkcji radia widocznych jako przyciski w panelu
("Panel funkcji" pod oknem PTT, nad waterfallem).
Kazda funkcja ma:
- id : klucz uzywany w API/config (np. "split")
- label : etykieta po polsku do UI
- icon : nazwa ikony (emoji jako fallback, frontend moze zmapowac na svg)
- group : grupowanie w UI ("vfo", "mode", "tx", "scope", "advanced")
- backend : "civ" | "rigcat" | "both" — ktory backend musi wspierac te funkcje
Przeplyw:
1. CivRig/RigCAT.get_capabilities() -> dict {feature_id: bool}
"co radio TECHNICZNIE umie" (na podstawie profilu modelu / dump_caps)
2. config.json["rigs"][i]["enabledFeatures"] -> dict {feature_id: bool}
"co admin WLACZYL dla uzytkownikow" (whitelist, edytowana w panelu admina)
3. effective_features() laczy oba: funkcja jest aktywna w UI uzytkownika
TYLKO gdy capabilities[id]==True ORAZ enabledFeatures[id]==True
4. webapp.py sprawdza effective_features() przed wykonaniem akcji
(oprocz admina, ktory ma dostep do wszystkiego co radio wspiera)
"""
# Rejestr wszystkich mozliwych funkcji — jedno miejsce prawdy.
# Domyslna wartosc enabled_default = True oznacza ze nowo wykryta funkcja
# (wspierana przez radio) jest domyslnie WIDOCZNA dla userow, dopoki admin
# jej nie wylaczy. Funkcje oznaczone enabled_default=False wymagaja
# recznej akceptacji admina (np. funkcje "ryzykowne" jak zmiana mocy).
FEATURES = [
# ── VFO / czestotliwosc ──────────────────────────────────────────────────
{"id": "freq_set", "label": "Zmiana czestotliwosci", "icon": "📻",
"group": "vfo", "backend": "both", "enabled_default": True},
{"id": "mode_set", "label": "Zmiana trybu pracy (USB/CW/...)", "icon": "🎛️",
"group": "mode", "backend": "both", "enabled_default": True},
{"id": "split", "label": "Split VFO A/B", "icon": "⇄",
"group": "vfo", "backend": "both", "enabled_default": True},
# REMOVED (2026-10-18): "rit" (RIT/XIT) - setting Hz gave NG on the
# live radio, no fix found, removed end-to-end per user decision.
# ── TX / PTT ────────────────────────────────────────────────────────────
{"id": "ptt", "label": "PTT (nadawanie)", "icon": "🎙️",
"group": "tx", "backend": "both", "enabled_default": True},
{"id": "tx_power", "label": "Regulacja mocy TX", "icon": "⚡",
"group": "tx", "backend": "both", "enabled_default": False},
{"id": "tuner", "label": "Tuner (ATU) wl/wyl", "icon": "⚙️",
"group": "tuner", "backend": "both", "enabled_default": True},
{"id": "autotune", "label": "Autotune (generuje TX)", "icon": "🔧",
"group": "tuner", "backend": "civ", "enabled_default": True},
# ── Odbior / S-metr / scope ─────────────────────────────────────────────
{"id": "smeter", "label": "Wskaznik S-metr", "icon": "📶",
"group": "scope", "backend": "both", "enabled_default": True},
{"id": "scope", "label": "Waterfall / Scope", "icon": "🌊",
"group": "scope", "backend": "civ", "enabled_default": True},
{"id": "agc", "label": "AGC — tryb pracy", "icon": "📡",
"group": "scope", "backend": "civ", "enabled_default": False},
# ── Inne ────────────────────────────────────────────────────────────────
{"id": "memory", "label": "Kanaly pamieci", "icon": "💾",
"group": "advanced", "backend": "both", "enabled_default": False},
{"id": "dstar", "label": "D-STAR", "icon": "🛰️",
"group": "advanced", "backend": "civ", "enabled_default": False},
]
FEATURES_BY_ID = {f["id"]: f for f in FEATURES}
def default_enabled_features() -> dict:
"""Domyslna whitelista admina — uzywana gdy config.json nie ma jeszcze
sekcji enabledFeatures dla danego radia (pierwsze uruchomienie)."""
return {f["id"]: f["enabled_default"] for f in FEATURES}
def effective_features(capabilities: dict, enabled_features: dict | None = None) -> dict:
"""
Polacz "co radio umie" (capabilities) z "co admin wlaczyl" (enabled_features).
Zwraca dict {feature_id: bool} — True = funkcja widoczna/aktywna dla usera.
capabilities moze byc:
- stary format: {feature_id: bool} (kompatybilnosc)
- nowy format: {"raw_caps": {...}, "actions": [...], "sliders": [...]}
— w tym przypadku uzywany jest tylko raw_caps dla statycznych FEATURES
Jesli enabled_features is None, uzyj default_enabled_features().
Funkcje nieobecne w capabilities sa traktowane jako False (radio nie wspiera).
"""
if enabled_features is None:
enabled_features = default_enabled_features()
raw_caps = capabilities.get("raw_caps", capabilities) if isinstance(capabilities, dict) else {}
out = {}
for f in FEATURES:
fid = f["id"]
supported = bool(raw_caps.get(fid, False))
admin_on = bool(enabled_features.get(fid, f["enabled_default"]))
out[fid] = supported and admin_on
return out
def effective_dynamic(capabilities: dict, enabled_dynamic: dict | None = None) -> dict:
"""
Filtruj dynamiczne actions/sliders (z hamlib_caps.discover_capabilities)
wedlug whitelisty admina (enabled_dynamic = {dynamic_id: bool}).
Domyslnie WSZYSTKIE wykryte dynamiczne elementy sa WLACZONE (enabled_default=True)
— admin musi recznie WYLACZYC te ktore nie powinny byc widoczne dla userow.
To odwrotna logika niz statyczne FEATURES (tam default zalezy od enabled_default),
bo dynamiczne elementy to z reguly bezpieczne regulacje audio/funkcje radia.
Zwraca {"actions": [...], "sliders": [...]} — tylko elementy z
enabled_dynamic.get(id, True) == True.
"""
if enabled_dynamic is None:
enabled_dynamic = {}
actions = [a for a in capabilities.get("actions", [])
if enabled_dynamic.get(a["id"], True)]
sliders = [s for s in capabilities.get("sliders", [])
if enabled_dynamic.get(s["id"], True)]
return {"actions": actions, "sliders": sliders}
def features_for_admin(capabilities: dict, enabled_features: dict | None = None) -> list:
"""
Zwroc liste funkcji do panelu admina — kazda z metadanymi + 3 flagami:
- supported : czy radio technicznie wspiera (capabilities)
- enabled : czy admin wlaczyl (enabled_features) — TYLKO informacyjnie
gdy supported=False (admin moze i tak przygotowac config
na przyszlosc, np. przed podlaczeniem innego radia)
- effective : supported AND enabled — czy faktycznie widoczne dla usera
Admin moze edytowac 'enabled' nawet dla supported=False (np. przygotowanie
konfiguracji dla radia ktore bedzie podlaczone pozniej), ale przelacznik
bedzie wizualnie wyszarzony w UI z etykieta "radio nie wspiera".
"""
if enabled_features is None:
enabled_features = default_enabled_features()
raw_caps = capabilities.get("raw_caps", capabilities) if isinstance(capabilities, dict) else {}
out = []
for f in FEATURES:
fid = f["id"]
supported = bool(raw_caps.get(fid, False))
enabled = bool(enabled_features.get(fid, f["enabled_default"]))
out.append({
**f,
"supported": supported,
"enabled": enabled,
"effective": supported and enabled,
})
return out
def dynamic_for_admin(capabilities: dict, enabled_dynamic: dict | None = None) -> dict:
"""
Zwroc dynamiczne actions/sliders z flaga 'enabled' (dla panelu admina) —
kazdy element + {"enabled": bool}. Domyslnie enabled=True dla wszystkich
wykrytych elementow (admin moze wylaczyc konkretne).
"""
if enabled_dynamic is None:
enabled_dynamic = {}
actions = [{**a, "enabled": enabled_dynamic.get(a["id"], True)}
for a in capabilities.get("actions", [])]
sliders = [{**s, "enabled": enabled_dynamic.get(s["id"], True)}
for s in capabilities.get("sliders", [])]
return {"actions": actions, "sliders": sliders}