Wireless Communication Protocols: A Practical Guide to ESP-NOW, WiFi, and LoRaWAN
11/16/20258 min read


After mastering wired protocols, the next frontier is wireless communication. While wired connections are reliable and fast, wireless protocols give you freedom, mobility, and the ability to deploy devices where cables can't reach. In this guide, we'll explore three powerful wireless protocols—ESP-NOW, WiFi, and LoRaWAN—each serving different needs in the IoT ecosystem.
Understanding Wireless Options
ESP-NOW is Espressif's proprietary protocol for fast, low-power communication between ESP32 and ESP8266 devices. Think of it as a direct walkie-talkie connection—no router needed, minimal latency, and very power efficient. It's perfect for sensor networks and device-to-device communication.
WiFi needs no introduction—it's the ubiquitous protocol that connects billions of devices to the internet. With WiFi, your microcontroller can access cloud services, send data to servers, and be controlled from anywhere in the world. However, it's more power-hungry and requires infrastructure (a router).
LoRaWAN (Long Range Wide Area Network) is the long-distance champion, capable of transmitting small amounts of data over several kilometers with minimal power consumption. It's designed for IoT applications where devices need years of battery life and long-range connectivity matters more than speed.
ESP-NOW: Direct Device Communication
ESP-NOW operates on the same 2.4GHz band as WiFi but without the overhead of connecting to an access point. It supports one-to-one, one-to-many, and many-to-many communication patterns.
Key Characteristics:
Range: ~200m in open air, ~50-100m indoors
Speed: Up to 1 Mbps
Max payload: 250 bytes per message
Power: Much lower than WiFi
Latency: Very low (ms range)
ESP-NOW Sender Example
// ESP-NOW Sender (ESP32) #include <esp_now.h> #include <WiFi.h> // Replace with receiver's MAC address uint8_t receiverAddress[] = {0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}; // Structure to send data typedef struct struct_message { char deviceId[32]; float temperature; float humidity; int battery; } struct_message; struct_message myData; // Callback when data is sent void OnDataSent(const uint8_t mac_addr, esp_now_send_status_t status) { Serial.print("Last Packet Send Status: "); Serial.println(status == ESP_NOW_SEND_SUCCESS ? "Success" : "Fail"); } void setup() { Serial.begin(115200); // Set device as WiFi Station WiFi.mode(WIFI_STA); // Print MAC address Serial.print("MAC Address: "); Serial.println(WiFi.macAddress()); // Initialize ESP-NOW if (esp_now_init() != ESP_OK) { Serial.println("Error initializing ESP-NOW"); return; } // Register send callback esp_now_register_send_cb(OnDataSent); // Register peer esp_now_peer_info_t peerInfo; memcpy(peerInfo.peer_addr, receiverAddress, 6); peerInfo.channel = 0; peerInfo.encrypt = false; if (esp_now_add_peer(&peerInfo) != ESP_OK) { Serial.println("Failed to add peer"); return; } } void loop() { // Prepare data strcpy(myData.deviceId, "SENSOR_01"); myData.temperature = random(200, 350) / 10.0; myData.humidity = random(400, 800) / 10.0; myData.battery = random(50, 100); // Send message esp_err_t result = esp_now_send(receiverAddress, (uint8_t ) &myData, sizeof(myData)); if (result == ESP_OK) { Serial.println("Sent successfully"); } else { Serial.println("Error sending data"); } delay(2000); }
ESP-NOW Receiver Example
// ESP-NOW Receiver (ESP32) #include <esp_now.h> #include <WiFi.h> // Structure to receive data (must match sender) typedef struct struct_message { char deviceId[32]; float temperature; float humidity; int battery; } struct_message; struct_message incomingData; // Callback when data is received void OnDataRecv(const uint8_t mac, const uint8_t incomingDataBuffer, int len) { memcpy(&incomingData, incomingDataBuffer, sizeof(incomingData)); Serial.println("\n--- New Data Received ---"); Serial.print("Device ID: "); Serial.println(incomingData.deviceId); Serial.print("Temperature: "); Serial.print(incomingData.temperature); Serial.println(" °C"); Serial.print("Humidity: "); Serial.print(incomingData.humidity); Serial.println(" %"); Serial.print("Battery: "); Serial.print(incomingData.battery); Serial.println(" %"); // Print sender MAC address char macStr[18]; snprintf(macStr, sizeof(macStr), "%02X:%02X:%02X:%02X:%02X:%02X", mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]); Serial.print("From MAC: "); Serial.println(macStr); } void setup() { Serial.begin(115200); // Set device as WiFi Station WiFi.mode(WIFI_STA); // Print MAC address Serial.print("Receiver MAC Address: "); Serial.println(WiFi.macAddress()); // Initialize ESP-NOW if (esp_now_init() != ESP_OK) { Serial.println("Error initializing ESP-NOW"); return; } // Register receive callback esp_now_register_recv_cb(OnDataRecv); Serial.println("Receiver ready!"); } void loop() { // Nothing here - all handled by callback delay(1000); }
Getting MAC Address: Upload a simple sketch to get your ESP32's MAC address:
#include <WiFi.h> void setup() { Serial.begin(115200); WiFi.mode(WIFI_STA); Serial.print("MAC Address: "); Serial.println(WiFi.macAddress()); } void loop() {}
WiFi: Internet Connectivity
WiFi gives your ESP32 full internet access. You can host web servers, connect to cloud platforms, implement REST APIs, and much more.
Key Characteristics:
Range: ~50m indoors, ~100m outdoors
Speed: 150 Mbps (theoretical)
Power: High consumption
Infrastructure: Requires router/access point
Latency: Higher than ESP-NOW (tens of ms)
WiFi Client Example (Sending Data to Server)
// ESP32 WiFi Client - POST data to server #include <WiFi.h> #include <HTTPClient.h> const char* ssid = "YOUR_WIFI_SSID"; const char* password = "YOUR_WIFI_PASSWORD"; const char* serverUrl = "http://your-server.com/api/data"; void setup() { Serial.begin(115200); // Connect to WiFi WiFi.begin(ssid, password); Serial.print("Connecting to WiFi"); while (WiFi.status() != WL_CONNECTED) { delay(500); Serial.print("."); } Serial.println(); Serial.println("Connected to WiFi!"); Serial.print("IP Address: "); Serial.println(WiFi.localIP()); } void loop() { if (WiFi.status() == WL_CONNECTED) { HTTPClient http; // Prepare JSON data String jsonData = "{"; jsonData += "\"temperature\":" + String(random(200, 300) / 10.0) + ","; jsonData += "\"humidity\":" + String(random(400, 800) / 10.0) + ","; jsonData += "\"device\":\"ESP32_SENSOR\""; jsonData += "}"; // Send POST request http.begin(serverUrl); http.addHeader("Content-Type", "application/json"); int httpResponseCode = http.POST(jsonData); if (httpResponseCode > 0) { Serial.print("HTTP Response code: "); Serial.println(httpResponseCode); String response = http.getString(); Serial.println("Response: " + response); } else { Serial.print("Error code: "); Serial.println(httpResponseCode); } http.end(); } else { Serial.println("WiFi Disconnected"); } delay(10000); // Send every 10 seconds }
WiFi Web Server Example
// ESP32 WiFi Web Server #include <WiFi.h> #include <WebServer.h> const char* ssid = "YOUR_WIFI_SSID"; const char* password = "YOUR_WIFI_PASSWORD"; WebServer server(80); float temperature = 25.5; float humidity = 60.0; // Handle root page void handleRoot() { String html = "<!DOCTYPE html><html>"; html += "<head><meta name='viewport' content='width=device-width, initial-scale=1'>"; html += "<style>body{font-family:Arial;text-align:center;margin:50px;}"; html += ".sensor{font-size:3em;font-weight:bold;color:#0066cc;}</style></head>"; html += "<body><h1>ESP32 Sensor Dashboard</h1>"; html += "<p>Temperature</p><p class='sensor'>" + String(temperature) + " °C</p>"; html += "<p>Humidity</p><p class='sensor'>" + String(humidity) + " %</p>"; html += "<p><a href='/data'>Get JSON Data</a></p>"; html += "</body></html>"; server.send(200, "text/html", html); } // Handle JSON API endpoint void handleData() { String json = "{"; json += "\"temperature\":" + String(temperature) + ","; json += "\"humidity\":" + String(humidity) + ","; json += "\"timestamp\":" + String(millis()); json += "}"; server.send(200, "application/json", json); } // Handle LED control void handleLED() { if (server.hasArg("state")) { String state = server.arg("state"); if (state == "on") { digitalWrite(LED_BUILTIN, HIGH); server.send(200, "text/plain", "LED turned ON"); } else if (state == "off") { digitalWrite(LED_BUILTIN, LOW); server.send(200, "text/plain", "LED turned OFF"); } } else { server.send(400, "text/plain", "Missing state parameter"); } } void setup() { Serial.begin(115200); pinMode(LED_BUILTIN, OUTPUT); // Connect to WiFi WiFi.begin(ssid, password); Serial.print("Connecting to WiFi"); while (WiFi.status() != WL_CONNECTED) { delay(500); Serial.print("."); } Serial.println(); Serial.println("Connected to WiFi!"); Serial.print("IP Address: "); Serial.println(WiFi.localIP()); // Define routes server.on("/", handleRoot); server.on("/data", handleData); server.on("/led", handleLED); // Start server server.begin(); Serial.println("HTTP server started"); Serial.println("Visit: http://" + WiFi.localIP().toString()); } void loop() { server.handleClient(); // Update sensor readings temperature = random(200, 300) / 10.0; humidity = random(400, 800) / 10.0; delay(100); }
Testing the Web Server:
Visit http://[ESP32_IP_ADDRESS] for the dashboard
Visit http://[ESP32_IP_ADDRESS]/data for JSON
Visit http://[ESP32_IP_ADDRESS]/led?state=on to control LED
LoRaWAN: Long-Range IoT
LoRaWAN operates on sub-GHz frequencies (868 MHz in Europe, 915 MHz in US) enabling kilometer-range communication with years of battery life. It's ideal for remote sensors, agriculture, smart cities, and asset tracking.
Key Characteristics:
Range: 2-15 km (depends on environment)
Speed: Very slow (0.3 - 50 kbps)
Power: Extremely low
Payload: 51-222 bytes per message
Infrastructure: Requires LoRaWAN gateway
Note: You'll need LoRa hardware modules (like RFM95W, SX1276) or ESP32 boards with built-in LoRa (like Heltec WiFi LoRa 32).
LoRaWAN Sender (End Device)
// LoRaWAN Sender using LMIC library // Hardware: ESP32 with SX1276/RFM95W module // Install: MCCI LoRaWAN LMIC library #include <lmic.h> #include <hal/hal.h> #include <SPI.h> // LoRaWAN credentials - Get these from your network server (TTN, ChirpStack, etc.) static const u1_t PROGMEM APPEUI[8] = { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }; static const u1_t PROGMEM DEVEUI[8] = { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }; static const u1_t PROGMEM APPKEY[16] = { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }; void os_getArtEui (u1_t* buf) { memcpy_P(buf, APPEUI, 8); } void os_getDevEui (u1_t* buf) { memcpy_P(buf, DEVEUI, 8); } void os_getDevKey (u1_t* buf) { memcpy_P(buf, APPKEY, 16); } // Pin mapping for ESP32 const lmic_pinmap lmic_pins = { .nss = 18, .rxtx = LMIC_UNUSED_PIN, .rst = 14, .dio = {26, 33, 32}, }; static osjob_t sendjob; const unsigned TX_INTERVAL = 60; // Send every 60 seconds void onEvent (ev_t ev) { Serial.print(os_getTime()); Serial.print(": "); switch(ev) { case EV_JOINING: Serial.println("Joining network..."); break; case EV_JOINED: Serial.println("Network joined!"); LMIC_setLinkCheckMode(0); break; case EV_TXCOMPLETE: Serial.println("Transmission complete"); if (LMIC.txrxFlags & TXRX_ACK) Serial.println("Received ACK"); if (LMIC.dataLen) { Serial.print("Received "); Serial.print(LMIC.dataLen); Serial.println(" bytes"); } // Schedule next transmission os_setTimedCallback(&sendjob, os_getTime()+sec2osticks(TX_INTERVAL), do_send); break; case EV_TXSTART: Serial.println("Transmission started"); break; default: Serial.print("Unknown event: "); Serial.println(ev); break; } } void do_send(osjob_t* j) { // Check if there is not a current TX/RX job running if (LMIC.opmode & OP_TXRXPEND) { Serial.println("OP_TXRXPEND, not sending"); } else { // Prepare payload uint8_t payload[8]; // Temperature (2 bytes, fixed point) int16_t temp = random(200, 300); // 20.0 - 30.0°C payload[0] = (temp >> 8) & 0xFF; payload[1] = temp & 0xFF; // Humidity (2 bytes, fixed point) int16_t humidity = random(400, 800); // 40.0 - 80.0% payload[2] = (humidity >> 8) & 0xFF; payload[3] = humidity & 0xFF; // Battery voltage (2 bytes, mV) uint16_t battery = 3700; // 3.7V payload[4] = (battery >> 8) & 0xFF; payload[5] = battery & 0xFF; // Status byte payload[6] = 0x01; // Device OK // Sequence number payload[7] = LMIC.seqnoUp & 0xFF; // Send packet LMIC_setTxData2(1, payload, sizeof(payload), 0); Serial.println("Packet queued"); } } void setup() { Serial.begin(115200); Serial.println("Starting LoRaWAN device"); // LMIC initialization os_init(); LMIC_reset(); // Set data rate and transmit power LMIC_setDrTxpow(DR_SF7, 14); // Start job do_send(&sendjob); } void loop() { os_runloop_once(); }
LoRa Point-to-Point (Simpler Alternative)
If you don't need the full LoRaWAN stack, you can use simple LoRa point-to-point:
LoRa Sender:
// Simple LoRa Sender (no LoRaWAN) // Install: LoRa library by Sandeep Mistry #include <SPI.h> #include <LoRa.h> #define SCK 5 #define MISO 19 #define MOSI 27 #define SS 18 #define RST 14 #define DIO0 26 int counter = 0; void setup() { Serial.begin(115200); SPI.begin(SCK, MISO, MOSI, SS); LoRa.setPins(SS, RST, DIO0); if (!LoRa.begin(915E6)) { // 915 MHz (use 868E6 for Europe) Serial.println("Starting LoRa failed!"); while (1); } // Optional: Set spreading factor, bandwidth, etc. LoRa.setSpreadingFactor(7); LoRa.setSignalBandwidth(125E3); LoRa.setTxPower(20); Serial.println("LoRa Sender initialized"); } void loop() { Serial.print("Sending packet: "); Serial.println(counter); // Send packet LoRa.beginPacket(); LoRa.print("Sensor:"); LoRa.print(counter); LoRa.print(",Temp:"); LoRa.print(random(200, 300) / 10.0); LoRa.print(",Hum:"); LoRa.print(random(400, 800) / 10.0); LoRa.endPacket(); counter++; delay(10000); // Send every 10 seconds }
LoRa Receiver:
// Simple LoRa Receiver #include <SPI.h> #include <LoRa.h> #define SCK 5 #define MISO 19 #define MOSI 27 #define SS 18 #define RST 14 #define DIO0 26 void setup() { Serial.begin(115200); SPI.begin(SCK, MISO, MOSI, SS); LoRa.setPins(SS, RST, DIO0); if (!LoRa.begin(915E6)) { Serial.println("Starting LoRa failed!"); while (1); } LoRa.setSpreadingFactor(7); LoRa.setSignalBandwidth(125E3); Serial.println("LoRa Receiver initialized"); } void loop() { // Try to parse packet int packetSize = LoRa.parsePacket(); if (packetSize) { Serial.print("Received packet: '"); // Read packet while (LoRa.available()) { Serial.print((char)LoRa.read()); } Serial.print("' with RSSI "); Serial.println(LoRa.packetRssi()); } }
Comparison Table
Protocol Range Speed Power Latency Use Case ESP-NOW 50-200m Fast Low Very Low Sensor networks, home automation, device-to-device WiFi 50-100m Very Fast High Medium Internet access, cloud integration, streaming LoRaWAN 2-15km Very Slow Ultra Low High Remote sensors, agriculture, asset tracking
When to Use Each Protocol
Choose ESP-NOW when:
You need fast, local communication between ESP devices
Power efficiency matters but you need decent speed
No internet connectivity is required
Building a mesh network of sensors
Example: Smart home sensors, RC vehicles, local automation
Choose WiFi when:
You need internet access for cloud services
High data throughput is required
Power consumption is not critical (plugged in)
Remote access and control is needed
Example: Security cameras, smart displays, web servers, IoT dashboards
Choose LoRaWAN when:
Devices are spread over large areas
Battery life needs to be years, not days
Data payloads are small and infrequent
Cellular coverage is unavailable or expensive
Example: Soil moisture sensors, parking sensors, livestock tracking, environmental monitoring
Power Consumption Comparison
Typical power consumption (approximate):
ESP-NOW:
Active transmission: ~120-240 mA
Sleep mode with wake on receive: ~0.8 mA
WiFi:
Active transmission: ~200-400 mA
Connected idle: ~100 mA
Deep sleep: ~10 µA (but loses connection)
LoRaWAN:
Transmission: ~100-150 mA (brief)
Sleep mode: ~1-10 µA
Can sleep between transmissions
Getting Started Tips
For ESP-NOW:
No additional hardware needed—built into ESP32/ESP8266
Get MAC addresses of all devices first
Start with simple one-way communication before bidirectional
For WiFi:
Ensure stable WiFi coverage in deployment area
Consider power supply—WiFi drains batteries quickly
Implement reconnection logic for reliability
Use mDNS for easy local discovery
For LoRaWAN:
Register with a LoRaWAN network (The Things Network is free)
Check local frequency regulations (868/915 MHz)
Position gateway within range or use public gateways
Keep payloads small—you're charged by airtime
Respect duty cycle limits (1% in Europe)
Conclusion
Each wireless protocol has its place in the IoT ecosystem. ESP-NOW excels at local, low-latency device communication. WiFi dominates when you need internet connectivity and high speeds. LoRaWAN wins for long-range, ultra-low-power applications where small data packets are sufficient.
For many projects, you might even combine protocols—use ESP-NOW for local sensor networks that feed into a WiFi gateway, or deploy LoRaWAN sensors in the field that report to a WiFi-connected hub.
The examples provided are starting points you can modify and expand. Experiment with each protocol to understand their strengths and limitations, and choose based on your specific requirements for range, power, speed, and infrastructure availability. Happy wireless building!