mirror of
https://github.com/ipnet-mesh/meshcore-hub.git
synced 2026-03-28 17:42:56 +01:00
Fix MQTT authentication and integrate meshcore library
CLI fixes: - Add --mqtt-username and --mqtt-password options to receiver/sender shortcut commands so they work with authenticated MQTT brokers - These options read from MQTT_USERNAME/MQTT_PASSWORD env vars Device integration: - Integrate with meshcore>=2.2.0 library for actual serial device support - Implement async-to-sync bridge for meshcore's async API - Add proper event subscription mapping between meshcore and hub events - Add meshcore>=2.2.0 to dependencies in pyproject.toml
This commit is contained in:
@@ -38,6 +38,7 @@ dependencies = [
|
||||
"python-multipart>=0.0.6",
|
||||
"httpx>=0.25.0",
|
||||
"aiosqlite>=0.19.0",
|
||||
"meshcore>=2.2.0",
|
||||
]
|
||||
|
||||
[project.optional-dependencies]
|
||||
|
||||
@@ -189,6 +189,20 @@ def run(
|
||||
envvar="MQTT_PORT",
|
||||
help="MQTT broker port",
|
||||
)
|
||||
@click.option(
|
||||
"--mqtt-username",
|
||||
type=str,
|
||||
default=None,
|
||||
envvar="MQTT_USERNAME",
|
||||
help="MQTT username",
|
||||
)
|
||||
@click.option(
|
||||
"--mqtt-password",
|
||||
type=str,
|
||||
default=None,
|
||||
envvar="MQTT_PASSWORD",
|
||||
help="MQTT password",
|
||||
)
|
||||
@click.option(
|
||||
"--prefix",
|
||||
type=str,
|
||||
@@ -202,6 +216,8 @@ def receiver(
|
||||
mock: bool,
|
||||
mqtt_host: str,
|
||||
mqtt_port: int,
|
||||
mqtt_username: str | None,
|
||||
mqtt_password: str | None,
|
||||
prefix: str,
|
||||
) -> None:
|
||||
"""Run interface in RECEIVER mode.
|
||||
@@ -221,6 +237,8 @@ def receiver(
|
||||
mock=mock,
|
||||
mqtt_host=mqtt_host,
|
||||
mqtt_port=mqtt_port,
|
||||
mqtt_username=mqtt_username,
|
||||
mqtt_password=mqtt_password,
|
||||
mqtt_prefix=prefix,
|
||||
)
|
||||
|
||||
@@ -261,6 +279,20 @@ def receiver(
|
||||
envvar="MQTT_PORT",
|
||||
help="MQTT broker port",
|
||||
)
|
||||
@click.option(
|
||||
"--mqtt-username",
|
||||
type=str,
|
||||
default=None,
|
||||
envvar="MQTT_USERNAME",
|
||||
help="MQTT username",
|
||||
)
|
||||
@click.option(
|
||||
"--mqtt-password",
|
||||
type=str,
|
||||
default=None,
|
||||
envvar="MQTT_PASSWORD",
|
||||
help="MQTT password",
|
||||
)
|
||||
@click.option(
|
||||
"--prefix",
|
||||
type=str,
|
||||
@@ -274,6 +306,8 @@ def sender(
|
||||
mock: bool,
|
||||
mqtt_host: str,
|
||||
mqtt_port: int,
|
||||
mqtt_username: str | None,
|
||||
mqtt_password: str | None,
|
||||
prefix: str,
|
||||
) -> None:
|
||||
"""Run interface in SENDER mode.
|
||||
@@ -293,5 +327,7 @@ def sender(
|
||||
mock=mock,
|
||||
mqtt_host=mqtt_host,
|
||||
mqtt_port=mqtt_port,
|
||||
mqtt_username=mqtt_username,
|
||||
mqtt_password=mqtt_password,
|
||||
mqtt_prefix=prefix,
|
||||
)
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
"""MeshCore device wrapper for serial communication."""
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
import time
|
||||
from abc import ABC, abstractmethod
|
||||
@@ -210,12 +211,23 @@ class BaseMeshCoreDevice(ABC):
|
||||
logger.error(f"Error in event handler for {event_type.value}: {e}")
|
||||
|
||||
|
||||
class MeshCoreDevice(BaseMeshCoreDevice):
|
||||
"""Real MeshCore device implementation using meshcore_py library.
|
||||
# Map meshcore library EventType to our EventType
|
||||
MESHCORE_EVENT_MAP = {
|
||||
"advertisement": EventType.ADVERTISEMENT,
|
||||
"contact_message": EventType.CONTACT_MSG_RECV,
|
||||
"channel_message": EventType.CHANNEL_MSG_RECV,
|
||||
"trace_data": EventType.TRACE_DATA,
|
||||
"telemetry_response": EventType.TELEMETRY_RESPONSE,
|
||||
"contacts": EventType.CONTACTS,
|
||||
"message_sent": EventType.SEND_CONFIRMED,
|
||||
"status_response": EventType.STATUS_RESPONSE,
|
||||
"battery_info": EventType.BATTERY,
|
||||
"path_update": EventType.PATH_UPDATED,
|
||||
}
|
||||
|
||||
Note: This is a placeholder implementation. The actual implementation
|
||||
would use the meshcore_py library for serial communication.
|
||||
"""
|
||||
|
||||
class MeshCoreDevice(BaseMeshCoreDevice):
|
||||
"""Real MeshCore device implementation using meshcore library."""
|
||||
|
||||
def __init__(self, config: DeviceConfig):
|
||||
"""Initialize real device.
|
||||
@@ -225,42 +237,115 @@ class MeshCoreDevice(BaseMeshCoreDevice):
|
||||
"""
|
||||
super().__init__(config)
|
||||
self._running = False
|
||||
self._device = None
|
||||
self._mc = None
|
||||
self._loop = None
|
||||
self._subscriptions = []
|
||||
|
||||
def connect(self) -> bool:
|
||||
"""Connect to the MeshCore device."""
|
||||
try:
|
||||
# Note: In actual implementation, this would use meshcore_py
|
||||
# from meshcore_py import MeshCore
|
||||
# self._device = MeshCore(self.config.port, self.config.baud)
|
||||
# self._device.connect()
|
||||
# self._public_key = self._device.get_public_key()
|
||||
|
||||
logger.info(f"Connecting to MeshCore device on {self.config.port}")
|
||||
|
||||
# Placeholder: In real implementation, connect via meshcore_py
|
||||
# For now, we simulate connection failure since we don't have
|
||||
# the actual device/library available
|
||||
logger.warning(
|
||||
"Real MeshCore device not available. "
|
||||
"Use --mock flag for testing."
|
||||
from meshcore import MeshCore, SerialConnection
|
||||
from meshcore import EventType as MCEventType
|
||||
except ImportError:
|
||||
logger.error(
|
||||
"meshcore library not installed. "
|
||||
"Install with: pip install meshcore"
|
||||
)
|
||||
return False
|
||||
|
||||
try:
|
||||
logger.info(f"Connecting to MeshCore device on {self.config.port}")
|
||||
|
||||
# Create event loop if needed
|
||||
try:
|
||||
self._loop = asyncio.get_event_loop()
|
||||
except RuntimeError:
|
||||
self._loop = asyncio.new_event_loop()
|
||||
asyncio.set_event_loop(self._loop)
|
||||
|
||||
# Create serial connection and MeshCore instance
|
||||
cx = SerialConnection(
|
||||
self.config.port,
|
||||
baudrate=self.config.baud,
|
||||
)
|
||||
self._mc = MeshCore(cx, auto_reconnect=True)
|
||||
|
||||
# Connect asynchronously
|
||||
self._loop.run_until_complete(self._mc.connect())
|
||||
|
||||
# Get device info to retrieve public key
|
||||
async def get_self_info():
|
||||
from meshcore import EventType as MCEventType
|
||||
# Wait for SELF_INFO event
|
||||
event = await self._mc.wait_for_event(
|
||||
MCEventType.SELF_INFO,
|
||||
timeout=5.0
|
||||
)
|
||||
if event:
|
||||
return event.attributes.get("public_key")
|
||||
return None
|
||||
|
||||
self._public_key = self._loop.run_until_complete(get_self_info())
|
||||
|
||||
if not self._public_key:
|
||||
logger.warning("Could not retrieve device public key")
|
||||
|
||||
self._connected = True
|
||||
logger.info(f"Connected to MeshCore device, public_key: {self._public_key}")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to connect to device: {e}")
|
||||
return False
|
||||
|
||||
def _setup_event_subscriptions(self) -> None:
|
||||
"""Set up event subscriptions for the meshcore library."""
|
||||
if not self._mc:
|
||||
return
|
||||
|
||||
from meshcore import EventType as MCEventType
|
||||
|
||||
# Map of meshcore event types to subscribe to
|
||||
event_map = {
|
||||
MCEventType.ADVERTISEMENT: EventType.ADVERTISEMENT,
|
||||
MCEventType.CONTACT_MSG_RECV: EventType.CONTACT_MSG_RECV,
|
||||
MCEventType.CHANNEL_MSG_RECV: EventType.CHANNEL_MSG_RECV,
|
||||
MCEventType.TRACE_DATA: EventType.TRACE_DATA,
|
||||
MCEventType.TELEMETRY_RESPONSE: EventType.TELEMETRY_RESPONSE,
|
||||
MCEventType.CONTACTS: EventType.CONTACTS,
|
||||
MCEventType.MSG_SENT: EventType.SEND_CONFIRMED,
|
||||
MCEventType.STATUS_RESPONSE: EventType.STATUS_RESPONSE,
|
||||
MCEventType.BATTERY: EventType.BATTERY,
|
||||
MCEventType.PATH_UPDATE: EventType.PATH_UPDATED,
|
||||
}
|
||||
|
||||
for mc_event_type, our_event_type in event_map.items():
|
||||
async def callback(event, et=our_event_type):
|
||||
# Convert event to dict and dispatch
|
||||
payload = dict(event.attributes) if hasattr(event, 'attributes') else {}
|
||||
self._dispatch_event(et, payload)
|
||||
|
||||
sub = self._mc.subscribe(mc_event_type, callback)
|
||||
self._subscriptions.append(sub)
|
||||
logger.debug(f"Subscribed to {mc_event_type.name}")
|
||||
|
||||
def disconnect(self) -> None:
|
||||
"""Disconnect from the device."""
|
||||
if self._device:
|
||||
if self._mc:
|
||||
try:
|
||||
# self._device.disconnect()
|
||||
pass
|
||||
# Unsubscribe from events
|
||||
for sub in self._subscriptions:
|
||||
self._mc.unsubscribe(sub)
|
||||
self._subscriptions.clear()
|
||||
|
||||
# Disconnect
|
||||
if self._loop:
|
||||
self._loop.run_until_complete(self._mc.disconnect())
|
||||
except Exception as e:
|
||||
logger.error(f"Error disconnecting: {e}")
|
||||
|
||||
self._connected = False
|
||||
self._device = None
|
||||
self._mc = None
|
||||
logger.info("Disconnected from MeshCore device")
|
||||
|
||||
def send_message(
|
||||
@@ -270,13 +355,16 @@ class MeshCoreDevice(BaseMeshCoreDevice):
|
||||
timestamp: Optional[int] = None,
|
||||
) -> bool:
|
||||
"""Send a direct message."""
|
||||
if not self._connected or not self._device:
|
||||
if not self._connected or not self._mc:
|
||||
logger.error("Cannot send message: not connected")
|
||||
return False
|
||||
|
||||
try:
|
||||
ts = timestamp or int(time.time())
|
||||
# self._device.send_message(destination, text, ts)
|
||||
async def _send():
|
||||
from meshcore.commands import send_msg
|
||||
await send_msg(self._mc, destination, text)
|
||||
|
||||
self._loop.run_until_complete(_send())
|
||||
logger.info(f"Sent message to {destination[:12]}...")
|
||||
return True
|
||||
except Exception as e:
|
||||
@@ -290,13 +378,16 @@ class MeshCoreDevice(BaseMeshCoreDevice):
|
||||
timestamp: Optional[int] = None,
|
||||
) -> bool:
|
||||
"""Send a channel message."""
|
||||
if not self._connected or not self._device:
|
||||
if not self._connected or not self._mc:
|
||||
logger.error("Cannot send channel message: not connected")
|
||||
return False
|
||||
|
||||
try:
|
||||
ts = timestamp or int(time.time())
|
||||
# self._device.send_channel_message(channel_idx, text, ts)
|
||||
async def _send():
|
||||
from meshcore.commands import send_channel_msg
|
||||
await send_channel_msg(self._mc, channel_idx, text)
|
||||
|
||||
self._loop.run_until_complete(_send())
|
||||
logger.info(f"Sent message to channel {channel_idx}")
|
||||
return True
|
||||
except Exception as e:
|
||||
@@ -305,12 +396,16 @@ class MeshCoreDevice(BaseMeshCoreDevice):
|
||||
|
||||
def send_advertisement(self, flood: bool = True) -> bool:
|
||||
"""Send a node advertisement."""
|
||||
if not self._connected or not self._device:
|
||||
if not self._connected or not self._mc:
|
||||
logger.error("Cannot send advertisement: not connected")
|
||||
return False
|
||||
|
||||
try:
|
||||
# self._device.send_advertisement(flood)
|
||||
async def _send():
|
||||
from meshcore.commands import send_advert
|
||||
await send_advert(self._mc, flood=flood)
|
||||
|
||||
self._loop.run_until_complete(_send())
|
||||
logger.info(f"Sent advertisement (flood={flood})")
|
||||
return True
|
||||
except Exception as e:
|
||||
@@ -319,12 +414,16 @@ class MeshCoreDevice(BaseMeshCoreDevice):
|
||||
|
||||
def request_status(self, target: Optional[str] = None) -> bool:
|
||||
"""Request status from a node."""
|
||||
if not self._connected or not self._device:
|
||||
if not self._connected or not self._mc:
|
||||
logger.error("Cannot request status: not connected")
|
||||
return False
|
||||
|
||||
try:
|
||||
# self._device.request_status(target)
|
||||
async def _request():
|
||||
from meshcore.commands import request_status
|
||||
await request_status(self._mc, target)
|
||||
|
||||
self._loop.run_until_complete(_request())
|
||||
logger.info(f"Requested status from {target or 'self'}")
|
||||
return True
|
||||
except Exception as e:
|
||||
@@ -333,12 +432,16 @@ class MeshCoreDevice(BaseMeshCoreDevice):
|
||||
|
||||
def request_telemetry(self, target: str) -> bool:
|
||||
"""Request telemetry from a node."""
|
||||
if not self._connected or not self._device:
|
||||
if not self._connected or not self._mc:
|
||||
logger.error("Cannot request telemetry: not connected")
|
||||
return False
|
||||
|
||||
try:
|
||||
# self._device.request_telemetry(target)
|
||||
async def _request():
|
||||
from meshcore.commands import request_telemetry
|
||||
await request_telemetry(self._mc, target)
|
||||
|
||||
self._loop.run_until_complete(_request())
|
||||
logger.info(f"Requested telemetry from {target[:12]}...")
|
||||
return True
|
||||
except Exception as e:
|
||||
@@ -350,22 +453,26 @@ class MeshCoreDevice(BaseMeshCoreDevice):
|
||||
self._running = True
|
||||
logger.info("Starting device event loop")
|
||||
|
||||
while self._running and self._connected:
|
||||
try:
|
||||
# In actual implementation:
|
||||
# event = self._device.poll_event()
|
||||
# if event:
|
||||
# event_type = EventType(event.type)
|
||||
# self._dispatch_event(event_type, event.payload)
|
||||
time.sleep(0.1)
|
||||
except Exception as e:
|
||||
logger.error(f"Error in event loop: {e}")
|
||||
# Set up event subscriptions
|
||||
self._setup_event_subscriptions()
|
||||
|
||||
# Run the async event loop
|
||||
async def _run_loop():
|
||||
while self._running and self._connected:
|
||||
await asyncio.sleep(0.1)
|
||||
|
||||
try:
|
||||
self._loop.run_until_complete(_run_loop())
|
||||
except Exception as e:
|
||||
logger.error(f"Error in event loop: {e}")
|
||||
|
||||
logger.info("Device event loop stopped")
|
||||
|
||||
def stop(self) -> None:
|
||||
"""Stop the device event loop."""
|
||||
self._running = False
|
||||
if self._mc:
|
||||
self._mc.stop()
|
||||
logger.info("Stopping device event loop")
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user