From 39a84696c3fe06bca18ab4ff3a09b752454673fb Mon Sep 17 00:00:00 2001 From: Jimmy Date: Mon, 25 May 2026 21:55:39 +1200 Subject: [PATCH] feat(espnow): ping request/response with jittered delay Co-authored-by: Cursor --- src/espnow_transport.py | 60 +++++++++++++++++++++++++++++++++++++---- src/espnow_wire.py | 25 +++++++++++++++++ 2 files changed, 80 insertions(+), 5 deletions(-) diff --git a/src/espnow_transport.py b/src/espnow_transport.py index 8142785..e4730ec 100644 --- a/src/espnow_transport.py +++ b/src/espnow_transport.py @@ -1,6 +1,7 @@ """ESP-NOW receive loop and boot announce.""" import asyncio +import urandom import espnow import network @@ -12,13 +13,21 @@ from espnow_wire import ( MSG_CMD, MSG_GROUP_CMD, MSG_GROUPS, + MSG_PING_REQ, cmd_envelope, pack_announce, + pack_ping_rsp, parse_group_cmd, parse_groups, + parse_ping_req, wire_msg_type, ) + from controller_messages import process_data +from settings import WIFI_CHANNEL_DEFAULT + +_PING_DELAY_MS_MIN = 50 +_PING_DELAY_MS_MAX = 500 _esp = None _groups_received = False @@ -26,14 +35,14 @@ _groups_received = False def init_espnow(settings): global _esp - ch = 6 try: - ch = int(settings.get("wifi_channel", 1)) + ch = int(settings.get("wifi_channel", WIFI_CHANNEL_DEFAULT)) except (TypeError, ValueError): - pass + ch = WIFI_CHANNEL_DEFAULT ch = max(1, min(11, ch)) sta = network.WLAN(network.STA_IF) sta.active(True) + sta.config(pm=network.WLAN.PM_NONE) sta.config(channel=ch) _esp = espnow.ESPNow() _esp.active(True) @@ -44,6 +53,42 @@ def init_espnow(settings): return _esp +def _send_ping_rsp(host, settings, ping_id, delay_ms): + import utime + + utime.sleep_ms(delay_ms) + if _esp is None or not host or len(host) != 6: + return + pkt = pack_ping_rsp(ping_id, settings.get("name", "led")) + try: + try: + _esp.add_peer(host) + except Exception: + pass + _esp.send(host, pkt) + print("espnow ping rsp", ping_id, delay_ms, "ms") + except Exception as e: + print("espnow ping rsp failed:", e) + + +async def _send_ping_rsp_delayed(host, settings, ping_id): + span = _PING_DELAY_MS_MAX - _PING_DELAY_MS_MIN + delay_ms = _PING_DELAY_MS_MIN + (urandom.getrandbits(10) % (span + 1)) + await asyncio.sleep(delay_ms / 1000) + _send_ping_rsp(host, settings, ping_id, delay_ms) + + +def _schedule_ping_rsp(host, settings, ping_id): + span = _PING_DELAY_MS_MAX - _PING_DELAY_MS_MIN + delay_ms = _PING_DELAY_MS_MIN + (urandom.getrandbits(10) % (span + 1)) + try: + import _thread + + _thread.start_new_thread(_send_ping_rsp, (host, settings, ping_id, delay_ms)) + except ImportError: + asyncio.create_task(_send_ping_rsp_delayed(host, settings, ping_id)) + + def send_boot_announce(settings): if _esp is None: return @@ -61,7 +106,7 @@ def send_boot_announce(settings): print("espnow announce failed:", e) -def _handle_packet(pkt, settings, presets): +def _handle_packet(host, pkt, settings, presets): global _groups_received mt = wire_msg_type(pkt) if mt == MSG_GROUPS: @@ -91,6 +136,11 @@ def _handle_packet(pkt, settings, presets): if env: process_data(env, settings, presets, save=save) return + if mt == MSG_PING_REQ: + ping_id = parse_ping_req(pkt) + if ping_id is not None and host and len(host) == 6: + _schedule_ping_rsp(host, settings, ping_id) + return if mt == MSG_ANNOUNCE: return @@ -112,7 +162,7 @@ async def espnow_receive_loop(settings, presets, wdt=None): wdt.feed() continue try: - _handle_packet(msg, settings, presets) + _handle_packet(host, msg, settings, presets) except Exception as e: print("espnow rx error:", e) if wdt: diff --git a/src/espnow_wire.py b/src/espnow_wire.py index 0d4cd70..09ef807 100644 --- a/src/espnow_wire.py +++ b/src/espnow_wire.py @@ -9,6 +9,8 @@ MSG_ANNOUNCE = 0x01 MSG_GROUPS = 0x02 MSG_CMD = 0x03 MSG_GROUP_CMD = 0x04 +MSG_PING_REQ = 0x05 +MSG_PING_RSP = 0x06 BROADCAST_MAC = b"\xff\xff\xff\xff\xff\xff" @@ -104,6 +106,29 @@ def cmd_envelope(payload): return env[:need], save +def pack_ping_req(ping_id): + body = struct.pack("= 2 and payload[0] == WIRE_MAGIC: + if payload[1] != MSG_PING_REQ: + return None + body = payload[2:] + else: + body = payload + if len(body) < 4: + return None + return struct.unpack("= 2 and payload[0] == WIRE_MAGIC: return payload[1]