diff --git a/AGENTS.md b/AGENTS.md index 32941f6..cf7b328 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -31,7 +31,7 @@ MeshCore Hub is a Python 3.11+ monorepo for managing and orchestrating MeshCore | Migrations | Alembic | | REST API | FastAPI | | MQTT Client | paho-mqtt | -| MeshCore Interface | meshcore-py | +| MeshCore Interface | meshcore | | Templates | Jinja2 | | CSS Framework | Tailwind CSS + DaisyUI | | Testing | pytest, pytest-asyncio | @@ -432,9 +432,60 @@ logging.basicConfig(level=logging.DEBUG) export LOG_LEVEL=DEBUG ``` +## MeshCore Library Integration + +The interface component uses the `meshcore` Python library to communicate with MeshCore devices. Key patterns: + +### Device Commands + +Commands are accessed via `mc.commands.*` on the MeshCore instance: + +```python +# Set device time +await mc.commands.set_time(unix_timestamp) + +# Send advertisement +await mc.commands.send_advert(flood=False) + +# Send messages +await mc.commands.send_msg(destination, text) +await mc.commands.send_chan_msg(channel_idx, text) + +# Request data +await mc.commands.send_statusreq(target) +await mc.commands.send_telemetry_req(target) +``` + +### Event Subscription + +Events are received via the subscription system. The `Event` object has: +- `event.type` - The event type enum +- `event.payload` - Full event data (dict with all fields like `text`, `pubkey_prefix`, etc.) +- `event.attributes` - Subset of fields for filtering + +**Important**: Use `event.payload` (not `event.attributes`) to get full message data. + +### Auto Message Fetching + +The library requires explicit message fetching. Call `start_auto_message_fetching()` to: +1. Subscribe to `MESSAGES_WAITING` events +2. Automatically call `get_msg()` to fetch pending messages +3. Immediately fetch any queued messages on startup + +```python +await mc.start_auto_message_fetching() +``` + +### Receiver Initialization + +On startup, the receiver performs these initialization steps: +1. Set device clock to current Unix timestamp +2. Send a local (non-flood) advertisement +3. Start automatic message fetching + ## References -- [meshcore_py Documentation](https://github.com/meshcore-dev/meshcore_py) +- [meshcore Documentation](https://github.com/fdlamotte/meshcore) - [FastAPI Documentation](https://fastapi.tiangolo.com/) - [SQLAlchemy 2.0 Documentation](https://docs.sqlalchemy.org/en/20/) - [Pydantic Documentation](https://docs.pydantic.dev/) diff --git a/README.md b/README.md index 79c2986..4a7490b 100644 --- a/README.md +++ b/README.md @@ -316,4 +316,4 @@ See [LICENSE](LICENSE) for details. ## Acknowledgments - [MeshCore](https://meshcore.dev/) - The mesh networking protocol -- [meshcore_py](https://github.com/meshcore-dev/meshcore_py) - Python library for MeshCore devices +- [meshcore](https://github.com/fdlamotte/meshcore) - Python library for MeshCore devices diff --git a/src/meshcore_hub/interface/device.py b/src/meshcore_hub/interface/device.py index b9e1731..4efc58d 100644 --- a/src/meshcore_hub/interface/device.py +++ b/src/meshcore_hub/interface/device.py @@ -153,6 +153,29 @@ class BaseMeshCoreDevice(ABC): """ pass + @abstractmethod + def set_time(self, timestamp: int) -> bool: + """Set the device's hardware clock. + + Args: + timestamp: Unix timestamp to set + + Returns: + True if time was set successfully + """ + pass + + @abstractmethod + def start_message_fetching(self) -> bool: + """Start automatic message fetching. + + Subscribes to MESSAGES_WAITING events and fetches pending messages. + + Returns: + True if started successfully + """ + pass + @abstractmethod def run(self) -> None: """Run the device event loop (blocking).""" @@ -331,7 +354,9 @@ class MeshCoreDevice(BaseMeshCoreDevice): 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 {} + # Use event.payload for the full data (text, etc.) + # event.attributes only contains filtering fields + payload = dict(event.payload) if hasattr(event, 'payload') and isinstance(event.payload, dict) else {} self._dispatch_event(et, payload) sub = self._mc.subscribe(mc_event_type, callback) @@ -370,8 +395,7 @@ class MeshCoreDevice(BaseMeshCoreDevice): try: async def _send(): - from meshcore.commands import send_msg - await send_msg(self._mc, destination, text) + await self._mc.commands.send_msg(destination, text) self._loop.run_until_complete(_send()) logger.info(f"Sent message to {destination[:12]}...") @@ -393,8 +417,7 @@ class MeshCoreDevice(BaseMeshCoreDevice): try: async def _send(): - from meshcore.commands import send_channel_msg - await send_channel_msg(self._mc, channel_idx, text) + await self._mc.commands.send_chan_msg(channel_idx, text) self._loop.run_until_complete(_send()) logger.info(f"Sent message to channel {channel_idx}") @@ -411,8 +434,7 @@ class MeshCoreDevice(BaseMeshCoreDevice): try: async def _send(): - from meshcore.commands import send_advert - await send_advert(self._mc, flood=flood) + await self._mc.commands.send_advert(flood=flood) self._loop.run_until_complete(_send()) logger.info(f"Sent advertisement (flood={flood})") @@ -429,8 +451,7 @@ class MeshCoreDevice(BaseMeshCoreDevice): try: async def _request(): - from meshcore.commands import request_status - await request_status(self._mc, target) + await self._mc.commands.send_statusreq(target) self._loop.run_until_complete(_request()) logger.info(f"Requested status from {target or 'self'}") @@ -447,8 +468,7 @@ class MeshCoreDevice(BaseMeshCoreDevice): try: async def _request(): - from meshcore.commands import request_telemetry - await request_telemetry(self._mc, target) + await self._mc.commands.send_telemetry_req(target) self._loop.run_until_complete(_request()) logger.info(f"Requested telemetry from {target[:12]}...") @@ -457,6 +477,40 @@ class MeshCoreDevice(BaseMeshCoreDevice): logger.error(f"Failed to request telemetry: {e}") return False + def set_time(self, timestamp: int) -> bool: + """Set the device's hardware clock.""" + if not self._connected or not self._mc: + logger.error("Cannot set time: not connected") + return False + + try: + async def _set_time(): + await self._mc.commands.set_time(timestamp) + + self._loop.run_until_complete(_set_time()) + logger.info(f"Set device time to {timestamp}") + return True + except Exception as e: + logger.error(f"Failed to set device time: {e}") + return False + + def start_message_fetching(self) -> bool: + """Start automatic message fetching.""" + if not self._connected or not self._mc: + logger.error("Cannot start message fetching: not connected") + return False + + try: + async def _start_fetching(): + await self._mc.start_auto_message_fetching() + + self._loop.run_until_complete(_start_fetching()) + logger.info("Started automatic message fetching") + return True + except Exception as e: + logger.error(f"Failed to start message fetching: {e}") + return False + def run(self) -> None: """Run the device event loop.""" self._running = True diff --git a/src/meshcore_hub/interface/mock_device.py b/src/meshcore_hub/interface/mock_device.py index f3f2eda..cb1c46d 100644 --- a/src/meshcore_hub/interface/mock_device.py +++ b/src/meshcore_hub/interface/mock_device.py @@ -262,6 +262,24 @@ class MockMeshCoreDevice(BaseMeshCoreDevice): threading.Thread(target=send_telemetry, daemon=True).start() return True + def set_time(self, timestamp: int) -> bool: + """Set the mock device's hardware clock.""" + if not self._connected: + logger.error("Cannot set time: not connected") + return False + + logger.info(f"Mock: Set device time to {timestamp}") + return True + + def start_message_fetching(self) -> bool: + """Start automatic message fetching (mock).""" + if not self._connected: + logger.error("Cannot start message fetching: not connected") + return False + + logger.info("Mock: Started automatic message fetching") + return True + def run(self) -> None: """Run the mock device event loop.""" self._running = True diff --git a/src/meshcore_hub/interface/receiver.py b/src/meshcore_hub/interface/receiver.py index ece9018..78d2281 100644 --- a/src/meshcore_hub/interface/receiver.py +++ b/src/meshcore_hub/interface/receiver.py @@ -9,6 +9,7 @@ In RECEIVER mode, the interface: import logging import signal import threading +import time from typing import Any, Optional from meshcore_hub.common.mqtt import MQTTClient, MQTTConfig @@ -44,6 +45,30 @@ class Receiver: self._running = False self._shutdown_event = threading.Event() + def _initialize_device(self) -> None: + """Initialize device after connection. + + Sets the hardware clock, sends a local advertisement, and starts message fetching. + """ + # Set device time to current Unix timestamp + current_time = int(time.time()) + if self.device.set_time(current_time): + logger.info(f"Synchronized device clock to {current_time}") + else: + logger.warning("Failed to synchronize device clock") + + # Send a local (non-flood) advertisement to announce presence + if self.device.send_advertisement(flood=False): + logger.info("Sent local advertisement") + else: + logger.warning("Failed to send local advertisement") + + # Start automatic message fetching + if self.device.start_message_fetching(): + logger.info("Started automatic message fetching") + else: + logger.warning("Failed to start automatic message fetching") + def _handle_event(self, event_type: EventType, payload: dict[str, Any]) -> None: """Handle device event and publish to MQTT. @@ -98,6 +123,9 @@ class Receiver: logger.info(f"Connected to MeshCore device: {self.device.public_key}") + # Initialize device: set time and send local advertisement + self._initialize_device() + self._running = True def run(self) -> None: