LoRa: How to Send Data 5km with Arduino (No WiFi)
WiFi barely reaches your driveway. LoRa reaches the next town. Master the SX1278 module, Chirp Spread Spectrum physics, and build a 5km range sensor network.
Welcome to Day 31. The Grand Finale.
For the past 30 days, we have been “Learning”. We learned Ohm’s Law. We learned how a Transistor amplifies current. We learned how I2C moves bits between chips. We learned how HTTP requests traverse the globe. But “knowing” is not “building”. Engineering is the art of Integration. It is the ability to take ten disparate, conflicting components—some that want 5V, some that want 3.3V, some that block code, some that need microseconds—and making them dance together in perfect unison.
Today, we stop being “Hobbyists” tinkering with breadboards. Today, we become Systems Engineers.
We are going to build the technochips Smart Hub. This is not a toy. This is a prototype for a commercial product.
The Mission Specification:

Before we write a single line of code, we must understand the “System”.
A novice writes a loop() that reads a sensor, then updates the screen, then checks Wi-Fi.
This is Sequential Architecture. It is fatal for IoT.
Why?
If you sequence these, your User Interface (Web Dashboard) will lag. When a user taps “Fan ON” on their phone, the ESP8266 might be busy drawing pixels on the OLED. The TCP packet will be dropped. The user will think “This device is broken.”
The Solution: The Non-Blocking Super-Loop
We will implement a Cooperative Multitasking Scheduler. We will slice our timeline into millisecond-sized chunks. The CPU will “poll” tasks continuously, only executing them when their specific time slot arrives. We will use the millis() hardware timer as our master clock.

This architecture separates our concerns into three layers:
A valid question is: “Why not use FreeRTOS?” The ESP8266 does run a lightweight RTOS (Real-Time Operating System) in the background to manage Wi-Fi. However, adding user-level threads consumes stack memory per task.
We use millis() for coarse timing (ms level). But what if the DHT11 needs microsecond timing?
The DHT11 library uses ISRs. It halts the CPU for 50us to catch the rising edge of a signal.
Critical Risk: If your Wi-Fi interrupts an ISR, the system crashes (WDT Reset).
Mitigation: noInterrupts() and interrupts() are used carefully around critical timing sections to ensure data integrity.
Why did we choose the ESP8266 (6.00)?
We are combining high-speed digital logic (3.3V) with mains power control (110V/220V). This requires a deep understanding of the physics involved.
You cannot connect a 5V relay coil directly to an ESP8266 GPIO.

The Engineering Solution: We use a Relay Module with an onboard Optocoupler (PC817) and Flyback Diode.
We are using the I2C Protocol (Inter-Integrated Circuit).
The ESP8266 is a current-hungry beast.
Correct wiring is non-negotiable for system stability.

DANGER: 110V/220V KILLS. If you are switching a real load (Lamp/Heater):

How long will this hub last?
We are effectively writing a small Operating System. The ESP8266 has a Harvard Architecture (Modified). It has distinct Instruction Memory (Flash) and Data Memory (RAM).
A common rookie mistake is defining the HTML string like this:
String html = "<html>...</html>";
This puts the string in RAM. With a large dashboard, you will crash the chip (Stack Overflow).
new). Grows upwards.PROGMEM keyword.
const char html[] PROGMEM = "<html>...</html>";
This forces the compiler to keep the data in Flash Memory (Instruction Store), leaving RAM totally free for the Stack.Reliability is our #1 goal. The ESP8266 hardware contains a “Watchdog”. It is a countdown timer. If the CPU does not reset this timer every ~3 seconds, the Watchdog assumes the code has crashed and Hard Resets the board.
delay(5000) stops the CPU. The Watchdog does not get fed. The board reboots.
Sensors are noisy. Air turbulence causes the DHT11 to jitter.
23.1, 23.2, 23.1, 28.5 (Error), 23.2
If we use raw data, our Relay will “chatter” (Click-On-Click-Off).
We implement Digital Signal Processing (DSP): The Moving Average Filter.
We maintain a circular buffer of the last N readings.
This smooths out noise and rejects transient spikes.

The “Internet” in IoT is just HTTP. When your phone asks the hub for temperature, a complex dance occurs.
GET /temperature HTTP/1.1.200 OK, Content-Type: text/plain) followed by the Body (24.5).Keep-Alive is active).When you send “24.5”, you aren’t just sending 4 bytes. You are sending a 1500-byte Ethernet Frame:

