Add Phase 0 host-bridge: relay text across Meshtastic, MeshCore, Reticulum
This commit is contained in:
@@ -0,0 +1,83 @@
|
||||
"""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)
|
||||
Reference in New Issue
Block a user