diff --git a/config.yaml.example b/config.yaml.example index 4c4476e..b2f3d9f 100644 --- a/config.yaml.example +++ b/config.yaml.example @@ -144,7 +144,24 @@ letsmesh: enabled: true iata_code: "test" # e.g., "SFO", "LHR", "test" broker_index: 0 # Which LetsMesh broker (0=EU, 1=US West) - status_interval: 60 + status_interval: 60 + + # Block specific packet types from being published to LetsMesh + # 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: [] + # - REQ # Don't publish requests + # - RESPONSE # Don't publish responses + # - TXT_MSG # Don't publish text messages + # - ACK # Don't publish acknowledgments + # - ADVERT # Don't publish advertisements + # - GRP_TXT # Don't publish group text messages + # - GRP_DATA # Don't publish group data + # - ANON_REQ # Don't publish anonymous requests + # - PATH # Don't publish path packets + # - TRACE # Don't publish trace packets + # - RAW_CUSTOM # Don't publish custom raw packets logging: # Log level: DEBUG, INFO, WARNING, ERROR diff --git a/repeater/config.py b/repeater/config.py index 545a3c5..5d4e467 100644 --- a/repeater/config.py +++ b/repeater/config.py @@ -29,13 +29,22 @@ def get_node_info(config: Dict[str, Any]) -> Dict[str, Any]: 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 + 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") + "model": letsmesh_config.get("model", "PyMC-Repeater"), + "disallowed_packet_types": disallowed_hex } diff --git a/repeater/data_acquisition/letsmesh_handler.py b/repeater/data_acquisition/letsmesh_handler.py index 2547664..73c8aa4 100644 --- a/repeater/data_acquisition/letsmesh_handler.py +++ b/repeater/data_acquisition/letsmesh_handler.py @@ -5,6 +5,7 @@ import base64 import paho.mqtt.client as mqtt from datetime import datetime, timedelta, UTC +from dataclasses import dataclass, asdict from nacl.signing import SigningKey from typing import Callable, Optional from .. import __version__ @@ -301,3 +302,80 @@ class MeshCoreToMqttJwtPusher: result = self.client.publish(topic, message, retain=retain) logging.debug(f"Published to {topic}: {message}") return result + + +# ==================================================================== +# LetsMesh Packet Data Class +# ==================================================================== + +@dataclass +class LetsMeshPacket: + """ + Data class for LetsMesh packet format. + Converts internal packet_record format to LetsMesh publish format. + """ + origin: str + origin_id: str + timestamp: str + type: str + direction: str + time: str + date: str + len: str + packet_type: str + route: str + payload_len: str + raw: str + SNR: str + RSSI: str + score: str + duration: str + hash: str + + @classmethod + def from_packet_record(cls, packet_record: dict, origin: str, origin_id: str) -> Optional['LetsMeshPacket']: + """ + Create LetsMeshPacket from internal packet_record format. + + Args: + packet_record: Internal packet record dictionary + origin: Node name + origin_id: Public key of the node + + Returns: + LetsMeshPacket instance or None if raw_packet is missing + """ + if "raw_packet" not in packet_record or not packet_record["raw_packet"]: + return None + + # Extract timestamp and format date/time + timestamp = packet_record.get("timestamp", 0) + dt = datetime.fromtimestamp(timestamp) + + # Format route type (1=Flood->F, 2=Direct->D, etc) + route_map = {1: "F", 2: "D"} + route = route_map.get(packet_record.get("route", 0), str(packet_record.get("route", 0))) + + return cls( + origin=origin, + origin_id=origin_id, + timestamp=dt.isoformat(), + type="PACKET", + direction="rx", + time=dt.strftime("%H:%M:%S"), + date=dt.strftime("%-d/%-m/%Y"), + len=str(len(packet_record["raw_packet"]) // 2), + packet_type=str(packet_record.get("type", 0)), + route=route, + payload_len=str(packet_record.get("payload_length", 0)), + raw=packet_record["raw_packet"], + SNR=str(packet_record.get("snr", 0)), + RSSI=str(packet_record.get("rssi", 0)), + score=str(int(packet_record.get("score", 0) * 1000)), + duration="0", + hash=packet_record.get("packet_hash", "") + ) + + def to_dict(self) -> dict: + """Convert to dictionary for JSON serialization""" + return asdict(self) diff --git a/repeater/data_acquisition/storage_collector.py b/repeater/data_acquisition/storage_collector.py index 6e35ddf..5294ab1 100644 --- a/repeater/data_acquisition/storage_collector.py +++ b/repeater/data_acquisition/storage_collector.py @@ -8,7 +8,7 @@ from typing import Optional, Dict, Any from .sqlite_handler import SQLiteHandler from .rrdtool_handler import RRDToolHandler from .mqtt_handler import MQTTHandler -from .letsmesh_handler import MeshCoreToMqttJwtPusher +from .letsmesh_handler import MeshCoreToMqttJwtPusher, LetsMeshPacket logger = logging.getLogger("StorageCollector") @@ -42,10 +42,23 @@ class StorageCollector: 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"]) + logger.info(f"LetsMesh 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() def _get_live_stats(self) -> dict: """Get live stats from RepeaterHandler""" @@ -75,40 +88,28 @@ class StorageCollector: # Publish to LetsMesh if enabled if self.letsmesh_handler: try: - # Format packet data for LetsMesh publish_packet - if "raw_packet" in packet_record and packet_record["raw_packet"]: - # Extract timestamp and format date/time - timestamp = packet_record.get("timestamp", time.time()) - dt = datetime.fromtimestamp(timestamp) + # Check if packet has type field + if "type" not in packet_record: + logger.error("Cannot publish to LetsMesh: packet_record missing 'type' field") + return + + packet_type = packet_record["type"] + + if packet_type in self.disallowed_packet_types: + logger.debug(f"Skipped publishing packet type 0x{packet_type:02X} (in disallowed types)") + return + + # Create LetsMesh packet from record + node_name = self.config.get("repeater", {}).get("node_name", "Unknown") + letsmesh_packet = LetsMeshPacket.from_packet_record( + packet_record, + origin=node_name, + origin_id=self.letsmesh_handler.public_key + ) + + if letsmesh_packet: + self.letsmesh_handler.publish_packet(letsmesh_packet.to_dict()) - # Get node name from config - node_name = self.config.get("repeater", {}).get("node_name", "Unknown") - - # Format route type (1=Flood->F, 2=Direct->D, etc) - route_map = {1: "F", 2: "D"} - route = route_map.get(packet_record.get("route", 0), str(packet_record.get("route", 0))) - - letsmesh_packet = { - "origin": node_name, - "origin_id": self.letsmesh_handler.public_key, - "timestamp": dt.isoformat(), - "type": "PACKET", - "direction": "rx", - "time": dt.strftime("%H:%M:%S"), - "date": dt.strftime("%-d/%-m/%Y"), - "len": str(len(packet_record["raw_packet"]) // 2), # Raw packet length in bytes - "packet_type": str(packet_record.get("type", 0)), - "route": route, - "payload_len": str(packet_record.get("payload_length", 0)), - "raw": packet_record["raw_packet"], - "SNR": str(packet_record.get("snr", 0)), - "RSSI": str(packet_record.get("rssi", 0)), - "score": str(int(packet_record.get("score", 0) * 1000)), # Convert to integer score - "duration": "0", # Not available in our packet record - "hash": packet_record.get("packet_hash", "") - } - - self.letsmesh_handler.publish_packet(letsmesh_packet) except Exception as e: logger.error(f"Failed to publish packet to LetsMesh: {e}")