We are using AJAX Polling (Client pulls data every 2s).
We send data as JSON: {"temp": 24.5}.
0x41C40000.Connecting a heater to the internet creates a vector for cyber-physical attacks.
You see const char* password = "My_Password"; in the code.
secrets.h file and add it to .gitignore. Never commit keys.An ESP8266 has limited sockets (max 4). If a malicious actor opens 4 connections and keeps them open (“Slowloris Attack”), the hub becomes unresponsive.
client.stop().Our AJAX requests come from the same origin (the ESP itself).
But if a malicious website tries to fetch('http://192.168.1.50/toggle'), the browser blocks it by default.
DO NOT add Access-Control-Allow-Origin: * headers unless you understand the risk. This header allows any website to toggle your fans.
The interface is not just a “page”. It is an Application. We use CSS3 Grid/Flexbox for layout and Vanilla JS for logic. No heavy frameworks (React/Vue) needed.
We want a “Dark Mode” aesthetic using Material Design principles.
box-shadow: 0 10px 30px rgba(0,0,0,0.5)).conic-gradient) that changes color dynamically based on temperature.transition: all 0.3s ease) to make button presses feel tactile.
The browser runs a minimal state machine:
Disconnected (Gray text).Connected (Green/Blue text).Offline (Red text).This gives the user immediate feedback if the hub loses power, rather than just showing a frozen number.
Why use CSS Updates instead of replacing innerHTML?
conic-gradient. Changing the gradient is a Repaint. It is silky smooth (60fps) even on an old Android phone.To go from “Prototype” to “Product”, we need a Bill of Materials (BoM) and an Assembly Process.
| Component | SKU | Cost | Notes |
|---|---|---|---|
| Microcontroller | NodeMCU v3 | $3.50 | The brain. |
| Display | 0.96” OLED I2C | $2.50 | 128x64 White Pixel. |
| Sensor | DHT11 Module | $1.50 | Includes 10k pull-up. |
| Relay | 1-Ch 5V Relay | $1.00 | With Optocoupler. |
| Power | 5V 2A PSU | $0.00 | Recycled Phone Charger. |
| Case | Project Box | $2.00 | ABS Plastic (Safety). |
| Total | $10.50 |

