"""ESP-NOW bridge: Pi USB-serial downlink, ESP-NOW to drivers (sync loop).""" import gc, json, struct, time import espnow, machine, network from machine import Pin, UART from settings import Settings BROADCAST = b"\xff\xff\xff\xff\xff\xff" WIRE = 0x4C MAX_SERIAL = 4096 MAX_ESPNOW = 250 ESPNOW_EXIST = -12395 ESPNOW_FULL = -12392 def add_peer_if_needed(esp, dest, ch): try: esp.add_peer(dest, channel=ch) except TypeError: try: esp.add_peer(dest) except OSError as e: if e.args[0] != ESPNOW_EXIST: raise except OSError as e: if e.args[0] != ESPNOW_EXIST: raise def del_peer_if_present(esp, dest): try: esp.del_peer(dest) except Exception: pass def send_unicast_temp_peer(esp, dest, ch, pkt): try: add_peer_if_needed(esp, dest, ch) except OSError as e: if e.args and e.args[0] == ESPNOW_FULL: del_peer_if_present(esp, dest) add_peer_if_needed(esp, dest, ch) else: raise try: esp.send(dest, pkt, True) finally: del_peer_if_present(esp, dest) def init_radio(ch, name, password): network.WLAN(network.STA_IF).active(False) network.WLAN(network.AP_IF).active(False) time.sleep_ms(100) ap = network.WLAN(network.AP_IF) ap.active(True) time.sleep_ms(50) if password: try: ap.config(essid=name or "bridge", password=password, channel=ch, hidden=True) except TypeError: ap.config(essid=name or "bridge", channel=ch) else: try: ap.config(essid=name or "bridge", channel=ch, hidden=True) except TypeError: ap.config(essid=name or "bridge", channel=ch) sta = network.WLAN(network.STA_IF) sta.active(True) sta.config(pm=network.WLAN.PM_NONE) try: sta.config(channel=ch) except Exception: pass def mac_bytes(addr): h = str(addr).replace(":", "").replace("-", "").strip().lower() return bytes.fromhex(h) def read_serial(uart, buf): if uart.any(): buf.extend(uart.read(min(uart.any(), 256))) out = [] while len(buf) >= 2: n = (buf[0] << 8) | buf[1] if n > MAX_SERIAL: buf[:] = buf[1:] continue need = 2 + n if len(buf) < need: break out.append(bytes(buf[2:need])) buf[:] = buf[need:] return out def downlink(esp, ch, raw): if not raw: return if raw[0] == WIRE: if len(raw) < 2: return esp.send(BROADCAST, raw, True) return if len(raw) < 8 or raw[0] != ord("{"): return try: data = json.loads(raw) except ValueError: return devs = data.get("dv") or data.get("devices") if data.get("v") != "1" or not isinstance(devs, dict): return for mac_s, body in devs.items(): if not isinstance(body, dict): continue try: msg = {"v": "1"} msg.update(body) pkt = json.dumps(msg, separators=(",", ":")).encode() if len(pkt) > MAX_ESPNOW: continue dest = mac_bytes(mac_s) except (ValueError, TypeError): continue if dest == BROADCAST: esp.send(BROADCAST, pkt, True) else: send_unicast_temp_peer(esp, dest, ch, pkt) time.sleep_ms(5) gc.collect() s = Settings() ch = max(1, min(11, int(s.get("wifi_channel", 5)))) init_radio(ch, s.get("name"), s.get("ap_password") or "") baud = int(s.get("serial_baudrate", 921600)) uart = UART( int(s.get("serial_uart_id", 1)), baud, tx=Pin(int(s.get("serial_tx_pin", 2))), rx=Pin(int(s.get("serial_rx_pin", 3))), ) esp = espnow.ESPNow() esp.active(True) add_peer_if_needed(esp, BROADCAST, ch) print("bridge ch", ch, "baud", baud, "heap", gc.mem_free()) wdt = machine.WDT(timeout=10000) rx_buf = bytearray() while True: wdt.feed() for frame in read_serial(uart, rx_buf): try: downlink(esp, ch, frame) except OSError as e: print("dl", e) host, msg = esp.recv(0) if host: up = bytes([0]) + host + msg uart.write(struct.pack(">H", len(up)) + up) else: time.sleep_ms(1)