"""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)