84 lines
3.1 KiB
Python
84 lines
3.1 KiB
Python
"""MeshCore adapter — wraps the official `meshcore` (meshcore_py) async client.
|
|
|
|
meshcore_py is asyncio-native (MeshCore.create_serial/create_tcp/create_ble,
|
|
event subscription via meshcore.subscribe(EventType.X, coroutine_handler)).
|
|
The rest of this bridge is thread/callback-based (Meshtastic's pubsub,
|
|
Reticulum's RNS transport thread), so this adapter owns a dedicated event
|
|
loop on a background thread and exposes plain synchronous connect()/send()/
|
|
close() methods, matching the shape of the other two adapters.
|
|
"""
|
|
|
|
import asyncio
|
|
import threading
|
|
from collections.abc import Callable
|
|
|
|
|
|
class MeshCoreAdapter:
|
|
NETWORK = "meshcore"
|
|
|
|
def __init__(
|
|
self,
|
|
on_message: Callable[[str, str, str], None],
|
|
port: str | None = None,
|
|
host: str | None = None,
|
|
tcp_port: int = 4000,
|
|
channel_idx: int = 0,
|
|
):
|
|
"""port: serial device path. host: TCP hostname/IP (mutually exclusive
|
|
with port). channel_idx: which MeshCore public channel to bridge —
|
|
channel 0 is the default public channel on stock firmware.
|
|
"""
|
|
self._on_message = on_message
|
|
self._port = port
|
|
self._host = host
|
|
self._tcp_port = tcp_port
|
|
self._channel_idx = channel_idx
|
|
self._mc = None
|
|
self._loop: asyncio.AbstractEventLoop | None = None
|
|
self._thread: threading.Thread | None = None
|
|
|
|
def connect(self) -> None:
|
|
ready = threading.Event()
|
|
self._thread = threading.Thread(target=self._run_loop, args=(ready,), daemon=True)
|
|
self._thread.start()
|
|
ready.wait(timeout=15)
|
|
|
|
def _run_loop(self, ready: threading.Event) -> None:
|
|
self._loop = asyncio.new_event_loop()
|
|
asyncio.set_event_loop(self._loop)
|
|
self._loop.run_until_complete(self._connect_async())
|
|
ready.set()
|
|
self._loop.run_forever()
|
|
|
|
async def _connect_async(self) -> None:
|
|
from meshcore import EventType, MeshCore
|
|
|
|
if self._host:
|
|
self._mc = await MeshCore.create_tcp(self._host, self._tcp_port)
|
|
else:
|
|
self._mc = await MeshCore.create_serial(self._port or "/dev/ttyUSB0")
|
|
|
|
self._mc.subscribe(EventType.CHANNEL_MSG_RECV, self._handle_channel_msg)
|
|
|
|
async def _handle_channel_msg(self, event) -> None: # noqa: ANN001
|
|
data = event.payload
|
|
if data.get("channel_idx") != self._channel_idx:
|
|
return
|
|
text = data.get("text", "")
|
|
if text:
|
|
# CHANNEL_MSG_RECV carries no sender identity at the protocol
|
|
# level (verified against meshcore/reader.py) — MeshCore apps
|
|
# convey the sender by convention inside the text itself.
|
|
self._on_message(self.NETWORK, "?", text)
|
|
|
|
def send(self, text: str) -> None:
|
|
if self._mc is None or self._loop is None:
|
|
raise RuntimeError("MeshCoreAdapter.send() called before connect()")
|
|
asyncio.run_coroutine_threadsafe(
|
|
self._mc.commands.send_chan_msg(self._channel_idx, text), self._loop
|
|
)
|
|
|
|
def close(self) -> None:
|
|
if self._loop is not None:
|
|
self._loop.call_soon_threadsafe(self._loop.stop)
|