mirror of
https://github.com/pyMC-dev/pyMC_Repeater.git
synced 2026-08-06 17:03:32 +02:00
+34
-93
@@ -273,49 +273,6 @@ duty_cycle:
|
||||
# Maximum airtime per minute in milliseconds
|
||||
max_airtime_per_minute: 3600
|
||||
|
||||
|
||||
# MQTT Publishing Configuration (Optional)
|
||||
mqtt:
|
||||
# Enable/disable MQTT publishing
|
||||
enabled: false
|
||||
|
||||
# MQTT broker settings
|
||||
broker: "localhost"
|
||||
port: 1883 # Use 8883 for TLS/SSL, 80/443/9001 for WebSockets
|
||||
|
||||
# Use WebSocket transport instead of standard TCP
|
||||
# Typically uses ports: 80 (ws://), 443 (wss://), or 9001
|
||||
use_websockets: false
|
||||
|
||||
# Authentication (optional)
|
||||
username: null
|
||||
password: null
|
||||
|
||||
# TLS/SSL configuration (optional)
|
||||
# For public brokers with trusted certificates, just enable TLS:
|
||||
# tls:
|
||||
# enabled: true
|
||||
tls:
|
||||
enabled: false
|
||||
|
||||
# Advanced TLS options (usually not needed for public brokers):
|
||||
|
||||
# Custom CA certificate for server verification
|
||||
# Leave null to use system default CA certificates (recommended)
|
||||
ca_cert: null # e.g., "/etc/ssl/certs/ca-certificates.crt"
|
||||
|
||||
# Client certificate and key for mutual TLS (rarely needed)
|
||||
client_cert: null # e.g., "/etc/pymc/client.crt"
|
||||
client_key: null # e.g., "/etc/pymc/client.key"
|
||||
|
||||
# Skip certificate verification (insecure, not recommended)
|
||||
insecure: false
|
||||
|
||||
# Base topic for publishing
|
||||
# Messages will be published to: {base_topic}/{node_name}/{packet|advert}
|
||||
base_topic: "meshcore/repeater"
|
||||
|
||||
|
||||
# Storage Configuration
|
||||
storage:
|
||||
# Directory for persistent storage files (SQLite, RRD).
|
||||
@@ -333,63 +290,31 @@ storage:
|
||||
# - 1 hour resolution for 1 year
|
||||
|
||||
|
||||
letsmesh:
|
||||
enabled: false
|
||||
mqtt:
|
||||
iata_code: "Test" # e.g., "SFO", "LHR", "Test"
|
||||
|
||||
# ============================================================
|
||||
# BROKER SELECTION MODE - Choose how to connect to brokers
|
||||
# ============================================================
|
||||
#
|
||||
# EXAMPLE 1: Single built-in broker (default, most common)
|
||||
# Connect to Europe only - simple, low bandwidth
|
||||
broker_index: 0 # 0 = Europe, 1 = US West
|
||||
|
||||
# EXAMPLE 2: All built-in brokers for maximum redundancy
|
||||
# Survives single broker failure, best uptime
|
||||
# broker_index: -1 # or null - connects to both EU and US
|
||||
|
||||
# EXAMPLE 3: Only custom brokers (private/self-hosted)
|
||||
# Ignores built-in LetsMesh brokers completely
|
||||
# broker_index: -2
|
||||
# additional_brokers:
|
||||
# - name: "Private Server"
|
||||
# host: "mqtt.myserver.com"
|
||||
# port: 443
|
||||
# audience: "mqtt.myserver.com"
|
||||
|
||||
# EXAMPLE 4: Single built-in + custom backup
|
||||
# Use EU primary with your own backup
|
||||
# broker_index: 0
|
||||
# additional_brokers:
|
||||
# - name: "Backup Server"
|
||||
# host: "mqtt-backup.mydomain.com"
|
||||
# port: 8883
|
||||
# audience: "mqtt-backup.mydomain.com"
|
||||
|
||||
# EXAMPLE 5: All built-in + multiple custom (maximum redundancy)
|
||||
# EU + US + your own servers - best for critical deployments
|
||||
# broker_index: -1
|
||||
# additional_brokers:
|
||||
# - name: "Custom Primary"
|
||||
# host: "mqtt-1.mydomain.com"
|
||||
# port: 443
|
||||
# audience: "mqtt-1.mydomain.com"
|
||||
# - name: "Custom Backup"
|
||||
# host: "mqtt-2.mydomain.com"
|
||||
# port: 443
|
||||
# audience: "mqtt-2.mydomain.com"
|
||||
# ============================================================
|
||||
|
||||
status_interval: 300
|
||||
status_interval: 300 # How often a status message is sent (in seconds)
|
||||
owner: ""
|
||||
email: ""
|
||||
brokers: []
|
||||
|
||||
# Block specific packet types from being published to LetsMesh
|
||||
# Below is the broker object schema:
|
||||
# enabled: true|false # Enable this specific mqtt broker
|
||||
# name: "" # Internal name for this broker
|
||||
# host: "" # hostname or ip of mqtt endpoints
|
||||
# port: # Typically 443 for websocket endpoints or 1883 for tcp
|
||||
# transport: "tcp" or "websockets"
|
||||
# audience: "" # For JWT auth'd endpoints, this is usually the host unless always stated by endpoint owners
|
||||
# use_jwt_auth: true|false # Does this endpoint require JWT auth
|
||||
# username: "" # Username for basic auth. If empty or missing, uses anonymous access
|
||||
# password: "" # Password for basic auth. Required if username is set
|
||||
# format: letsmesh|mqtt
|
||||
# retain_status: true|false # Sets MQTT "retain" on status messages so they remain on the broker when disconnected. Also enforces a QOS of 1 (guaranteed delivery)
|
||||
|
||||
# Block specific packet types from being published to the MQTT endpoint
|
||||
# If not specified or empty list, all types are published
|
||||
# Available types: REQ, RESPONSE, TXT_MSG, ACK, ADVERT, GRP_TXT,
|
||||
# GRP_DATA, ANON_REQ, PATH, TRACE, RAW_CUSTOM
|
||||
disallowed_packet_types: []
|
||||
# disallowed_packet_types: []
|
||||
# - REQ # Don't publish requests
|
||||
# - RESPONSE # Don't publish responses
|
||||
# - TXT_MSG # Don't publish text messages
|
||||
@@ -402,6 +327,22 @@ letsmesh:
|
||||
# - TRACE # Don't publish trace packets
|
||||
# - RAW_CUSTOM # Don't publish custom raw packets
|
||||
|
||||
# Example of using the US and EU LetsMesh endpoints
|
||||
# brokers:
|
||||
# - name: US West (LetsMesh v1)
|
||||
# host: mqtt-us-v1.letsmesh.net
|
||||
# port: 443
|
||||
# audience: mqtt-us-v1.letsmesh.net
|
||||
# use_jwt_auth: true
|
||||
# enabled: true
|
||||
|
||||
# - name: Europe (LetsMesh v1)
|
||||
# host: mqtt-eu-v1.letsmesh.net
|
||||
# port: 443
|
||||
# audience: mqtt-eu-v1.letsmesh.net
|
||||
# use_jwt_auth: true
|
||||
# enabled: true
|
||||
|
||||
# pyMC_Glass control-plane integration (optional)
|
||||
glass:
|
||||
# Enable repeater -> pyMC_Glass /inform loop
|
||||
|
||||
+9
-18
@@ -11,13 +11,13 @@ logger = logging.getLogger("Config")
|
||||
|
||||
def get_node_info(config: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""
|
||||
Extract node name, radio configuration, and LetsMesh settings from config.
|
||||
Extract node name, radio configuration, and MQTT settings from config.
|
||||
|
||||
Args:
|
||||
config: Configuration dictionary
|
||||
|
||||
Returns:
|
||||
Dictionary with node_name, radio_config, and LetsMesh configuration
|
||||
Dictionary with node_name, radio_config, and MQTT configuration
|
||||
"""
|
||||
node_name = config.get("repeater", {}).get("node_name", "PyMC-Repeater")
|
||||
radio_config = config.get("radio", {})
|
||||
@@ -30,26 +30,17 @@ def get_node_info(config: Dict[str, Any]) -> Dict[str, Any]:
|
||||
radio_bw_khz = radio_bw / 1_000
|
||||
radio_config_str = f"{radio_freq_mhz},{radio_bw_khz},{radio_sf},{radio_cr}"
|
||||
|
||||
letsmesh_config = config.get("letsmesh", {})
|
||||
|
||||
from pymc_core.protocol.utils import PAYLOAD_TYPES
|
||||
|
||||
disallowed_types = letsmesh_config.get("disallowed_packet_types", [])
|
||||
type_name_map = {name: code for code, name in PAYLOAD_TYPES.items()}
|
||||
|
||||
disallowed_hex = [type_name_map.get(name.upper(), None) for name in disallowed_types]
|
||||
disallowed_hex = [val for val in disallowed_hex if val is not None] # Filter out invalid names
|
||||
# Handle getting the config from mqtt brokers, falling back to letsmesh if it doesn't exist
|
||||
mqtt_config = config.get("mqtt_brokers", config.get("letsmesh", {}))
|
||||
|
||||
return {
|
||||
"node_name": node_name,
|
||||
"radio_config": radio_config_str,
|
||||
"iata_code": letsmesh_config.get("iata_code", "TEST"),
|
||||
"broker_index": letsmesh_config.get("broker_index", 0),
|
||||
"status_interval": letsmesh_config.get("status_interval", 60),
|
||||
"model": letsmesh_config.get("model", "PyMC-Repeater"),
|
||||
"disallowed_packet_types": disallowed_hex,
|
||||
"email": letsmesh_config.get("email", ""),
|
||||
"owner": letsmesh_config.get("owner", ""),
|
||||
"iata_code": mqtt_config.get("iata_code", "TEST"),
|
||||
"status_interval": mqtt_config.get("status_interval", 60),
|
||||
"model": mqtt_config.get("model", "PyMC-Repeater"),
|
||||
"email": mqtt_config.get("email", ""),
|
||||
"owner": mqtt_config.get("owner", ""),
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
from .glass_handler import GlassHandler
|
||||
from .mqtt_handler import MQTTHandler
|
||||
from .rrdtool_handler import RRDToolHandler
|
||||
from .sqlite_handler import SQLiteHandler
|
||||
from .storage_collector import StorageCollector
|
||||
__all__ = ["SQLiteHandler", "RRDToolHandler", "MQTTHandler", "StorageCollector", "GlassHandler"]
|
||||
__all__ = ["SQLiteHandler", "RRDToolHandler", "StorageCollector", "GlassHandler"]
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -6,8 +6,7 @@ from datetime import datetime
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, Optional
|
||||
|
||||
from .letsmesh_handler import MeshCoreToMqttJwtPusher
|
||||
from .mqtt_handler import MQTTHandler
|
||||
from .mqtt_handler import MeshCoreToMqttPusher
|
||||
from .rrdtool_handler import RRDToolHandler
|
||||
from .sqlite_handler import SQLiteHandler
|
||||
from .storage_utils import PacketRecord
|
||||
@@ -30,45 +29,28 @@ class StorageCollector:
|
||||
self.storage_dir = Path(storage_dir_cfg)
|
||||
self.storage_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
node_name = config.get("repeater", {}).get("node_name", "unknown")
|
||||
node_id = local_identity.get_public_key().hex() if local_identity else "unknown"
|
||||
|
||||
self.sqlite_handler = SQLiteHandler(self.storage_dir)
|
||||
self.rrd_handler = RRDToolHandler(self.storage_dir)
|
||||
self.mqtt_handler = MQTTHandler(config.get("mqtt", {}), node_name, node_id)
|
||||
|
||||
# Initialize LetsMesh handler if configured
|
||||
self.letsmesh_handler = None
|
||||
if config.get("letsmesh", {}).get("enabled", False) and local_identity:
|
||||
# Initialize MQTT handler if configured
|
||||
self.mqtt_handler = None
|
||||
if (config.get("mqtt_brokers", {}) or config.get("letsmesh", {}) or config.get("mqtt", {})) and local_identity:
|
||||
try:
|
||||
# Pass local_identity directly (supports both standard and firmware keys)
|
||||
self.letsmesh_handler = MeshCoreToMqttJwtPusher(
|
||||
self.mqtt_handler = MeshCoreToMqttPusher(
|
||||
local_identity=local_identity,
|
||||
config=config,
|
||||
stats_provider=self._get_live_stats,
|
||||
)
|
||||
self.letsmesh_handler.connect()
|
||||
|
||||
# Get disallowed packet types from config
|
||||
from ..config import get_node_info
|
||||
|
||||
node_info = get_node_info(config)
|
||||
self.disallowed_packet_types = set(node_info["disallowed_packet_types"])
|
||||
self.mqtt_handler.connect()
|
||||
|
||||
public_key_hex = local_identity.get_public_key().hex()
|
||||
logger.info(
|
||||
f"LetsMesh handler initialized with public key: {public_key_hex[:16]}..."
|
||||
f"MQTT handler initialized with public key: {public_key_hex[:16]}..."
|
||||
)
|
||||
if self.disallowed_packet_types:
|
||||
logger.info(f"Disallowed packet types: {sorted(self.disallowed_packet_types)}")
|
||||
else:
|
||||
logger.info("All packet types allowed")
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to initialize LetsMesh handler: {e}")
|
||||
self.letsmesh_handler = None
|
||||
self.disallowed_packet_types = set()
|
||||
else:
|
||||
self.disallowed_packet_types = set()
|
||||
logger.error(f"Failed to initialize MQTT handler: {e}")
|
||||
self.mqtt_handler = None
|
||||
|
||||
# Initialize hardware stats collector
|
||||
from .hardware_stats import HardwareStatsCollector
|
||||
@@ -160,12 +142,12 @@ class StorageCollector:
|
||||
|
||||
return stats
|
||||
|
||||
def record_packet(self, packet_record: dict, skip_letsmesh_if_invalid: bool = True):
|
||||
"""Record packet to storage and defer network publishing to background tasks.
|
||||
def record_packet(self, packet_record: dict, skip_mqtt_if_invalid: bool = True):
|
||||
"""Record packet to storage and publish to MQTT
|
||||
|
||||
Args:
|
||||
packet_record: Dictionary containing packet information
|
||||
skip_letsmesh_if_invalid: If True, don't publish packets with drop_reason to LetsMesh
|
||||
skip_mqtt_if_invalid: If True, don't publish packets with drop_reason to mqtt
|
||||
"""
|
||||
logger.debug(
|
||||
f"Recording packet: type={packet_record.get('type')}, "
|
||||
@@ -182,20 +164,19 @@ class StorageCollector:
|
||||
self._schedule_background(
|
||||
self._deferred_publish,
|
||||
packet_record,
|
||||
skip_letsmesh_if_invalid,
|
||||
skip_mqtt_if_invalid,
|
||||
sync_fallback=self._publish_packet_sync,
|
||||
)
|
||||
|
||||
async def _deferred_publish(self, packet_record: dict, skip_letsmesh: bool):
|
||||
async def _deferred_publish(self, packet_record: dict, skip_mqtt: bool):
|
||||
"""Deferred background task for all network publishing operations."""
|
||||
try:
|
||||
self._publish_packet_sync(packet_record, skip_letsmesh)
|
||||
self._publish_packet_sync(packet_record, skip_mqtt)
|
||||
except Exception as e:
|
||||
logger.error(f"Deferred publish failed: {e}", exc_info=True)
|
||||
|
||||
def _publish_packet_sync(self, packet_record: dict, skip_letsmesh: bool):
|
||||
def _publish_packet_sync(self, packet_record: dict, skip_mqtt: bool):
|
||||
"""Publish packet updates synchronously (used when no asyncio loop is active)."""
|
||||
self.mqtt_handler.publish(packet_record, "packet")
|
||||
self._publish_to_glass(packet_record, "packet")
|
||||
|
||||
if self.websocket_available:
|
||||
@@ -214,41 +195,33 @@ class StorageCollector:
|
||||
except Exception as e:
|
||||
logger.debug(f"WebSocket broadcast failed: {e}")
|
||||
|
||||
if skip_letsmesh and packet_record.get("drop_reason"):
|
||||
logger.debug(
|
||||
f"Skipping LetsMesh publish for packet with drop_reason: {packet_record.get('drop_reason')}"
|
||||
)
|
||||
else:
|
||||
self._publish_to_letsmesh(packet_record)
|
||||
|
||||
def _publish_to_letsmesh(self, packet_record: dict):
|
||||
"""Publish packet to LetsMesh broker if enabled and allowed"""
|
||||
if not self.letsmesh_handler:
|
||||
self._publish_packet_to_mqtt(packet_record)
|
||||
|
||||
def _publish_packet_to_mqtt(self, packet_record: dict):
|
||||
"""Publish packet to mqtt broker if enabled and allowed"""
|
||||
if not self.mqtt_handler:
|
||||
return
|
||||
|
||||
try:
|
||||
packet_type = packet_record.get("type")
|
||||
if packet_type is None:
|
||||
logger.error("Cannot publish to LetsMesh: packet_record missing 'type' field")
|
||||
return
|
||||
|
||||
if packet_type in self.disallowed_packet_types:
|
||||
logger.debug(f"Skipped publishing packet type 0x{packet_type:02X} (disallowed)")
|
||||
logger.error("Cannot publish to mqtt: packet_record missing 'type' field")
|
||||
return
|
||||
|
||||
node_name = self.config.get("repeater", {}).get("node_name", "Unknown")
|
||||
packet = PacketRecord.from_packet_record(
|
||||
packet_record, origin=node_name, origin_id=self.letsmesh_handler.public_key
|
||||
packet_record, origin=node_name, origin_id=self.mqtt_handler.public_key
|
||||
)
|
||||
|
||||
if packet:
|
||||
self.letsmesh_handler.publish_packet(packet.to_dict())
|
||||
logger.debug(f"Published packet type 0x{packet_type:02X} to LetsMesh")
|
||||
self.mqtt_handler.publish_packet(packet.to_dict())
|
||||
logger.debug(f"Published packet type 0x{packet_type:02X} to mqtt")
|
||||
else:
|
||||
logger.debug("Skipped LetsMesh publish: packet missing raw_packet data")
|
||||
logger.debug("Skipped mqtt publish: packet missing raw_packet data")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to publish packet to LetsMesh: {e}", exc_info=True)
|
||||
logger.error(f"Failed to publish packet to mqtt: {e}", exc_info=True)
|
||||
|
||||
def record_advert(self, advert_record: dict):
|
||||
"""Record advert to storage and defer network publishing to background tasks."""
|
||||
@@ -267,7 +240,8 @@ class StorageCollector:
|
||||
logger.error(f"Deferred advert publish failed: {e}", exc_info=True)
|
||||
|
||||
def _publish_advert_sync(self, advert_record: dict):
|
||||
self.mqtt_handler.publish(advert_record, "advert")
|
||||
if self.mqtt_handler:
|
||||
self.mqtt_handler.publish_mqtt(advert_record, "advert")
|
||||
self._publish_to_glass(advert_record, "advert")
|
||||
|
||||
def record_noise_floor(self, noise_floor_dbm: float):
|
||||
@@ -288,7 +262,8 @@ class StorageCollector:
|
||||
logger.error(f"Deferred noise floor publish failed: {e}", exc_info=True)
|
||||
|
||||
def _publish_noise_floor_sync(self, noise_record: dict):
|
||||
self.mqtt_handler.publish(noise_record, "noise_floor")
|
||||
if self.mqtt_handler:
|
||||
self.mqtt_handler.publish_mqtt(noise_record, "noise_floor")
|
||||
self._publish_to_glass(noise_record, "noise_floor")
|
||||
|
||||
def record_crc_errors(self, count: int):
|
||||
@@ -309,7 +284,8 @@ class StorageCollector:
|
||||
logger.error(f"Deferred CRC errors publish failed: {e}", exc_info=True)
|
||||
|
||||
def _publish_crc_errors_sync(self, crc_record: dict):
|
||||
self.mqtt_handler.publish(crc_record, "crc_errors")
|
||||
if self.mqtt_handler:
|
||||
self.mqtt_handler.publish_mqtt(crc_record, "crc_errors")
|
||||
self._publish_to_glass(crc_record, "crc_errors")
|
||||
|
||||
def get_crc_error_count(self, hours: int = 24) -> int:
|
||||
@@ -422,13 +398,12 @@ class StorageCollector:
|
||||
if not task.done():
|
||||
task.cancel()
|
||||
|
||||
self.mqtt_handler.close()
|
||||
if self.letsmesh_handler:
|
||||
if self.mqtt_handler:
|
||||
try:
|
||||
self.letsmesh_handler.disconnect()
|
||||
logger.info("LetsMesh handler disconnected")
|
||||
self.mqtt_handler.disconnect()
|
||||
logger.info("MQTT handler disconnected")
|
||||
except Exception as e:
|
||||
logger.error(f"Error disconnecting LetsMesh handler: {e}")
|
||||
logger.error(f"Error disconnecting MQTT handler: {e}")
|
||||
|
||||
def set_glass_publisher(self, publish_callback):
|
||||
self.glass_publish_callback = publish_callback
|
||||
|
||||
@@ -10,7 +10,7 @@ class PacketRecord:
|
||||
"""
|
||||
Data class for packet record format.
|
||||
Converts internal packet_record format to standardized publish format.
|
||||
Reusable across MQTT, LetsMesh, and other handlers.
|
||||
Reusable across MQTT and other handlers.
|
||||
"""
|
||||
|
||||
origin: str
|
||||
|
||||
+7
-7
@@ -362,13 +362,13 @@ class RepeaterHandler(BaseHandler):
|
||||
)
|
||||
|
||||
# Store packet record to persistent storage
|
||||
# Skip LetsMesh only for invalid packets (not duplicates or operational drops)
|
||||
# Skip mqtt only for invalid packets (not duplicates or operational drops)
|
||||
if self.storage:
|
||||
try:
|
||||
# Only skip LetsMesh for actual invalid/bad packets
|
||||
# Only skip mqtt for actual invalid/bad packets
|
||||
invalid_reasons = ["Invalid advert packet", "Empty payload", "Path too long"]
|
||||
skip_letsmesh = drop_reason in invalid_reasons if drop_reason else False
|
||||
self.storage.record_packet(packet_record, skip_letsmesh_if_invalid=skip_letsmesh)
|
||||
skip_mqtt = drop_reason in invalid_reasons if drop_reason else False
|
||||
self.storage.record_packet(packet_record, skip_mqtt_if_invalid=skip_mqtt)
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to store packet record: {e}")
|
||||
|
||||
@@ -445,7 +445,7 @@ class RepeaterHandler(BaseHandler):
|
||||
packet_hash=packet.calculate_packet_hash().hex().upper(),
|
||||
)
|
||||
try:
|
||||
self.storage.record_packet(packet_record, skip_letsmesh_if_invalid=False)
|
||||
self.storage.record_packet(packet_record, skip_mqtt_if_invalid=False)
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to store packet record (record_packet_only): {e}")
|
||||
return
|
||||
@@ -487,7 +487,7 @@ class RepeaterHandler(BaseHandler):
|
||||
|
||||
if self.storage:
|
||||
try:
|
||||
self.storage.record_packet(packet_record, skip_letsmesh_if_invalid=False)
|
||||
self.storage.record_packet(packet_record, skip_mqtt_if_invalid=False)
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to store duplicate record: {e}")
|
||||
|
||||
@@ -1145,7 +1145,7 @@ class RepeaterHandler(BaseHandler):
|
||||
"unscoped_flood_allow": self.config.get("mesh", {}).get("unscoped_flood_allow", self.config.get("mesh", {}).get("global_flood_allow", True)),
|
||||
"path_hash_mode": self.config.get("mesh", {}).get("path_hash_mode", 0),
|
||||
},
|
||||
"letsmesh": self.config.get("letsmesh", {}),
|
||||
"mqtt_brokers": self.config.get("mqtt_brokers", {}),
|
||||
},
|
||||
"public_key": None,
|
||||
}
|
||||
|
||||
@@ -54,8 +54,8 @@ logger = logging.getLogger("HTTPServer")
|
||||
# POST /api/update_duty_cycle_config {"enabled": true, "on_time": 300, "off_time": 60} - Update duty cycle config
|
||||
# POST /api/update_radio_config - Update radio configuration
|
||||
# POST /api/update_advert_rate_limit_config - Update advert rate limiting settings
|
||||
# GET /api/letsmesh_status - Get LetsMesh Observer connection status
|
||||
# POST /api/update_letsmesh_config - Update LetsMesh Observer configuration
|
||||
# GET /api/mqtt_status - Get MQTT Observer connection status
|
||||
# POST /api/update_mqtt_config - Update MQTT Observer configuration
|
||||
|
||||
# Packets
|
||||
# GET /api/packet_stats?hours=24 - Get packet statistics
|
||||
@@ -999,18 +999,17 @@ class APIEndpoints:
|
||||
|
||||
@cherrypy.expose
|
||||
@cherrypy.tools.json_out()
|
||||
def letsmesh_status(self):
|
||||
"""Get LetsMesh connection status and configuration."""
|
||||
def mqtt_status(self):
|
||||
"""Get MQTT connection status and configuration."""
|
||||
self._set_cors_headers()
|
||||
try:
|
||||
letsmesh_cfg = self.config.get("letsmesh", {})
|
||||
enabled = letsmesh_cfg.get("enabled", False)
|
||||
# mqtt_cfg = self.config.get("mqtt_brokers", {})
|
||||
|
||||
# Walk the chain to the letsmesh_handler
|
||||
# Walk the chain to the mqtt_handler
|
||||
handler = None
|
||||
try:
|
||||
storage = self._get_storage()
|
||||
handler = getattr(storage, "letsmesh_handler", None)
|
||||
handler = getattr(storage, "mqtt_handler", None)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
@@ -1018,36 +1017,40 @@ class APIEndpoints:
|
||||
if handler:
|
||||
for conn in getattr(handler, "connections", []):
|
||||
connected_brokers.append({
|
||||
"enabled": conn.enabled,
|
||||
"name": conn.broker.get("name", ""),
|
||||
"host": conn.broker.get("host", ""),
|
||||
"connected": conn.is_connected(),
|
||||
"reconnecting": conn.has_pending_reconnect(),
|
||||
"status": {
|
||||
"connected": conn.is_connected(),
|
||||
"reconnecting": conn.has_pending_reconnect(),
|
||||
},
|
||||
"format": conn.format
|
||||
})
|
||||
|
||||
return self._success({
|
||||
"enabled": enabled,
|
||||
"handler_active": handler is not None,
|
||||
"brokers": connected_brokers,
|
||||
})
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting LetsMesh status: {e}")
|
||||
logger.error(f"Error getting MQTT status: {e}")
|
||||
return self._error(str(e))
|
||||
|
||||
@cherrypy.expose
|
||||
@cherrypy.tools.json_out()
|
||||
@cherrypy.tools.json_in()
|
||||
def update_letsmesh_config(self):
|
||||
"""Update LetsMesh Observer configuration.
|
||||
def update_mqtt_config(self):
|
||||
"""Update MQTT Observer configuration.
|
||||
|
||||
POST /api/update_letsmesh_config
|
||||
POST /api/update_mqtt_config
|
||||
Body: {
|
||||
"enabled": true,
|
||||
"iata_code": "SFO",
|
||||
"broker_index": 0,
|
||||
"status_interval": 300,
|
||||
"owner": "Callsign",
|
||||
"email": "user@example.com",
|
||||
"disallowed_packet_types": ["ACK"]
|
||||
"brokers": [
|
||||
{
|
||||
|
||||
}]
|
||||
}
|
||||
"""
|
||||
self._set_cors_headers()
|
||||
@@ -1062,55 +1065,72 @@ class APIEndpoints:
|
||||
if not data:
|
||||
return self._error("No configuration updates provided")
|
||||
|
||||
letsmesh_updates = {}
|
||||
mqtt_updates = {}
|
||||
|
||||
if "enabled" in data:
|
||||
letsmesh_updates["enabled"] = bool(data["enabled"])
|
||||
if "iata_code" in data:
|
||||
letsmesh_updates["iata_code"] = str(data["iata_code"]).strip()
|
||||
if "broker_index" in data:
|
||||
letsmesh_updates["broker_index"] = int(data["broker_index"])
|
||||
mqtt_updates["iata_code"] = str(data["iata_code"]).strip()
|
||||
if "status_interval" in data:
|
||||
letsmesh_updates["status_interval"] = max(60, int(data["status_interval"]))
|
||||
mqtt_updates["status_interval"] = max(60, int(data["status_interval"]))
|
||||
if "owner" in data:
|
||||
letsmesh_updates["owner"] = str(data["owner"]).strip()
|
||||
mqtt_updates["owner"] = str(data["owner"]).strip()
|
||||
if "email" in data:
|
||||
letsmesh_updates["email"] = str(data["email"]).strip()
|
||||
if "disallowed_packet_types" in data:
|
||||
letsmesh_updates["disallowed_packet_types"] = list(data["disallowed_packet_types"])
|
||||
if "additional_brokers" in data:
|
||||
brokers = data["additional_brokers"]
|
||||
mqtt_updates["email"] = str(data["email"]).strip()
|
||||
# if "disallowed_packet_types" in data:
|
||||
# mqtt_updates["disallowed_packet_types"] = list(data["disallowed_packet_types"])
|
||||
if "brokers" in data:
|
||||
brokers = data["brokers"]
|
||||
if not isinstance(brokers, list):
|
||||
return self._error("additional_brokers must be a list")
|
||||
return self._error("brokers must be a list")
|
||||
validated = []
|
||||
for i, b in enumerate(brokers):
|
||||
if not isinstance(b, dict):
|
||||
return self._error(f"Broker at index {i} must be an object")
|
||||
for field in ("name", "host", "audience"):
|
||||
if not b.get(field, "").strip():
|
||||
for field in ("name", "host", "port", "format"):
|
||||
if not b.get(field, ""):
|
||||
return self._error(f"Broker at index {i} missing required field: {field}")
|
||||
|
||||
try:
|
||||
port = int(b.get("port", 443))
|
||||
except (ValueError, TypeError):
|
||||
return self._error(f"Broker at index {i} has invalid port")
|
||||
validated.append({
|
||||
"name": str(b["name"]).strip(),
|
||||
"host": str(b["host"]).strip(),
|
||||
"port": port,
|
||||
"audience": str(b["audience"]).strip(),
|
||||
})
|
||||
letsmesh_updates["additional_brokers"] = validated
|
||||
|
||||
new_broker = {
|
||||
"name": str(b["name"]).strip(),
|
||||
"enabled": b.get("enabled", False),
|
||||
"transport": str(b.get("transport", "websockets")).strip(),
|
||||
"host": str(b["host"]).strip(),
|
||||
"port": port,
|
||||
"format": str(b["format"]).strip(),
|
||||
"disallowed_packet_types": list(b.get("disallowed_packet_types", [])),
|
||||
"retain_status": bool(b.get("retain_status", False)),
|
||||
"tls": {
|
||||
"enabled": bool(b.get("tls", {}).get("enabled", True if port == 443 else False)),
|
||||
"insecure": bool(b.get("tls", {}).get("insecure", False)),
|
||||
}
|
||||
}
|
||||
|
||||
if b.get("use_jwt_auth", False):
|
||||
new_broker["use_jwt_auth"] = True
|
||||
new_broker["audience"] = str(b["audience"]).strip()
|
||||
else:
|
||||
new_broker["use_jwt_auth"] = False
|
||||
new_broker["username"] = b.get("username", None)
|
||||
new_broker["password"] = b.get("password", None)
|
||||
|
||||
if not letsmesh_updates:
|
||||
validated.append(new_broker)
|
||||
|
||||
mqtt_updates["brokers"] = validated
|
||||
|
||||
if not mqtt_updates:
|
||||
return self._error("No valid settings provided")
|
||||
|
||||
result = self.config_manager.update_and_save(
|
||||
updates={"letsmesh": letsmesh_updates},
|
||||
live_update=False, # Restart required for LetsMesh handler changes
|
||||
updates={"mqtt_brokers": mqtt_updates, "mqtt": None, "letsmesh": None},
|
||||
live_update=False, # Restart required for MQTT handler changes
|
||||
)
|
||||
|
||||
if result.get("success"):
|
||||
logger.info(f"LetsMesh config updated: {list(letsmesh_updates.keys())}")
|
||||
logger.info(f"MQTT config updated: {list(mqtt_updates.keys())}")
|
||||
return self._success({
|
||||
"persisted": result.get("saved", False),
|
||||
"restart_required": True,
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1 @@
|
||||
.bg-gradient-light[data-v-63d1c99c]{background:linear-gradient(#0ea5e966,#06b6d44d)}.bg-gradient-dark[data-v-63d1c99c]{background:linear-gradient(#67e8f94d,#a5f3fc26)}.login-card[data-v-63d1c99c]{-webkit-backdrop-filter:blur(40px)saturate(180%);background:#ffffffb3}.dark .login-card[data-v-63d1c99c]{background:#11191c66}.input-glass[data-v-63d1c99c]{-webkit-backdrop-filter:blur(20px);background:#ffffffe6;border:1px solid #d1d5db}.dark .input-glass[data-v-63d1c99c]{background:#ffffff0d;border-color:#ffffff1a}.input-glass[data-v-63d1c99c]:focus{background:#fff}.dark .input-glass[data-v-63d1c99c]:focus{background:#ffffff1a}.input-glass[data-v-63d1c99c]:focus{box-shadow:0 0 0 1px #aae8e833,0 0 20px #aae8e826,inset 0 1px #ffffff1a}.input-glow[data-v-63d1c99c]{opacity:0;transition:opacity .3s;box-shadow:inset 0 1px #ffffff0d}.input-glass:focus+.input-glow[data-v-63d1c99c]{opacity:1;box-shadow:0 0 20px #aae8e833,inset 0 1px #ffffff1a}.button-glass[data-v-63d1c99c]{-webkit-backdrop-filter:blur(20px);position:relative}.button-glass[data-v-63d1c99c]:before{content:"";-webkit-mask-composite:xor;background:linear-gradient(90deg,#0000 0%,#aae8e84d 50%,#0000 100%);border-radius:12px;padding:1px;transition:transform 1s;position:absolute;inset:0;transform:translate(-100%);-webkit-mask-image:linear-gradient(#fff 0 0),linear-gradient(#fff 0 0);-webkit-mask-position:0 0,0 0;-webkit-mask-size:auto,auto;-webkit-mask-repeat:repeat,repeat;-webkit-mask-clip:content-box,border-box;-webkit-mask-origin:content-box,border-box;-webkit-mask-composite:xor;mask-composite:exclude;-webkit-mask-source-type:auto,auto;mask-mode:match-source,match-source}.button-glass[data-v-63d1c99c]:hover:not(:disabled):before{transform:translate(100%)}.button-glass[data-v-63d1c99c]{box-shadow:0 0 0 1px #aae8e833,0 4px 16px #0003,inset 0 1px #ffffff1a}.button-glass[data-v-63d1c99c]:hover:not(:disabled){box-shadow:0 0 0 1px #aae8e866,0 0 30px #aae8e84d,0 4px 20px #0000004d,inset 0 1px #ffffff26}.login-content:has(.button-glass:hover:not(:disabled)) .logo-image[data-v-63d1c99c]{filter:brightness(1.4)drop-shadow(0 0 12px #aae8e8b3);transform:scale(1.02)}.login-content:has(.button-glass:hover:not(:disabled)) .logo-glow[data-v-63d1c99c]{opacity:.6;transform:scale(1.15)}.logo-glow[data-v-63d1c99c]{opacity:0}.dark .logo-glow[data-v-63d1c99c]{opacity:1}@keyframes float-63d1c99c{0%,to{transform:translateY(0)}50%{transform:translateY(-10px)}}@keyframes pulse-slow-63d1c99c{0%,to{opacity:.8;transform:scale(1)}50%{opacity:.6;transform:scale(1.05)}}@keyframes pulse-slower-63d1c99c{0%,to{opacity:.75;transform:scale(1)}50%{opacity:.5;transform:scale(1.08)}}@keyframes pulse-slowest-63d1c99c{0%,to{opacity:.8;transform:scale(1)}50%{opacity:.6;transform:scale(1.06)}}.animate-pulse-slow[data-v-63d1c99c]{animation:8s ease-in-out infinite pulse-slow-63d1c99c}.animate-pulse-slower[data-v-63d1c99c]{animation:10s ease-in-out infinite pulse-slower-63d1c99c}.animate-pulse-slowest[data-v-63d1c99c]{animation:12s ease-in-out infinite pulse-slowest-63d1c99c}@keyframes shake-63d1c99c{0%,to{transform:translate(0)}10%,30%,50%,70%,90%{transform:translate(-5px)}20%,40%,60%,80%{transform:translate(5px)}}.animate-shake[data-v-63d1c99c]{animation:.5s ease-in-out shake-63d1c99c}.form-group[data-v-63d1c99c]{position:relative}.form-group:hover label[data-v-63d1c99c]{color:#aae8e8e6;transition:color .3s}
|
||||
@@ -0,0 +1 @@
|
||||
import{n as e}from"./index-cutq4vvY.js";export{e as default};
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user