One of the best things about ESP-NOW is that one can add lots of boards to the party with very little extra work!
Multiple Receivers
For example, add second receiver board with identical code to the first and both will get the same message from the sender containing the BME280 sensor data.
Multiple Senders
Yep, you guessed it! Put sender code on an additional board and all receivers will get the individual messages from both senders.
Transcievers Too?!
"But, what about transceivers?", you may ask. No problem, ESP-NOW's got you covered there, too.
You can write code that will allow multiple boards to both send and receive messages amongst each other. This can be in broadcast mode, where no specific MAC addresses are added to the peer list, or in very targeted peer-to-peer modes if you have specific transceiver pairings/groupings in mind.
# SPDX-FileCopyrightText: John Park for Adafruit 2025
# SPDX-License-Identifier: MIT
"""
ESP-NOW transciever demo for ESP32-S2/S3 TFT Feather boards with display
"""
import time
import wifi
import espnow
import board
import digitalio
import displayio
import terminalio
from adafruit_display_text import label
from adafruit_display_shapes.rect import Rect
# Setup the display (using built-in display like your example)
display = board.DISPLAY
group = displayio.Group()
# Create background rectangles like your example
background_rect = Rect(0, 0, display.width, display.height, fill=0x000000)
group.append(background_rect)
# Set up a button on pin D0/BOOT
button = digitalio.DigitalInOut(board.BUTTON)
button.direction = digitalio.Direction.INPUT
button.pull = digitalio.Pull.UP
# CONFIGURATION: Set this for each device
DEVICE_ID = "board_A" # Options: "board_A", "board_B", "board_C", "board_D"
# Channel switching hack
wifi.radio.start_ap(" ", "", channel=6, max_connections=0)
wifi.radio.stop_ap()
def format_mac(mac_bytes):
return ':'.join(f'{b:02x}' for b in mac_bytes)
def get_my_mac():
return format_mac(wifi.radio.mac_address)
# Initialize ESP-NOW
e = espnow.ESPNow()
peer = espnow.Peer(mac=b'\xff\xff\xff\xff\xff\xff', channel=6)
e.peers.append(peer)
my_mac = get_my_mac()
print(f"{DEVICE_ID} board starting - MAC: {my_mac}")
# Colors
TOMATO = 0xFF6347
WHITE = 0xFFFFFF
GREEN = 0x00FF00
BLUE = 0x0080FF
RED = 0xFF0000
# Sender colors mapping
SENDER_COLORS = {
"board_A": 0x32FF32, # Lime green
"board_B": 0x00FFFF, # Cyan
"board_C": 0xC8A2C8, # Lilac
"board_D": 0xFFFFFF # White
}
def get_sender_color(rx_message):
"""Extract sender ID from message and return corresponding color"""
for board_id in SENDER_COLORS:
if rx_message.startswith(board_id):
return SENDER_COLORS[board_id]
return WHITE # Default to white if sender not recognized
# Create text labels using anchor positioning like your example
title_label = label.Label(terminalio.FONT, text=f"ESP-NOW {DEVICE_ID}", color=TOMATO,
scale=2, anchor_point=(0, 0), anchored_position=(5, 5))
group.append(title_label)
status_label = label.Label(terminalio.FONT, text="press D0 to send", color=TOMATO,
scale=2, anchor_point=(0, 0), anchored_position=(5, 35))
group.append(status_label)
sent_label = label.Label(terminalio.FONT, text="TX'd: --", color=TOMATO,
scale=2, anchor_point=(0, 0), anchored_position=(5, 65))
group.append(sent_label)
# Create a second label for the colored counter part
sent_counter_label = label.Label(terminalio.FONT, text="",color=SENDER_COLORS.get(DEVICE_ID, WHITE),
scale=2, anchor_point=(0, 0), anchored_position=(80, 65))
group.append(sent_counter_label)
received_label = label.Label(terminalio.FONT, text="RX'd: --", color=TOMATO,
scale=2, anchor_point=(0, 0), anchored_position=(5, 95))
group.append(received_label)
# Create a second label for the colored message part
received_message_label = label.Label(terminalio.FONT, text="", color=WHITE,
scale=2, anchor_point=(0, 0), anchored_position=(80, 95))
group.append(received_message_label)
# Show the display
display.root_group = group
# Button debouncing and status tracking
last_button_time = 0
button_debounce = 0.2 # 200ms debounce
message_count = 0
status_reset_time = 0
status_needs_reset = False
while True:
current_time = time.monotonic()
# Check for button press (button is active low due to pull-up)
if not button.value and (current_time - last_button_time > button_debounce):
message_count += 1
message = f"{DEVICE_ID} {message_count}"
try:
e.send(message, peer)
print(f"Sent: {message}")
# Update display
status_label.text = "...>"
status_label.color = TOMATO
sent_label.text = "TX'd: " # Keep this tomato colored
sent_counter_label.text = str(message_count) # Color with sender color
sent_counter_label.color = SENDER_COLORS.get(DEVICE_ID, WHITE)
status_reset_time = current_time + 0.5 # Reset after 0.5 seconds
status_needs_reset = True
except Exception as ex: # pylint: disable=broad-except
print(f"Send failed: {ex}")
status_label.text = "xxx"
status_label.color = RED
status_reset_time = current_time + 2.0 # Show error for 2 seconds
status_needs_reset = True
last_button_time = current_time
# Reset status only when needed and after appropriate delay
if status_needs_reset and current_time >= status_reset_time:
status_label.text = "press D0 to send"
status_label.color = TOMATO
status_needs_reset = False
# Check for incoming packets
if e:
packet = e.read()
if packet:
sender_mac = format_mac(packet.mac)
if sender_mac != my_mac:
message = packet.msg.decode('utf-8')
print(f"received: {message}")
# Update display with sender color
status_label.text = "<..."
status_label.color = TOMATO
sender_color = get_sender_color(message)
received_label.text = "RX'd: " # Keep this tomato colored
received_message_label.text = message[-12:] # Color just the message
received_message_label.color = sender_color
status_reset_time = current_time + 0.5 # Reset after 0.5 seconds
status_needs_reset = True
time.sleep(0.05) # Light polling
import time import wifi import espnow import board import digitalio import displayio import terminalio from adafruit_display_text import label from adafruit_display_shapes.rect import Rect
Board Setup and Identification
Each board can be set to its own unique ID:
DEVICE_ID = "board_A" # Change this for each board: board_A, board_B, board_C, board_D
When you power up a board, it announces itself:
board_A board starting - MAC: xx:xx:xx:xx:xx:xx
display = board.DISPLAY group = displayio.Group() background_rect = Rect(0, 0, display.width, display.height, fill=0x000000) group.append(background_rect)
button = digitalio.DigitalInOut(board.BUTTON) button.direction = digitalio.Direction.INPUT button.pull = digitalio.Pull.UP
ESP-NOW Setup
This is the "channel switching hack" - we briefly start then stop an access point to force the WiFi radio onto channel 6. Then we create an ESP-NOW object and add a broadcast peer (the \xff\xff\xff\xff\xff\xff address means "send to everyone").
wifi.radio.start_ap(" ", "", channel=6, max_connections=0)
wifi.radio.stop_ap()
e = espnow.ESPNow()
peer = espnow.Peer(mac=b'\xff\xff\xff\xff\xff\xff', channel=6)
e.peers.append(peer)
Color Coding
Each board ID maps to a hex color value. When we receive a message, we'll look up the sender and use their color.
SENDER_COLORS = {
"board_A": 0x32FF32, # Lime green
"board_B": 0x00FFFF, # Cyan
"board_C": 0xC8A2C8, # Lilac
"board_D": 0xFFFFFF # White
}
Color Detect Function
This function parses incoming messages like "board_B 5" and returns cyan (board_B's color). If the sender isn't recognized, it defaults to white.
def get_sender_color(rx_message):
for board_id in SENDER_COLORS:
if rx_message.startswith(board_id):
return SENDER_COLORS[board_id]
return WHITE
Display Labels
Instead of one label saying "TX'd: 5", we use two labels:
-
sent_label: Shows "TX'd: " in red -
sent_counter_label: Shows just the number in the sender's color
The anchored_position=(80, 65) for the counter positions it right after the "TX'd: " text.
sent_label = label.Label(terminalio.FONT, text="TX'd: --", color=TOMATO, ...) sent_counter_label = label.Label(terminalio.FONT, text="", color=SENDER_COLORS.get(DEVICE_ID, WHITE), ...)
Main Loop
We check if the button is pressed (not button.value because of pull-up) and enough time has passed since the last press (debouncing prevents multiple triggers from one press).
if not button.value and (current_time - last_button_time > button_debounce):
message_count += 1
message = f"{DEVICE_ID} {message_count}"
Message Send
Next we try to send the message. Success shows "...>" for 0.5 seconds. Errors show "xxx" for 2 seconds.
try:
e.send(message, peer)
# Update display...
status_reset_time = current_time + 0.5
status_needs_reset = True
except Exception as ex:
# Show error...
status_reset_time = current_time + 2.0
status_needs_reset = True
Status Update
We update the display status if a message has come in or gone out. The ESP-NOW object's read() method returns a packet if one is available, or None if not. We:
- Check it's not from ourselves (avoid echo)
- Decode the message from bytes to string
- Determine the sender's color
- Update the split labels (prefix stays red, message gets sender color)
if e:
packet = e.read()
if packet:
sender_mac = format_mac(packet.mac)
if sender_mac != my_mac: # Don't process our own messages
message = packet.msg.decode('utf-8')
sender_color = get_sender_color(message)
received_label.text = "RX'd: " # Stays tomato
received_message_label.text = message[-12:] # In sender color
received_message_label.color = sender_color
Page last edited August 05, 2025
Text editor powered by tinymce.