163 lines
4.6 KiB
Python
163 lines
4.6 KiB
Python
"""USB-to-Meshtastic bridge for the XIAO RP2040 curses dashboard.
|
|
|
|
Install this file as ``main.py`` on the XIAO. UART0 talks to the Meshtastic
|
|
radio on D6/D7, while newline-delimited JSON events and commands use USB stdio.
|
|
"""
|
|
|
|
import sys
|
|
|
|
try:
|
|
import ujson as json
|
|
except ImportError:
|
|
import json
|
|
|
|
try:
|
|
import uselect as select
|
|
except ImportError:
|
|
import select
|
|
|
|
from machine import Pin, UART
|
|
from time import sleep_ms, ticks_diff, ticks_ms
|
|
|
|
from micromesh import PortNum, SerialInterface
|
|
|
|
|
|
EVENT_PREFIX = "MMEVT "
|
|
COMMAND_PREFIX = "MMCMD "
|
|
|
|
|
|
def emit(event):
|
|
print(EVENT_PREFIX + json.dumps(event))
|
|
flush = getattr(sys.stdout, "flush", None)
|
|
if flush:
|
|
flush()
|
|
|
|
|
|
def on_packet(packet):
|
|
event = {
|
|
"type": "packet",
|
|
"source": packet.from_,
|
|
"destination": packet.to,
|
|
"portnum": 0,
|
|
"text": "[encrypted]",
|
|
"is_text": False,
|
|
}
|
|
if packet.WhichOneof("payload_variant") == "decoded":
|
|
data = packet.decoded
|
|
event["portnum"] = data.portnum
|
|
if data.portnum == PortNum.TEXT_MESSAGE_APP:
|
|
try:
|
|
event["text"] = data.payload.decode("utf-8")
|
|
except UnicodeError:
|
|
event["text"] = repr(data.payload)
|
|
event["is_text"] = True
|
|
else:
|
|
event["text"] = "[port %d, %d bytes]" % (
|
|
data.portnum, len(data.payload)
|
|
)
|
|
emit(event)
|
|
|
|
|
|
def node_event(info):
|
|
event = {"type": "node", "num": info.num}
|
|
if info.HasField("user"):
|
|
event["name"] = info.user.long_name or info.user.id or "unknown"
|
|
event["short"] = info.user.short_name or "--"
|
|
event["user_id"] = info.user.id
|
|
if info.HasField("snr"):
|
|
event["snr"] = info.snr
|
|
if info.HasField("last_heard"):
|
|
event["last_heard"] = info.last_heard
|
|
if info.HasField("hops_away"):
|
|
event["hops"] = info.hops_away
|
|
if info.HasField("device_metrics") and info.device_metrics.HasField("battery_level"):
|
|
event["battery"] = info.device_metrics.battery_level
|
|
return event
|
|
|
|
|
|
def on_receive(message):
|
|
variant = message.WhichOneof("payload_variant") or "unknown"
|
|
emit({"type": "frame", "variant": variant})
|
|
if variant == "my_info":
|
|
emit({"type": "local", "num": message.my_info.my_node_num})
|
|
elif variant == "node_info":
|
|
emit(node_event(message.node_info))
|
|
elif variant == "config_complete_id":
|
|
emit({
|
|
"type": "config",
|
|
"complete": message.config_complete_id == mesh.config_id,
|
|
"nodes": len(mesh.nodes),
|
|
})
|
|
|
|
|
|
def on_error(error, payload):
|
|
emit({"type": "error", "text": str(error), "bytes": len(payload)})
|
|
|
|
|
|
uart = UART(
|
|
0,
|
|
baudrate=115200,
|
|
tx=Pin(0), # XIAO D6
|
|
rx=Pin(1), # XIAO D7
|
|
timeout=0,
|
|
rxbuf=2048,
|
|
)
|
|
|
|
mesh = SerialInterface(
|
|
uart,
|
|
on_receive=on_receive,
|
|
on_packet=on_packet,
|
|
on_log=lambda line: emit({"type": "status", "text": "radio: " + line}),
|
|
on_error=on_error,
|
|
)
|
|
|
|
command_poll = select.poll()
|
|
command_poll.register(sys.stdin, select.POLLIN)
|
|
|
|
|
|
def handle_commands():
|
|
for _ in command_poll.poll(0):
|
|
first = sys.stdin.read(1)
|
|
if first == "\x03":
|
|
# Never consume the interrupt mpremote uses to regain the REPL.
|
|
raise KeyboardInterrupt
|
|
# Dashboard commands are always newline-terminated. Reading the rest
|
|
# as one line also drains bytes MicroPython buffered after the first
|
|
# character but no longer reports through select.poll().
|
|
line = first + sys.stdin.readline()
|
|
line = line.strip()
|
|
if not line.startswith(COMMAND_PREFIX):
|
|
continue
|
|
try:
|
|
command = json.loads(line[len(COMMAND_PREFIX):])
|
|
kind = command.get("type")
|
|
if kind == "send":
|
|
text = command.get("text", "")
|
|
if text:
|
|
mesh.sendText(text)
|
|
emit({"type": "status", "text": "message queued"})
|
|
elif kind == "refresh":
|
|
mesh.request_config()
|
|
emit({"type": "status", "text": "configuration requested"})
|
|
except Exception as error:
|
|
emit({"type": "error", "text": "command: " + str(error)})
|
|
|
|
|
|
emit({"type": "status", "text": "XIAO bridge starting"})
|
|
mesh.connect()
|
|
emit({"type": "status", "text": "configuration requested"})
|
|
|
|
last_status = ticks_ms()
|
|
while True:
|
|
mesh.poll()
|
|
handle_commands()
|
|
now = ticks_ms()
|
|
if ticks_diff(now, last_status) >= 5000:
|
|
emit({
|
|
"type": "heartbeat",
|
|
"connected": mesh.config_complete,
|
|
"nodes": len(mesh.nodes),
|
|
})
|
|
last_status = now
|
|
sleep_ms(10)
|