Store
Training/IoT

Send sensor readings between two ESP32 boards—without a router

Build a direct ESP-NOW temperature-and-humidity link. Understand channels with an interactive packet demo, configure the receiver MAC and recognise missing or stale readings.

KKadosh Build24 October 2026⏱ 5 min readBeginner1 views

On this page

0% read

Two ESP32 boards can exchange a small sensor reading without joining your home Wi-Fi. In this build one board reads a DHT22; the other prints the received temperature, humidity and packet sequence to Serial Monitor.

Allow 60–90 minutes and keep both boards on the same bench to begin. You will learn more by deliberately breaking the channel setting once than by copying a wireless example and hoping it works.

Try the walkie-talkie idea

Try it yourself

Think of two walkie-talkies. They speak directly, but both must use the same channel. A destination MAC address is the label on the message: it identifies which board should receive it.

Sensor boardChannel 1ReceiverChannel 1No router or internet connection is needed
Change a channel, then send a packet.

Matching channels are necessary, not a delivery guarantee. Real links also depend on power, range and interference. A successful send call only queues a packet; the receiver log confirms application reception in this tutorial.

Send on matching channels, change only the receiver channel, then try a blocked link. The channel mismatch is intentional; actual radio delivery has more variables.

A Wi-Fi router normally coordinates a network. ESP-NOW instead exchanges small messages directly using the Wi-Fi radio. The walkie-talkie analogy explains why both boards must listen on the same channel. A MAC address adds something the analogy lacks: a specific destination for a unicast packet.

This tutorial uses a simple unencrypted learning link and a fixed 16-byte packet. It does not implement authentication or application acknowledgements, so do not use it for secrets, access control or critical actuation.

Parts and software

🧰 Build kit

  • 2×ESP32 development boardUse two classic ESP32 DevKit boards for this exact exampleGet it →
  • 1×DHT22 sensorBare four-pin device or labelled breakoutGet it →
  • 1×10k resistorBare-sensor DATA pull-up to 3V3Get it →
  • 2×USB cableOne for each board; use data-capable cablesGet it →
  • 1 set×Breadboard and jumper wiresNo wire connects the two ESP32 boardsGet it →

In Arduino IDE install the Espressif ESP32 board package, Adafruit DHT sensor library and its Adafruit Unified Sensor dependency. These sketches were compile-checked for ESP32 Dev Module with Arduino-ESP32 3.3.8, DHT sensor library 1.4.7 and Unified Sensor 1.1.15. The receive callback uses the Arduino-ESP32 3.x form; old 2.x examples use a different signature.

Wire only the sensor board

Follow a reading from the sensor to GPIO4

DHT22 front12343V310 kΩ3V3GNDNCSensor ESP32 · GPIO4Wait 2.5 s between reads

Power: For this 3.3 V setup, connect VCC and GND to the sensor board. Bare DHT22 pins, front facing you: 1 VCC, 2 DATA, 3 unused, 4 GND.

Check the bare-sensor pin order and pull-up. On a breakout, follow its actual pin labels instead.

Wiring map showing a DHT22 on sensor ESP32 GPIO4 with a 10 kiloohm pull-up to 3.3 volts; receiver board needs only USB.
The receiver does not need a display or sensor. Its Serial Monitor is the readout for this build.

For a bare DHT22, hold the front grille toward you with the legs pointing down: VCC, DATA, unused and GND from left to right. Power VCC from 3V3, join GND and connect DATA to GPIO4. Add 10 kΩ between DATA and 3V3. On a module, follow its printed pin names; the order can differ and the pull-up may already be fitted.

Keep the DHT data pull-up at 3.3 V when it feeds an ESP32 input. Leave the two boards electrically separate apart from their own USB supplies. ESP-NOW itself is the link between them.

Printable sensor-link wiring sheet

DHT22 connections, pull-up and radio setup checklist.

espnow-wiring.pdf · PDF · 73 KB
Download Printable sensor-link wiring sheet↓

Start with the receiver

Upload the receiver sketch first. Open Serial Monitor at 115200 and copy the six-byte address printed after Receiver STA MAC:. Keep this board running. Its channel is explicitly set to 1.

The callback runs in the Wi-Fi task. Instead of printing or waiting inside it, the sketch checks the packet shape and places a copy in a small queue. The normal loop reads the queue and prints the data. That keeps the radio callback short.

espnow_receiver.ino
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
/* Kadosh Build: receiver. Classic ESP32 DevKit; Arduino-ESP32 3.x.
   This learning link is unencrypted. Do not transmit secrets.
   Original example code, MIT license. */
