mirror of
https://github.com/ipnet-mesh/meshcore-hub.git
synced 2026-08-06 17:02:59 +02:00
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:
@@ -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/)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user