mirror of
https://github.com/agessaman/meshcore-bot.git
synced 2026-08-27 21:10:13 +00:00
Merge pull request #241 from ajquick/dev
Add configurable PacketCapture observer name Adds an optional [PacketCapture] observer_name used as the MQTT `origin` for packet and status reporting, letting the observer identity differ from the connected MeshCore device name. Unset keeps the previous behavior. The public key backing origin_id, authentication, and topic resolution is unchanged. CHANGELOG conflict resolved by keeping both Unreleased entries.
This commit is contained in:
@@ -23,6 +23,11 @@ semantic versioning.
|
||||
|
||||
- Migration 23: nullable `snr` / `rssi` columns on `observed_paths` for
|
||||
zero-hop advert rows.
|
||||
- `[PacketCapture] observer_name` — an optional name reported as the `origin` of
|
||||
MQTT packet and status payloads. It lets the observer/analyzer identity differ
|
||||
from the MeshCore RF node, which is useful when one bot name is already taken
|
||||
by the radio's advertised name. Unset (the default) keeps the previous
|
||||
behavior: the connected device name, falling back to `[Bot] bot_name`.
|
||||
|
||||
## [1.0.0] — 2026-08-07
|
||||
|
||||
|
||||
@@ -1860,6 +1860,10 @@ auth_token_method = device
|
||||
# Default: XYZ (invalid if not set)
|
||||
iata = XYZ
|
||||
|
||||
# Optional observer name used in MQTT packet/status payloads.
|
||||
# If unset, the connected device/bot name is used.
|
||||
observer_name =
|
||||
|
||||
# MQTT Broker Configuration
|
||||
# You can configure multiple MQTT brokers by using mqtt1_*, mqtt2_*, mqtt3_*, etc.
|
||||
# Each broker can have independent settings for transport, TLS, authentication, and topics.
|
||||
|
||||
@@ -50,6 +50,10 @@ mqtt_skip_unparseable_packets = true # Skip MQTT when content hash is all zero
|
||||
# Optional: skip MQTT for ADVERT packets whose Ed25519 signature does not verify (damaged or spoofed mesh payload).
|
||||
# Does not affect file/JSONL capture.
|
||||
advert_require_valid_signature = false
|
||||
|
||||
# Optional name reported as the MQTT observer "origin".
|
||||
# Defaults to the connected MeshCore device/bot name.
|
||||
observer_name = CustomObserverBot
|
||||
```
|
||||
|
||||
### Authentication
|
||||
|
||||
@@ -1212,7 +1212,7 @@ class PacketCaptureService(BaseServicePlugin):
|
||||
payload_len = str(max(0, packet_len - 1 - transport_bytes - 1 - path_bytes))
|
||||
|
||||
# Get device name and public key
|
||||
device_name = self._get_bot_name()
|
||||
device_name = self._get_observer_name()
|
||||
if not device_name:
|
||||
device_name = "MeshCore Device"
|
||||
|
||||
@@ -1556,6 +1556,23 @@ class PacketCaptureService(BaseServicePlugin):
|
||||
self.logger.debug(f"Decode error traceback: {traceback.format_exc()}")
|
||||
return None
|
||||
|
||||
def _get_observer_name(self) -> str:
|
||||
"""Get observer name for PacketCapture MQTT reporting.
|
||||
|
||||
Allows the observer/analyzer identity to differ from the
|
||||
MeshCore RF node and bot name.
|
||||
"""
|
||||
observer_name = self.bot.config.get(
|
||||
"PacketCapture",
|
||||
"observer_name",
|
||||
fallback=""
|
||||
).strip()
|
||||
|
||||
if observer_name:
|
||||
return observer_name
|
||||
|
||||
return self._get_bot_name()
|
||||
|
||||
def _get_bot_name(self) -> str:
|
||||
"""Get bot name from device or config.
|
||||
|
||||
@@ -2767,7 +2784,7 @@ class PacketCaptureService(BaseServicePlugin):
|
||||
firmware_info = await self.get_firmware_info()
|
||||
|
||||
# Get device name and public key
|
||||
device_name = self._get_bot_name()
|
||||
device_name = self._get_observer_name()
|
||||
if not device_name:
|
||||
device_name = "MeshCore Device"
|
||||
|
||||
|
||||
@@ -0,0 +1,110 @@
|
||||
"""Tests for PacketCapture observer_name override."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import configparser
|
||||
import logging
|
||||
import types
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
from modules.service_plugins.packet_capture_service import PacketCaptureService
|
||||
|
||||
LOGGER = logging.getLogger("test-packet-capture-observer-name")
|
||||
DEVICE_KEY = "ab" * 32
|
||||
|
||||
|
||||
def build_service(
|
||||
observer_name=None,
|
||||
device_name="DeviceNode",
|
||||
bot_name="BotNode",
|
||||
):
|
||||
"""Build a minimal PacketCaptureService without running __init__."""
|
||||
config = configparser.ConfigParser()
|
||||
|
||||
config["Bot"] = {
|
||||
"bot_name": bot_name,
|
||||
}
|
||||
|
||||
config["PacketCapture"] = {}
|
||||
|
||||
if observer_name is not None:
|
||||
config["PacketCapture"]["observer_name"] = observer_name
|
||||
|
||||
bot = MagicMock()
|
||||
bot.config = config
|
||||
|
||||
bot.meshcore = types.SimpleNamespace(
|
||||
self_info={
|
||||
"name": device_name,
|
||||
"public_key": DEVICE_KEY,
|
||||
}
|
||||
)
|
||||
|
||||
service = object.__new__(PacketCaptureService)
|
||||
service.bot = bot
|
||||
service.logger = LOGGER
|
||||
|
||||
service.debug = False
|
||||
service.decode_payloads = False
|
||||
service.channel_key_store = None
|
||||
|
||||
return service
|
||||
|
||||
|
||||
def test_observer_name_override():
|
||||
service = build_service(
|
||||
observer_name="ObserverStation",
|
||||
device_name="DeviceNode",
|
||||
)
|
||||
|
||||
assert service._get_observer_name() == "ObserverStation"
|
||||
|
||||
|
||||
def test_observer_name_falls_back_to_device_name():
|
||||
service = build_service(
|
||||
device_name="DeviceNode",
|
||||
)
|
||||
|
||||
assert service._get_observer_name() == "DeviceNode"
|
||||
|
||||
|
||||
def test_blank_observer_name_falls_back_to_device_name():
|
||||
service = build_service(
|
||||
observer_name=" ",
|
||||
device_name="DeviceNode",
|
||||
)
|
||||
|
||||
assert service._get_observer_name() == "DeviceNode"
|
||||
|
||||
|
||||
def test_packet_payload_uses_observer_name():
|
||||
service = build_service(
|
||||
observer_name="ObserverStation",
|
||||
device_name="DeviceNode",
|
||||
)
|
||||
|
||||
packet_info = {
|
||||
"route_type": "FLOOD",
|
||||
"payload_type": "ADVERT",
|
||||
"payload_type_value": 4,
|
||||
"payload_bytes": 1,
|
||||
"path_len": 0,
|
||||
"path_byte_length": 0,
|
||||
"path": [],
|
||||
"packet_hash": "0123456789ABCDEF",
|
||||
"has_transport_codes": False,
|
||||
}
|
||||
|
||||
payload = {
|
||||
"snr": 5.0,
|
||||
"rssi": -90,
|
||||
}
|
||||
|
||||
result = service._format_packet_data(
|
||||
"00AA",
|
||||
packet_info,
|
||||
payload,
|
||||
)
|
||||
|
||||
assert result["origin"] == "ObserverStation"
|
||||
assert result["origin_id"] == DEVICE_KEY.upper()
|
||||
@@ -132,7 +132,10 @@ def _service_for_format(decode_payloads: bool) -> PacketCaptureService:
|
||||
svc.debug = False
|
||||
svc.logger = logging.getLogger("test-packet-capture")
|
||||
svc.bot = None
|
||||
svc._get_bot_name = lambda: "TestBot" # type: ignore[method-assign]
|
||||
# _format_packet_data resolves the reported origin through _get_observer_name,
|
||||
# which falls back to _get_bot_name; stub the entry point so these tests stay
|
||||
# about payload decoding and not name resolution.
|
||||
svc._get_observer_name = lambda: "TestBot" # type: ignore[method-assign]
|
||||
if decode_payloads:
|
||||
svc.channel_key_store = ChannelKeyStore()
|
||||
svc.channel_key_store.add_secret(BOT_KEY, "#bot")
|
||||
|
||||
Reference in New Issue
Block a user