#include <Arduino.h>
#include <WiFi.h>
#include <esp_now.h>
#include <esp_wifi.h>
#include <freertos/FreeRTOS.h>
#include <freertos/queue.h>
#include <math.h>
constexpr uint8_t CHANNEL = 1;
constexpr uint32_t MAGIC = 0x4B424C31;
struct Packet { uint32_t magic; uint32_t sequence; float temperatureC; float humidityPct; };
static_assert(sizeof(Packet) == 16, "Packet layout differs");
QueueHandle_t packets;
uint32_t lastPacketAt = 0, lastSequence = 0;
bool received = false, warned = false;
void onReceive(const esp_now_recv_info_t* info, const uint8_t* bytes, int len) {
  if (!info || len != sizeof(Packet)) return;
  Packet packet;
  memcpy(&packet, bytes, sizeof(packet));
  if (packet.magic != MAGIC || !isfinite(packet.temperatureC) || !isfinite(packet.humidityPct)) return;
  // Wi-Fi task callback: queue the reading and return promptly.
  xQueueSend(packets, &packet, 0);
}
void setup() {
  Serial.begin(115200);
  WiFi.mode(WIFI_STA); WiFi.setSleep(false);
  if (esp_wifi_set_channel(CHANNEL, WIFI_SECOND_CHAN_NONE) != ESP_OK) {
    Serial.println("Channel setup failed"); while (true) delay(1000);
  }
  Serial.print("Receiver STA MAC: "); Serial.println(WiFi.macAddress());
  packets = xQueueCreate(4, sizeof(Packet));
  if (!packets || esp_now_init() != ESP_OK || esp_now_register_recv_cb(onReceive) != ESP_OK) {
    Serial.println("ESP-NOW setup failed"); while (true) delay(1000);
  }
  lastPacketAt = millis();
  Serial.println("Ready on channel 1. Copy the MAC into the sender sketch.");
}
void loop() {
  Packet packet;
  if (xQueueReceive(packets, &packet, 0) == pdTRUE) {
    const uint32_t gap = received && packet.sequence > lastSequence ? packet.sequence - lastSequence - 1 : 0;
    // A lower sequence can be a sender reboot: establish a fresh baseline.
    lastSequence = packet.sequence; received = true; warned = false; lastPacketAt = millis();
    Serial.printf("seq=%lu temp=%.1fC humidity=%.1f%% gap=%lu\n", (unsigned long)packet.sequence, packet.temperatureC, packet.humidityPct, (unsigned long)gap);
  }
  if (!warned && millis() - lastPacketAt > 10000UL) {
    Serial.println(received ? "STALE: no new packet for 10 seconds" : "WAITING: no packet received yet");
    warned = true;
  }
  delay(5);
}

Configure and upload the sender

Animated ESP-NOW reading passing from the sensor board to the receiver queue and Serial Monitor.
Animated walkthrough
This walkthrough separates a queued send request from the receiver actually printing the packet.

Replace the six bytes in receiverMac with the address you just copied. For example, an address written as AA:BB:CC:DD:EE:FF becomes {0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF}. The example address in the sketch is a placeholder; it is not your board.

Leave CHANNEL at 1 on both boards. The sender waits 2.5 seconds between reads and skips a packet if the DHT returns an invalid number. Both programs use the same Packet field order, field types and magic value.

espnow_sender.ino
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
/* Kadosh Build: DHT22 sender. Classic ESP32 DevKit; Arduino-ESP32 3.x.
   Install Adafruit DHT sensor library and Adafruit Unified Sensor.
   DHT22 DATA -> GPIO4; 10k pull-up from DATA to 3V3 for a bare sensor.
   Original example code, MIT license. */
#include <Arduino.h>
#include <WiFi.h>
#include <esp_now.h>
#include <esp_wifi.h>
#include <DHT.h>
#include <math.h>
constexpr uint8_t CHANNEL = 1, DHT_PIN = 4;
constexpr uint32_t MAGIC = 0x4B424C31;
// Replace all six bytes with the receiver's printed STA MAC. This is a placeholder.
uint8_t receiverMac[] = {0x24, 0x6F, 0x28, 0x00, 0x00, 0x01};
struct Packet { uint32_t magic; uint32_t sequence; float temperatureC; float humidityPct; };
static_assert(sizeof(Packet) == 16, "Packet layout differs");
DHT sensor(DHT_PIN, DHT22);
uint32_t sequence = 0, previous = 0;
void setup() {
  Serial.begin(115200); sensor.begin();
  WiFi.mode(WIFI_STA); WiFi.setSleep(false);
  if (esp_wifi_set_channel(CHANNEL, WIFI_SECOND_CHAN_NONE) != ESP_OK || esp_now_init() != ESP_OK) {
    Serial.println("ESP-NOW setup failed"); while (true) delay(1000);
  }
  esp_now_peer_info_t peer = {};
  memcpy(peer.peer_addr, receiverMac, 6);
  peer.channel = CHANNEL; peer.ifidx = WIFI_IF_STA; peer.encrypt = false;
  if (esp_now_add_peer(&peer) != ESP_OK) {
    Serial.println("Peer setup failed"); while (true) delay(1000);
  }
  Serial.println("Sender ready. Receiver must use the same channel.");
}
void loop() {
  if (millis() - previous < 2500UL) return;
  previous = millis();
  const float t = sensor.readTemperature(), h = sensor.readHumidity();
  if (isnan(t) || isnan(h)) { Serial.println("DHT read failed: check wiring and pull-up"); return; }
  Packet packet = {MAGIC, ++sequence, t, h};
  const esp_err_t result = esp_now_send(receiverMac, reinterpret_cast<const uint8_t*>(&packet), sizeof(packet));
  Serial.printf("seq=%lu temp=%.1fC humidity=%.1f%% %s\n", (unsigned long)sequence, t, h, result == ESP_OK ? "queued (check receiver log)" : "queue failed");
}
Both ESP-NOW sketches and checklist

ZIP containing separate Arduino sketch folders, a test worksheet and code licence.

espnow-starter.zip · ZIP · 3 KB
Download Both ESP-NOW sketches and checklist↓

Read the logs like evidence

Queued is different from received

Sensor ESP32DHT22 sampleReceiver ESP32ListeningExample: temperature + humidity + sequence

Sample: The sensor board takes a DHT22 reading and increments its packet sequence.

Step through sampling, queuing, receiver handling and stale-data detection. Time is compressed for teaching.

MessageWhat it provesWhat it does not prove
Sender: queuedThe API accepted a send requestThat the receiver application received it
Receiver: seq=7 ...The receiver decoded packet 7That every earlier packet arrived
Receiver: gap=2Two sequence numbers were skipped since the previous packetThe exact radio cause of the loss
Receiver: STALENo new packet for ten secondsWhether the cause is power, wiring or radio

Compare sender and receiver sequence numbers while the boards are side by side. The receiver prints a gap count between increasing sequence numbers. If the sender restarts and its count goes backwards, the receiver establishes a fresh baseline rather than reporting a huge unsigned-number loss.

A four-entry queue can overflow if the application is too slow. In that situation a sequence gap can reflect application buffering, not just a radio problem. Treat the count as a diagnostic clue rather than a calibrated radio measurement.

Break it on purpose, then recover

ExperimentExpected observationRecovery
Set sender channel to 6 onlyReceiver stops receiving and later prints STALERestore both to channel 1
Unplug the sensor DATA leadSender prints DHT read failedPower off, restore wiring, power on
Disconnect sender powerReceiver prints STALE after ten secondsReconnect sender power
Move boards farther apartReception may become intermittentRecord distance and obstructions; return to bench

Use the worksheet to record packet counts and conditions for a fixed observation window, such as 60 seconds. Change one thing at a time. Do not claim a maximum range from a single successful packet; antenna orientation, obstructions and local interference matter.

Wireless-link test worksheet

Record channel, observation time, sent/received packets and notes.

espnow-checks.csv · CSV · 1 KB
Download Wireless-link test worksheet↓

Troubleshooting

ProblemCheck first
No receiver packetsReceiver STA MAC, identical channel and correct board port
Compile error around the callbackUse the ESP32 3.x board package for these sketches
DHT reads failDATA pin, 3V3 pull-up, sensor type DHT22 and pin order
Works until connecting to Wi-FiThe Wi-Fi connection can change the radio channel; this build intentionally stays disconnected
Fresh data stops but old numbers remain visibleUse the STALE message; add a prominent stale-data indicator to any future display

Next: give the data a purpose

Replace the DHT fields with a water-level reading from the tank tutorial, keeping the packet structure identical at both ends. Add application acknowledgements if you need to know that the receiver processed a particular message. Add authentication and encryption before handling sensitive data or controlling real equipment.

The browser animation is an explanation, not a measurement of RF behaviour. The sketches compile for the versions listed above; carry out the bench tests on your own boards to validate the real link.

References and further reading: Espressif ESP-NOW Arduino guide · ESP-NOW protocol and callback guidance · Adafruit DHT wiring guide

Did you build it? 🎉

Tell us if this tutorial helped and share it with your classmates.

Need parts or a hand with your project?

Get the components from our store, or talk to a Kadosh engineer about your build or research.

Shop componentsExplore project topics

Questions & comments

Stuck on a step? Ask here — our engineers and other builders reply.

Sign in to ask a question or leave a comment.

Sign inCreate a free account
  • Loading comments…

You might also like

All tutorials →
Robotics

Build a line-following robot—and understand every steering decision

Wire an Arduino Uno, two reflectance sensors and a TB6612FNG driver. Calibrate with the motors disabled, explore steering interactively and run your first track with a repeatable test plan.

5 min read·Beginner·1 viewsRead →
ESP32 & ESP8266

Build an ESP32 water-tank monitor: see the level, understand the echo

Turn an ultrasonic echo into a useful water-level reading. Build a tabletop monitor, calibrate it, try the interactive tank and add a low-water alarm without alarm chatter.

5 min read·Beginner·1 viewsRead →