Fix meshcore library integration and add device initialization

- Fix event payload extraction: use event.payload instead of event.attributes
  to get full message data (text, pubkey_prefix, etc.)
- Fix command API: use mc.commands.* pattern instead of importing functions
- Add device initialization on receiver startup:
  - Set hardware clock to current Unix timestamp
  - Send local (non-flood) advertisement
  - Start automatic message fetching via start_auto_message_fetching()
- Add set_time() and start_message_fetching() methods to device interface
- Update AGENTS.md with meshcore library integration documentation
- Update README.md with correct meshcore library reference

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Louis King
2025-12-03 00:47:48 +00:00
parent 5a1b4fb855
commit 8964e1b2d4
5 changed files with 165 additions and 14 deletions
+53 -2
View File
@@ -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/)
+1 -1
View File
@@ -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
+65 -11
View File
@@ -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
+18
View File
@@ -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
+28
View File
@@ -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: