🔗Goal
Build a wireless sensor network with multiple ESP32 boards that communicate using the ESP-NOW protocol. Two sensor nodes collect environmental data (temperature/humidity and soil moisture), then transmit it wirelessly to a central gateway node. The gateway forwards everything to an MQTT broker over WiFi.
graph LR
subgraph Node 1
A1[BME280] -->|I2C| B1[ESP32]
end
subgraph Node 2
A2[Soil Moisture] -->|Analog| B2[ESP32]
end
subgraph Gateway
B3[ESP32]
end
B1 -->|ESP-NOW| B3
B2 -->|ESP-NOW| B3
B3 -->|WiFi| C[MQTT Broker]
C --> D[Dashboard / Home Assistant]The key advantage of this design is that the sensor nodes do not need WiFi. They use ESP-NOW, a lightweight peer-to-peer protocol that is faster to transmit, uses far less power, and does not require a router. Only the gateway connects to WiFi, which means sensor nodes can run on batteries with deep sleep for weeks or even months.
🔗Why ESP-NOW?
ESP-NOW is a connectionless protocol developed by Espressif (the company behind the ESP32). Here is how it compares to WiFi for sensor networks:
| Feature | ESP-NOW | WiFi + MQTT |
|---|---|---|
| Connection time | < 1 ms | 2-8 seconds |
| Power per transmission | ~0.3 mA | ~150-350 mA |
| Range (open air) | ~200 m | ~50-100 m |
| Requires router | No | Yes |
| Max payload | 250 bytes | Unlimited |
| Encryption | Optional (CCMP) | WPA2 |
| Max peers | 20 (ESP32) | N/A |
ESP-NOW sends small packets (up to 250 bytes) directly between ESP32 boards using their MAC addresses. There is no connection handshake, no DHCP, no TCP overhead. The transmitter wakes up, sends the packet, gets an acknowledgment, and goes back to sleep. This makes it ideal for battery-powered sensor nodes.
🔗Prerequisites
You will need the following components:
🔗Gateway Node
| Component | Qty | Notes |
|---|---|---|
| ESP32 dev board | 1 | Must stay powered (USB or wall adapter) |
| USB cable | 1 | For power and programming |
🔗Sensor Node 1 (Temperature/Humidity)
| Component | Qty | Notes |
|---|---|---|
| ESP32 dev board | 1 | Any ESP32-WROOM-32 DevKit |
| BME280 sensor module | 1 | I2C breakout board (not BMP280) |
| Jumper wires | 4 | Male-to-female |
| Battery holder | 1 | Optional: 2x AA or 18650 shield |
🔗Sensor Node 2 (Soil Moisture)
| Component | Qty | Notes |
|---|---|---|
| ESP32 dev board | 1 | Any ESP32-WROOM-32 DevKit |
| Capacitive soil moisture sensor v1.2 | 1 | Prefer capacitive over resistive |
| Jumper wires | 3 | Male-to-female |
| Battery holder | 1 | Optional: 2x AA or 18650 shield |
🔗Software
You will also need:
- An MQTT broker (e.g., Mosquitto running on a Raspberry Pi, or a cloud broker like HiveMQ)
- Arduino IDE with these libraries installed via Library Manager:
- Adafruit BME280 Library (also installs Adafruit Unified Sensor)
- PubSubClient by Nick O'Leary (for MQTT on the gateway)
The ESP-NOW library (
esp_now.h) is built into the ESP32 Arduino core. No extra installation needed.
🔗System Architecture
The network uses a star topology: all sensor nodes transmit to a single gateway.
graph TD
subgraph Sensor Nodes
N1[Node 1: BME280<br/>Deep sleep 5 min]
N2[Node 2: Soil Moisture<br/>Deep sleep 15 min]
N3[Node N: Any sensor<br/>Add as needed]
end
subgraph Gateway
GW[Gateway ESP32<br/>Always on]
end
subgraph Network
MQTT[MQTT Broker]
HA[Dashboard]
end
N1 -->|ESP-NOW| GW
N2 -->|ESP-NOW| GW
N3 -.->|ESP-NOW| GW
GW -->|WiFi + MQTT| MQTT
MQTT --> HAEach sensor node follows this cycle:
graph LR
A[Wake up] --> B[Read sensor]
B --> C[Send ESP-NOW packet]
C --> D{ACK received?}
D -->|Yes| E[Deep sleep]
D -->|No| F[Retry up to 3x]
F --> D🔗Data Packet Structure
Every node sends a structured packet so the gateway knows what it is receiving:
| Field | Type | Size | Purpose |
|---|---|---|---|
| nodeId | uint8_t | 1 byte | Unique ID (1-255) |
| sensorType | uint8_t | 1 byte | 1=BME280, 2=Soil, etc. |
| temperature | float | 4 bytes | Degrees Celsius |
| humidity | float | 4 bytes | Relative humidity % |
| value1 | float | 4 bytes | Extra reading (e.g., pressure, soil moisture) |
| battery | float | 4 bytes | Battery voltage |
| Total | 18 bytes | Well under 250-byte ESP-NOW limit |
🔗Tutorial
🔗Step 1: Get the Gateway MAC Address
Before programming the sensor nodes, you need to know the gateway ESP32's MAC address. Upload this short sketch to the board you will use as the gateway:
#include <WiFi.h>
void setup() {
Serial.begin(115200);
delay(1000);
WiFi.mode(WIFI_STA);
Serial.print("Gateway MAC Address: ");
Serial.println(WiFi.macAddress());
}
void loop() {}Open the Serial Monitor at 115200 baud. You will see something like:
Gateway MAC Address: 24:6F:28:A1:B2:C3Write this down. Every sensor node needs this address to know where to send data.
🔗Step 2: Wire Sensor Node 1 (BME280)
Connect the BME280 to the first sensor ESP32:
| BME280 Pin | ESP32 Pin |
|---|---|
| VCC | 3.3V |
| GND | GND |
| SDA | GPIO 21 |
| SCL | GPIO 22 |
The BME280 uses I2C with a default address of 0x76. If your module uses 0x77, change it in the code.
🔗Step 3: Wire Sensor Node 2 (Soil Moisture)
Connect the capacitive soil moisture sensor to the second sensor ESP32:
| Sensor Pin | ESP32 Pin |
|---|---|
| VCC | 3.3V |
| GND | GND |
| AOUT | GPIO 34 |
GPIO 34 is input-only and supports ADC, making it a reliable choice for analog sensors. Avoid ADC2 pins (GPIO 0, 2, 4, 12-15, 25-27) as they conflict with WiFi (even though sensor nodes do not use WiFi, it is good practice).
🔗Step 4: Sensor Node 1 Code (BME280)
Upload this sketch to the first sensor node. Replace the gateway MAC address with the one you noted in Step 1.
#include <esp_now.h>
#include <WiFi.h>
#include <Wire.h>
#include <Adafruit_Sensor.h>
#include <Adafruit_BME280.h>
// ---- CONFIGURATION ----
#define NODE_ID 1
#define SENSOR_TYPE 1 // 1 = BME280
#define SLEEP_SECONDS 300 // 5 minutes between readings
#define MAX_RETRIES 3
// Replace with YOUR gateway's MAC address
uint8_t gatewayMAC[] = {0x24, 0x6F, 0x28, 0xA1, 0xB2, 0xC3};
// ---- DATA STRUCTURE (must match gateway) ----
typedef struct __attribute__((packed)) {
uint8_t nodeId;
uint8_t sensorType;
float temperature;
float humidity;
float value1; // Pressure in hPa for BME280
float battery;
} SensorPacket;
SensorPacket packet;
Adafruit_BME280 bme;
bool sendSuccess = false;
// Callback: called when ESP-NOW send completes
void onDataSent(const uint8_t *mac, esp_now_send_status_t status) {
sendSuccess = (status == ESP_NOW_SEND_SUCCESS);
Serial.print("Send status: ");
Serial.println(sendSuccess ? "OK" : "FAIL");
}
// Read battery voltage via ADC (voltage divider on GPIO 35)
float readBatteryVoltage() {
// If you connect a voltage divider (100K + 100K) to GPIO 35:
// V_batt = ADC_reading / 4095.0 * 3.3 * 2.0
// Without a divider, return 0 to indicate USB power
return 0.0;
}
void setup() {
Serial.begin(115200);
delay(100);
// Initialize BME280
Wire.begin(21, 22);
if (!bme.begin(0x76)) {
Serial.println("BME280 not found! Check wiring.");
// Sleep and try again later
esp_deep_sleep(SLEEP_SECONDS * 1000000ULL);
}
// Force a reading (first reading after wake can be stale)
bme.takeForcedMeasurement();
delay(100);
// Build the packet
packet.nodeId = NODE_ID;
packet.sensorType = SENSOR_TYPE;
packet.temperature = bme.readTemperature();
packet.humidity = bme.readHumidity();
packet.value1 = bme.readPressure() / 100.0F; // Pa to hPa
packet.battery = readBatteryVoltage();
Serial.printf("Node %d: T=%.1fC H=%.1f%% P=%.1fhPa\n",
packet.nodeId, packet.temperature,
packet.humidity, packet.value1);
// Initialize ESP-NOW
WiFi.mode(WIFI_STA);
WiFi.disconnect(); // Not connecting to any AP
if (esp_now_init() != ESP_OK) {
Serial.println("ESP-NOW init failed");
esp_deep_sleep(SLEEP_SECONDS * 1000000ULL);
}
esp_now_register_send_cb(onDataSent);
// Register the gateway as a peer
esp_now_peer_info_t peerInfo = {};
memcpy(peerInfo.peer_addr, gatewayMAC, 6);
peerInfo.channel = 0; // Use current channel
peerInfo.encrypt = false;
if (esp_now_add_peer(&peerInfo) != ESP_OK) {
Serial.println("Failed to add gateway peer");
esp_deep_sleep(SLEEP_SECONDS * 1000000ULL);
}
// Send with retries
for (int attempt = 0; attempt < MAX_RETRIES; attempt++) {
sendSuccess = false;
esp_now_send(gatewayMAC, (uint8_t *)&packet, sizeof(packet));
delay(100); // Wait for callback
if (sendSuccess) break;
Serial.printf("Retry %d/%d\n", attempt + 1, MAX_RETRIES);
}
// Go to deep sleep
Serial.printf("Sleeping for %d seconds...\n", SLEEP_SECONDS);
esp_deep_sleep(SLEEP_SECONDS * 1000000ULL);
}
void loop() {
// Never reached — setup sends data then sleeps
}🔗Step 5: Sensor Node 2 Code (Soil Moisture)
Upload this sketch to the second sensor node. Again, replace the gateway MAC address.
#include <esp_now.h>
#include <WiFi.h>
// ---- CONFIGURATION ----
#define NODE_ID 2
#define SENSOR_TYPE 2 // 2 = Soil moisture
#define SENSOR_PIN 34
#define SLEEP_SECONDS 900 // 15 minutes between readings
#define MAX_RETRIES 3
// Replace with YOUR gateway's MAC address
uint8_t gatewayMAC[] = {0x24, 0x6F, 0x28, 0xA1, 0xB2, 0xC3};
// ---- DATA STRUCTURE (must match gateway) ----
typedef struct __attribute__((packed)) {
uint8_t nodeId;
uint8_t sensorType;
float temperature;
float humidity;
float value1; // Soil moisture (0-100%)
float battery;
} SensorPacket;
SensorPacket packet;
bool sendSuccess = false;
void onDataSent(const uint8_t *mac, esp_now_send_status_t status) {
sendSuccess = (status == ESP_NOW_SEND_SUCCESS);
Serial.print("Send status: ");
Serial.println(sendSuccess ? "OK" : "FAIL");
}
float readSoilMoisture() {
// Take multiple readings and average them for stability
long total = 0;
const int samples = 10;
for (int i = 0; i < samples; i++) {
total += analogRead(SENSOR_PIN);
delay(10);
}
int raw = total / samples;
// Map to percentage (calibrate these values for your sensor)
// Typical capacitive sensor: ~3400 in air, ~1400 in water
int dryValue = 3400;
int wetValue = 1400;
float percent = map(constrain(raw, wetValue, dryValue),
dryValue, wetValue, 0, 100);
return percent;
}
float readBatteryVoltage() {
return 0.0; // Return 0 if powered via USB
}
void setup() {
Serial.begin(115200);
delay(100);
// Read soil moisture
float moisture = readSoilMoisture();
// Build the packet
packet.nodeId = NODE_ID;
packet.sensorType = SENSOR_TYPE;
packet.temperature = 0.0; // Not measured on this node
packet.humidity = 0.0;
packet.value1 = moisture;
packet.battery = readBatteryVoltage();
Serial.printf("Node %d: Soil moisture = %.1f%%\n",
packet.nodeId, packet.value1);
// Initialize ESP-NOW
WiFi.mode(WIFI_STA);
WiFi.disconnect();
if (esp_now_init() != ESP_OK) {
Serial.println("ESP-NOW init failed");
esp_deep_sleep(SLEEP_SECONDS * 1000000ULL);
}
esp_now_register_send_cb(onDataSent);
// Register gateway peer
esp_now_peer_info_t peerInfo = {};
memcpy(peerInfo.peer_addr, gatewayMAC, 6);
peerInfo.channel = 0;
peerInfo.encrypt = false;
if (esp_now_add_peer(&peerInfo) != ESP_OK) {
Serial.println("Failed to add gateway peer");
esp_deep_sleep(SLEEP_SECONDS * 1000000ULL);
}
// Send with retries
for (int attempt = 0; attempt < MAX_RETRIES; attempt++) {
sendSuccess = false;
esp_now_send(gatewayMAC, (uint8_t *)&packet, sizeof(packet));
delay(100);
if (sendSuccess) break;
Serial.printf("Retry %d/%d\n", attempt + 1, MAX_RETRIES);
}
Serial.printf("Sleeping for %d seconds...\n", SLEEP_SECONDS);
esp_deep_sleep(SLEEP_SECONDS * 1000000ULL);
}
void loop() {
// Never reached
}🔗Step 6: Gateway Code
The gateway does two things: it listens for ESP-NOW packets from all sensor nodes, and it forwards the data to an MQTT broker over WiFi.
Upload this sketch to the gateway ESP32. Update the WiFi credentials and MQTT broker address.
#include <esp_now.h>
#include <WiFi.h>
#include <PubSubClient.h>
// ---- WiFi CONFIGURATION ----
const char* ssid = "YOUR_WIFI_SSID";
const char* password = "YOUR_WIFI_PASSWORD";
// ---- MQTT CONFIGURATION ----
const char* mqttServer = "192.168.1.100"; // Your MQTT broker IP
const int mqttPort = 1883;
const char* mqttUser = ""; // Leave empty if no auth
const char* mqttPassword = "";
// ---- DATA STRUCTURE (must match sensor nodes) ----
typedef struct __attribute__((packed)) {
uint8_t nodeId;
uint8_t sensorType;
float temperature;
float humidity;
float value1;
float battery;
} SensorPacket;
WiFiClient wifiClient;
PubSubClient mqtt(wifiClient);
// Buffer for incoming packets (simple queue)
#define QUEUE_SIZE 10
SensorPacket packetQueue[QUEUE_SIZE];
int queueHead = 0;
int queueTail = 0;
volatile bool newData = false;
// Callback: called when an ESP-NOW packet arrives
void onDataRecv(const uint8_t *mac, const uint8_t *data, int len) {
if (len != sizeof(SensorPacket)) {
Serial.printf("Bad packet size: %d (expected %d)\n",
len, sizeof(SensorPacket));
return;
}
// Add to queue
int nextHead = (queueHead + 1) % QUEUE_SIZE;
if (nextHead != queueTail) { // Queue not full
memcpy(&packetQueue[queueHead], data, sizeof(SensorPacket));
queueHead = nextHead;
newData = true;
} else {
Serial.println("WARNING: Packet queue full, dropping packet");
}
// Log the sender
Serial.printf("Received from %02X:%02X:%02X:%02X:%02X:%02X - Node %d\n",
mac[0], mac[1], mac[2], mac[3], mac[4], mac[5],
((SensorPacket*)data)->nodeId);
}
void connectWiFi() {
Serial.print("Connecting to WiFi");
WiFi.mode(WIFI_STA);
WiFi.begin(ssid, password);
int attempts = 0;
while (WiFi.status() != WL_CONNECTED && attempts < 30) {
delay(500);
Serial.print(".");
attempts++;
}
if (WiFi.status() == WL_CONNECTED) {
Serial.printf("\nConnected! IP: %s\n",
WiFi.localIP().toString().c_str());
Serial.printf("MAC: %s\n", WiFi.macAddress().c_str());
Serial.printf("Channel: %d\n", WiFi.channel());
} else {
Serial.println("\nWiFi connection failed! Restarting...");
ESP.restart();
}
}
void connectMQTT() {
while (!mqtt.connected()) {
Serial.print("Connecting to MQTT...");
String clientId = "ESP32-Gateway-" + String(random(0xffff), HEX);
bool connected;
if (strlen(mqttUser) > 0) {
connected = mqtt.connect(clientId.c_str(), mqttUser, mqttPassword);
} else {
connected = mqtt.connect(clientId.c_str());
}
if (connected) {
Serial.println("connected!");
} else {
Serial.printf("failed (rc=%d). Retrying in 5s...\n",
mqtt.state());
delay(5000);
}
}
}
void publishPacket(SensorPacket &pkt) {
char topic[64];
char payload[256];
// Publish to topic: sensors/node<ID>/<type>
const char* sensorName;
switch (pkt.sensorType) {
case 1: sensorName = "bme280"; break;
case 2: sensorName = "soil_moisture"; break;
default: sensorName = "unknown"; break;
}
// Publish individual values for easy dashboard integration
if (pkt.sensorType == 1) { // BME280
snprintf(topic, sizeof(topic), "sensors/node%d/temperature", pkt.nodeId);
snprintf(payload, sizeof(payload), "%.1f", pkt.temperature);
mqtt.publish(topic, payload, true); // retained
snprintf(topic, sizeof(topic), "sensors/node%d/humidity", pkt.nodeId);
snprintf(payload, sizeof(payload), "%.1f", pkt.humidity);
mqtt.publish(topic, payload, true);
snprintf(topic, sizeof(topic), "sensors/node%d/pressure", pkt.nodeId);
snprintf(payload, sizeof(payload), "%.1f", pkt.value1);
mqtt.publish(topic, payload, true);
}
else if (pkt.sensorType == 2) { // Soil moisture
snprintf(topic, sizeof(topic), "sensors/node%d/soil_moisture", pkt.nodeId);
snprintf(payload, sizeof(payload), "%.1f", pkt.value1);
mqtt.publish(topic, payload, true);
}
// Always publish battery voltage
snprintf(topic, sizeof(topic), "sensors/node%d/battery", pkt.nodeId);
snprintf(payload, sizeof(payload), "%.2f", pkt.battery);
mqtt.publish(topic, payload, true);
// Publish a JSON summary for convenience
snprintf(topic, sizeof(topic), "sensors/node%d/json", pkt.nodeId);
snprintf(payload, sizeof(payload),
"{\"node\":%d,\"type\":\"%s\",\"temp\":%.1f,"
"\"hum\":%.1f,\"val\":%.1f,\"batt\":%.2f}",
pkt.nodeId, sensorName, pkt.temperature,
pkt.humidity, pkt.value1, pkt.battery);
mqtt.publish(topic, payload, true);
Serial.printf("Published Node %d data to MQTT\n", pkt.nodeId);
}
void setup() {
Serial.begin(115200);
delay(1000);
Serial.println("\n=== ESP-NOW Gateway ===");
// Connect WiFi first (need to know the channel)
connectWiFi();
// Initialize ESP-NOW (WiFi must be in STA mode, which it already is)
if (esp_now_init() != ESP_OK) {
Serial.println("ESP-NOW init failed! Restarting...");
ESP.restart();
}
esp_now_register_recv_cb(onDataRecv);
Serial.println("ESP-NOW ready. Waiting for sensor data...");
// Connect MQTT
mqtt.setServer(mqttServer, mqttPort);
connectMQTT();
}
void loop() {
// Keep MQTT alive
if (!mqtt.connected()) {
connectMQTT();
}
mqtt.loop();
// Process any queued packets
while (queueTail != queueHead) {
publishPacket(packetQueue[queueTail]);
queueTail = (queueTail + 1) % QUEUE_SIZE;
}
delay(10);
}🔗Step 7: Important -- WiFi Channel Alignment
ESP-NOW and WiFi share the same radio. When the gateway connects to your WiFi router, it locks to whatever channel the router uses. The sensor nodes must transmit on the same channel, or the gateway will not receive their packets.
By default, WiFi.mode(WIFI_STA) with no connection uses channel 1. If your router uses a different channel (say, channel 6), the sensor nodes will be sending on channel 1 while the gateway listens on channel 6.
Solution: Set the sensor nodes to use the same channel. Add this line in the sensor node code, right after WiFi.mode(WIFI_STA):
esp_wifi_set_channel(6, WIFI_SECOND_CHAN_NONE); // Match your routerYou will need to add #include <esp_wifi.h> at the top of the sensor node sketches.
To find your router's channel, check the gateway Serial Monitor output (it prints the channel), or look in your router's admin page.
🔗Step 8: Upload and Test
- Upload the gateway sketch first. Open the Serial Monitor and confirm it connects to WiFi and MQTT.
- Upload Node 1 sketch (BME280). Watch the Serial Monitor -- it should print the sensor reading, send it, and go to sleep.
- Upload Node 2 sketch (soil moisture). Same process.
- On the gateway Serial Monitor, you should see messages arriving from each node.
- Use an MQTT client (like MQTT Explorer or
mosquitto_sub) to verify data reaches the broker:
mosquitto_sub -h 192.168.1.100 -t "sensors/#" -vYou should see output like:
sensors/node1/temperature 23.4
sensors/node1/humidity 45.2
sensors/node1/pressure 1013.2
sensors/node2/soil_moisture 67.3🔗Deep Sleep and Battery Life
The ESP32 draws about $150\,\text{mA}$ when WiFi is active, but only about $10\,\mu\text{A}$ in deep sleep. Since the sensor nodes only wake up briefly to take a reading and send it (roughly 200 ms of active time), the average current is extremely low.
For a node sleeping 5 minutes between readings:
$$I_{avg} = \frac{t_{active} \cdot I_{active} + t_{sleep} \cdot I_{sleep}}{t_{active} + t_{sleep}}$$
$$I_{avg} = \frac{0.2 \times 0.08 + 300 \times 0.00001}{0.2 + 300} \approx 0.063\,\text{mA}$$
With a pair of AA batteries (roughly $2500\,\text{mAh}$):
$$\text{Battery life} = \frac{2500}{0.063} \approx 39{,}700\,\text{hours} \approx 4.5\,\text{years}$$
In practice, battery self-discharge and sensor power consumption will reduce this, but you can reasonably expect several months to over a year on batteries.
Tip: To maximize battery life, power the BME280 from a GPIO pin instead of 3.3V. Set the pin HIGH before reading and LOW before sleeping. This eliminates the sensor's quiescent current during deep sleep.
🔗Calibration
🔗BME280
The BME280 is factory-calibrated and generally accurate to $\pm 1°\text{C}$ and $\pm 3\%$ RH. If you want to check accuracy, compare readings against a known thermometer.
🔗Soil Moisture
The capacitive soil moisture sensor needs manual calibration:
- Read the raw ADC value with the sensor in dry air (this is your "0% moisture" baseline)
- Read the raw ADC value with the sensor in a glass of water (this is your "100% moisture" baseline)
- Update
dryValueandwetValuein the code
🔗Adding More Nodes
To add a third sensor node (for example, a light sensor):
- Choose a unique
NODE_ID(e.g., 3) - Define a new
SENSOR_TYPE(e.g., 3 for light) - Use the same
SensorPacketstructure -- put the light reading invalue1 - Flash the new node with the same gateway MAC address
- On the gateway, add a new case in the
publishPacket()switch statement:
case 3: sensorName = "light"; break;Then add the MQTT publish logic:
else if (pkt.sensorType == 3) { // Light sensor
snprintf(topic, sizeof(topic), "sensors/node%d/light", pkt.nodeId);
snprintf(payload, sizeof(payload), "%.0f", pkt.value1);
mqtt.publish(topic, payload, true);
}No changes are needed on the existing nodes. The network scales easily up to 20 peers (the ESP-NOW limit on ESP32).
🔗Common Issues and Solutions
| Problem | Cause | Fix |
|---|---|---|
| Gateway never receives packets | WiFi channel mismatch | Set sensor nodes to the same channel as your router (see Step 7) |
| "ESP-NOW init failed" | WiFi not in STA mode | Make sure WiFi.mode(WIFI_STA) is called before esp_now_init() |
| Send callback reports FAIL | Gateway MAC address wrong | Double-check the MAC address bytes. Use the MAC printer sketch from Step 1 |
| BME280 not found | Wrong I2C address or bad wiring | Try address 0x77 instead of 0x76. Check SDA/SCL connections |
| Soil moisture reads 0 or 4095 | Wrong pin or sensor not connected | Use an ADC1 pin (GPIO 32-39). Check wiring |
| MQTT not publishing | Broker unreachable or wrong IP | Verify broker IP and port. Test with mosquitto_pub from another machine |
| Node wakes too often | SLEEP_SECONDS too low | Increase the sleep duration. 300-900 seconds is typical |
| Packets arrive but data is garbage | Struct mismatch between node and gateway | Make sure the SensorPacket struct is identical on all boards (same field order, same types, same __attribute__((packed))) |
| Node works on USB but not battery | Battery voltage too low | ESP32 needs at least 3.0V. Use a voltage regulator or 2S 18650 pack with a buck converter |
🔗Extensions
- Battery monitoring: Add a voltage divider (two 100K resistors) from the battery to GPIO 35 to track battery level. Publish it via MQTT so you know when to recharge.
- OLED on the gateway: Wire an SSD1306 OLED to the gateway to display the last reading from each node.
- Home Assistant integration: MQTT topics map directly to Home Assistant sensors. Add them via
configuration.yamlor MQTT auto-discovery. - Encryption: ESP-NOW supports CCMP encryption. Add a 16-byte key in
peerInfo.lmkand setpeerInfo.encrypt = trueon both sides. - Two-way communication: The gateway can send commands back to nodes (e.g., change sleep interval). Nodes would need to briefly listen after transmitting.
- Solar power: Add a small solar panel and TP4056 charge controller to make outdoor sensor nodes completely self-sustaining.