Modify CompanionBridge integration to support persisting NodePrefs

- Added new methods to SQLiteHandler for loading and saving companion preferences as JSON, improving data persistence.
- Introduced a migration to create a companion_prefs table for storing preferences, ensuring compatibility with existing data.
- Refactored main.py to utilize RepeaterCompanionBridge instead of CompanionBridge, aligning with the new architecture.
This commit is contained in:
agessaman
2026-03-02 21:28:15 -08:00
parent e54d79d7c2
commit 4f94b343cc
4 changed files with 160 additions and 3 deletions
+2
View File
@@ -3,6 +3,7 @@
Exposes the MeshCore companion frame protocol over TCP for standard clients.
"""
from .bridge import RepeaterCompanionBridge
from .constants import (
CMD_APP_START,
CMD_GET_CONTACTS,
@@ -17,6 +18,7 @@ from .frame_server import CompanionFrameServer
__all__ = [
"CompanionFrameServer",
"RepeaterCompanionBridge",
"CMD_APP_START",
"CMD_GET_CONTACTS",
"CMD_SEND_TXT_MSG",
+92
View File
@@ -0,0 +1,92 @@
"""
Repeater CompanionBridge with SQLite-backed preference persistence.
Persists full NodePrefs as a JSON blob so companion settings (including
auto-add config) survive repeater restarts. Merge-on-load supports
schema evolution when NodePrefs gains or loses fields.
"""
from __future__ import annotations
import dataclasses
import logging
from typing import Any, Callable, Optional
from pymc_core.companion import CompanionBridge
logger = logging.getLogger("RepeaterCompanionBridge")
class RepeaterCompanionBridge(CompanionBridge):
"""CompanionBridge that persists and loads prefs (full NodePrefs) via SQLite JSON blob."""
def __init__(
self,
identity,
packet_injector: Callable[..., Any],
node_name: str = "pyMC",
adv_type: int = 1,
max_contacts: int = 1000,
max_channels: int = 40,
offline_queue_size: int = 512,
radio_config: Optional[dict] = None,
authenticate_callback: Optional[Callable[..., tuple[bool, int]]] = None,
initial_contacts: Optional[Any] = None,
*,
sqlite_handler=None,
companion_hash: str = "",
) -> None:
self._sqlite_handler = sqlite_handler
self._companion_hash = companion_hash
super().__init__(
identity=identity,
packet_injector=packet_injector,
node_name=node_name,
adv_type=adv_type,
max_contacts=max_contacts,
max_channels=max_channels,
offline_queue_size=offline_queue_size,
radio_config=radio_config,
authenticate_callback=authenticate_callback,
initial_contacts=initial_contacts,
)
def _save_prefs(self) -> None:
"""Persist full NodePrefs as JSON to SQLite."""
if not self._sqlite_handler or not self._companion_hash:
return
try:
prefs_dict = dataclasses.asdict(self.prefs)
self._sqlite_handler.companion_save_prefs(self._companion_hash, prefs_dict)
except Exception as e:
logger.warning("Failed to persist companion prefs: %s", e)
def _load_prefs(self) -> None:
"""Load prefs from SQLite JSON and merge into self.prefs (only known keys)."""
if not self._sqlite_handler or not self._companion_hash:
return
try:
stored = self._sqlite_handler.companion_load_prefs(self._companion_hash)
if not stored or not isinstance(stored, dict):
return
for key, value in stored.items():
if not hasattr(self.prefs, key):
continue
current = getattr(self.prefs, key)
try:
if value is None:
continue
if isinstance(current, bool):
setattr(self.prefs, key, bool(value))
elif isinstance(current, int):
setattr(self.prefs, key, int(value))
elif isinstance(current, float):
setattr(self.prefs, key, float(value))
elif isinstance(current, str):
setattr(self.prefs, key, str(value))
else:
setattr(self.prefs, key, value)
except (TypeError, ValueError) as e:
logger.debug("Skip prefs key %r: %s", key, e)
except Exception as e:
logger.warning("Failed to load companion prefs: %s", e)
@@ -423,6 +423,33 @@ class SQLiteHandler:
)
logger.info(f"Migration '{migration_name}' applied successfully")
# Migration 7: Add companion_prefs table (JSON blob for full NodePrefs persistence)
migration_name = "add_companion_prefs"
existing = conn.execute(
"SELECT migration_name FROM migrations WHERE migration_name = ?",
(migration_name,),
).fetchone()
if not existing:
cursor = conn.execute(
"SELECT name FROM sqlite_master WHERE type='table' AND name='companion_prefs'"
)
if not cursor.fetchone():
conn.execute(
"""
CREATE TABLE companion_prefs (
companion_hash TEXT PRIMARY KEY,
prefs_json TEXT NOT NULL
)
"""
)
logger.info("Created companion_prefs table")
conn.execute(
"INSERT INTO migrations (migration_name, applied_at) VALUES (?, ?)",
(migration_name, time.time()),
)
logger.info(f"Migration '{migration_name}' applied successfully")
conn.commit()
except Exception as e:
@@ -1786,6 +1813,41 @@ class SQLiteHandler:
logger.error(f"Failed to upsert companion contact: {e}")
return False
def companion_load_prefs(self, companion_hash: str) -> Optional[Dict]:
"""Load persisted prefs for a companion. Returns parsed JSON dict or None if no row."""
try:
with sqlite3.connect(self.sqlite_path) as conn:
cursor = conn.execute(
"SELECT prefs_json FROM companion_prefs WHERE companion_hash = ?",
(companion_hash,),
)
row = cursor.fetchone()
if row is None:
return None
return json.loads(row[0])
except Exception as e:
logger.error(f"Failed to load companion prefs: {e}")
return None
def companion_save_prefs(self, companion_hash: str, prefs: Dict) -> bool:
"""Persist prefs for a companion as JSON. Upserts by companion_hash."""
try:
prefs_json = json.dumps(prefs)
with sqlite3.connect(self.sqlite_path) as conn:
conn.execute(
"""
INSERT INTO companion_prefs (companion_hash, prefs_json)
VALUES (?, ?)
ON CONFLICT(companion_hash) DO UPDATE SET prefs_json = excluded.prefs_json
""",
(companion_hash, prefs_json),
)
conn.commit()
return True
except Exception as e:
logger.error(f"Failed to save companion prefs: {e}")
return False
def companion_load_channels(self, companion_hash: str) -> List[Dict]:
"""Load channels for a companion from storage."""
try:
+4 -3
View File
@@ -356,10 +356,9 @@ class RepeaterDaemon:
async def _load_companion_identities(self) -> None:
"""Load companion identities from config and create CompanionBridge + frame server for each."""
from pymc_core import LocalIdentity
from pymc_core.companion import CompanionBridge
from pymc_core.companion.models import Channel, Contact
from repeater.companion import CompanionFrameServer
from repeater.companion import CompanionFrameServer, RepeaterCompanionBridge
companions_config = self.config.get("identities", {}).get("companions") or []
if not companions_config:
@@ -412,11 +411,13 @@ class RepeaterDaemon:
tcp_port = settings.get("tcp_port", 5000)
bind_address = settings.get("bind_address", "0.0.0.0")
bridge = CompanionBridge(
bridge = RepeaterCompanionBridge(
identity=identity,
packet_injector=self.router.inject_packet,
node_name=node_name,
radio_config=radio_config,
sqlite_handler=sqlite_handler,
companion_hash=companion_hash_str,
)
# Load contacts from SQLite