Here is the complete, integrated firmware. It combines the millis() scheduler, ArduinoOTA, ESP8266WebServer, and SSD1306 logic.
/**
* Project: DeepMind SafeHome Hub (Capstone Edition)
* Author: TechnoChips
* Hardware: ESP8266, DHT11, SSD1306, 5V Relay
* License: MIT
*/
#include <ESP8266WiFi.h>
#include <ESP8266WebServer.h>
#include <ArduinoOTA.h>
#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
#include <DHT.h>
// --- Configuration Constants ---
const char* ssid = "YOUR_WIFI_SSID";
const char* password = "YOUR_WIFI_PASS";
const char* ota_pass = "admin123";
#define RELAY_PIN 14 // GPIO 14 (D5) - Safe Pin
#define DHT_PIN 12 // GPIO 12 (D6) - Safe Pin
#define DHT_TYPE DHT11
// --- Objects ---
Adafruit_SSD1306 display(128, 64, &Wire, -1);
DHT dht(DHT_PIN, DHT_TYPE);
ESP8266WebServer server(80);
// --- Global State ---
float currentTemp = 0.0;
float currentHum = 0.0;
bool relayState = false;
unsigned long lastSensorRead = 0;
unsigned long lastDisplayUpdate = 0;
const long SENSOR_INTERVAL = 2000; // 2 Seconds
const long DISPLAY_INTERVAL = 1000; // 1 Second
// --- Signal Processing Buffer ---
const int FILTER_SIZE = 5;
float tempBuffer[FILTER_SIZE];
int bufferIndex = 0;
// --- HTML Application (PROGMEM for Flash Storage) ---
const char index_html[] PROGMEM = R"rawliteral(
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>SafeHome Hub</title>
<style>
:root { --primary: #00bcd4; --bg: #121212; --card: #1e1e1e; --text: #fff; }
body { font-family: 'Segoe UI', sans-serif; background: var(--bg); color: var(--text); display: flex; justify-content: center; align-items: center; min-height: 100vh; margin: 0; }
.hub-card { background: var(--card); padding: 2rem; border-radius: 20px; box-shadow: 0 10px 40px rgba(0,0,0,0.5); width: 300px; text-align: center; }
h1 { margin: 0 0 20px 0; font-weight: 300; letter-spacing: 2px; }
.gauge { width: 150px; height: 150px; border-radius: 50%; border: 10px solid #333; margin: 0 auto 20px; display: flex; align-items: center; justify-content: center; position: relative; }
.temp-display { font-size: 3rem; font-weight: bold; }
.unit { font-size: 1rem; color: #888; }
.status-dot { width: 10px; height: 10px; background: #555; border-radius: 50%; display: inline-block; margin-right: 5px; box-shadow: 0 0 5px #555; transition: all 0.3s; }
.status-dot.active { background: #00ff00; box-shadow: 0 0 10px #00ff00; }
/* Toggle Switch */
.switch-container { display: flex; justify-content: space-between; align-items: center; background: #2a2a2a; padding: 15px; border-radius: 10px; }
.switch { position: relative; display: inline-block; width: 50px; height: 28px; }
.switch input { opacity: 0; width: 0; height: 0; }
.slider { position: absolute; cursor: pointer; top: 0; left: 0; right: 0; bottom: 0; background-color: #444; transition: .4s; border-radius: 34px; }
.slider:before { position: absolute; content: ""; height: 20px; width: 20px; left: 4px; bottom: 4px; background-color: white; transition: .4s; border-radius: 50%; }
input:checked + .slider { background-color: var(--primary); }
input:checked + .slider:before { transform: translateX(22px); }
</style>
</head>
<body>
<div class="hub-card">
<h1>HUB STATUS</h1>
<div class="gauge" id="gaugeBody">
<div>
<span class="temp-display" id="temp">--</span>
<span class="unit">°C</span>
</div>
</div>
<div class="switch-container">
<span>Active Cooling</span>
<label class="switch">
<input type="checkbox" id="fanToggle" onchange="toggleFan()">
<span class="slider"></span>
</label>
</div>
<p style="margin-top:20px; color:#555; font-size:0.8rem;">
<span class="status-dot" id="connDot"></span> Live Connection
</p>
</div>
<script>
let failCount = 0;
function update() {
fetch('/status')
.then(response => response.json())
.then(data => {
// Update Temp
document.getElementById('temp').innerText = data.t.toFixed(1);
// Update Toggle (only if user isn't interacting)
// document.getElementById('fanToggle').checked = data.r;
// Update Connection Status
document.getElementById('connDot').classList.add('active');
failCount = 0;
// Update Gauge Color
let hue = 200 - (data.t * 4); // Blue to Red mapping
document.getElementById('gaugeBody').style.border = `10px solid hsl(${hue}, 100%, 50%)`;
})
.catch(err => {
failCount++;
if(failCount > 2) document.getElementById('connDot').classList.remove('active');
});
}
function toggleFan() {
fetch('/toggle'); // Fire and forget
}
setInterval(update, 2000); // The Heartbeat
update();
</script>
</body>
</html>
)rawliteral";
void setup() {
Serial.begin(115200);
// 1. Hardware Init
pinMode(RELAY_PIN, OUTPUT);
digitalWrite(RELAY_PIN, HIGH); // Relay OFF (Active LOW usually)
// 2. Display Init
if(!display.begin(SSD1306_SWITCHCAPVCC, 0x3C)) {
Serial.println(F("OLED Failed"));
for(;;);
}
display.clearDisplay();
display.setTextColor(WHITE);
display.println("Booting System...");
display.display();
// 3. WiFi Init
WiFi.mode(WIFI_STA);
WiFi.begin(ssid, password);
while (WiFi.status() != WL_CONNECTED) {
delay(500);
display.print(".");
display.display();
}
// 4. OTA Init
ArduinoOTA.setHostname("SafeHome-Hub-v1");
ArduinoOTA.setPassword(ota_pass);
ArduinoOTA.begin();
// 5. Server Routes
server.on("/", HTTP_GET, []() {
server.send_P(200, "text/html", index_html);
});
server.on("/status", HTTP_GET, []() {
// JSON API
String json = "{";
json += "\"t\":" + String(currentTemp) + ",";
json += "\"h\":" + String(currentHum) + ",";
json += "\"r\":" + String(relayState);
json += "}";
server.send(200, "application/json", json);
});
server.on("/toggle", HTTP_GET, []() {
relayState = !relayState;
digitalWrite(RELAY_PIN, relayState ? LOW : HIGH);
server.send(200, "text/plain", "OK");
});
server.begin();
dht.begin();
// Init average buffer
for(int i=0; i<FILTER_SIZE; i++) tempBuffer[i] = 25.0;
}
void loop() {
// CRITICAL: Handle Background Tasks
server.handleClient();
ArduinoOTA.handle();
unsigned long now = millis();
// TASK 1: Sensor Read (Every 2s)
if (now - lastSensorRead >= SENSOR_INTERVAL) {
lastSensorRead = now;
float newT = dht.readTemperature();
float newH = dht.readHumidity();
if (!isnan(newT)) {
// Moving Average
tempBuffer[bufferIndex] = newT;
bufferIndex = (bufferIndex + 1) % FILTER_SIZE;
float sum = 0;
for(int i=0; i<FILTER_SIZE; i++) sum += tempBuffer[i];
currentTemp = sum / FILTER_SIZE; // Use filtered value
currentHum = newH;
// Automation Logic (Thermostat)
if (currentTemp > 28.0 && !relayState) {
relayState = true;
digitalWrite(RELAY_PIN, LOW); // ON
} else if (currentTemp < 26.0 && relayState) {
relayState = false;
digitalWrite(RELAY_PIN, HIGH); // OFF
}
}
}
// TASK 2: UI Update (Every 1s)
// Decoupled from Sensor Read so animations feel smooth
if (now - lastDisplayUpdate >= DISPLAY_INTERVAL) {
lastDisplayUpdate = now;
display.clearDisplay();
display.setTextSize(1);
display.setCursor(0,0);
display.print("WikiHub IP:");
display.setCursor(0,10);
display.print(WiFi.localIP());
display.drawLine(0, 20, 128, 20, WHITE);
display.setTextSize(2);
display.setCursor(10, 30);
display.print(currentTemp, 1);
display.print(" C");
display.setTextSize(1);
display.setCursor(10, 55);
display.print(relayState ? "FAN: ON" : "FAN: OFF");
// Draw Activity Bar to show system is alive
int barWidth = (millis() % 1000) / 8; // 0 to 125
display.fillRect(0, 62, barWidth, 2, WHITE);
display.display();
}
}
A generic “It doesn’t work” doesn’t help. We debug with data.
If your Serial Monitor spams garbage and resets:
Exception (29): vaddr=0x3ff....
This is a StoreProhibited error. You likely tried to write to a pointer that is NULL.
display.begin() before calling display.print()?readings array index out of bounds?You tried to read a 4-byte integer from an address not divisible by 4.
memcpy instead of casting pointers.Is your hub disconnecting?
Add this line to your updateOLED() function:
display.print(WiFi.RSSI());
If the OLED is dead, run this diagnostic code in setup():
Wire.begin();
for(byte i = 8; i < 120; i++) {
Wire.beginTransmission(i);
if (Wire.endTransmission() == 0) {
Serial.print("Found I2C Device at: 0x");
Serial.println(i, HEX);
}
}
If this prints nothing? Your wires are broken.
The Hub is built. But a Systems Engineer is never finished. Because the code is modular, adding features is trivial:
SinricPro library to expose the Toggle function to Alexa/Google Home.currentTemp to a Google Sheet via IFTTT webhook every hour.Currently, we use “Bang-Bang” control:
if (28.1 > 28.0) Fan = ON
if (27.9 < 28.0) Fan = OFF
This oscillates.
Professional Solution: PID (Proportional-Integral-Derivative).


If you have followed us from Day 1 to Day 31: Congratulations. You have gone from blinking an LED to building a Cloud-Connected, Self-Updating, Multi-Tasking IoT Product. You have learned that “Smart Home” is not magic. It is just Physics, Code, and Logic.
The world is full of “Black Boxes” that you are not supposed to open. Open them. Build your own. Control your world.
This concludes the January Zero-to-Hero Series. See you in February for: PCB Design & Embedded Linux.
Q: Can I use an ESP32 instead?
A: Yes. The code is 95% compatible. You just need to change the pin definitions (#define RELAY_PIN 15).
Q: Can I power this with a battery? A: Not recommended. The Relay and Diode consume ~100mA constantly. A 2000mAh battery would last < 20 hours. This is a mains-powered device.
Q: Why is my temperature reading 50°C? A: Heat rises. If you put the sensor above the ESP8266 inside a small box, it measures the CPU heat, not the room heat. Solution: Mount the sensor outside the box or verify airflow.
Q: How do I access this from the internet (outside my home)? A: Do NOT port forward this device. It lacks HTTPS/TLS. To access remotely, use a VPN (like Tailscale) or an MQTT Broker (like AWS IoT Core).
Copyright © 2026 TechnoChips. Open Source Hardware (OSHW).