I bought a pile of sensor modules in 2019 and most of them sat in a drawer. This project exists to put as many of those modules on one system as I could: two DHT11s, an LDR module, an MQ smoke module, an MQ alcohol module, a black IR flame sensor, an HC-05, an HC-06, and a 4-pin SSD1306 OLED. No cloud account. The page is served on the LAN.
Three boards, because one board cannot hold the outdoor radio and the indoor analog inputs and still leave UART0 free for flashing the web board. Arduino Nano outdoors. Arduino Mega 2560 indoors. NodeMCU LoLin V3 (ESP-12E) for the page.
Outdoor node (Arduino Nano + HC-06)
The Arduino Nano reads a DHT11 on D2 every five seconds and sends one line to an HC-06 slave over SoftwareSerial (D10 RX, D11 TX, 9600 8N1). The payload is:
OUT_TEMP:24.0,OUT_HUM:84.0If the DHT11 read fails, the Arduino Nano keeps the last valid sample and does not send until it has a valid reading. The Nano has its own USB supply. It is not powered from the Arduino Mega 2560 5 V rail.
Indoor controller (Arduino Mega 2560)
The Arduino Mega 2560 is the HC-05 master on Serial1 (HC-05 TX to D19, Mega D18 through a 1 kΩ / 2 kΩ divider to HC-05 RX, 9600 baud). It reads:
- DHT11 on D22, indoor temperature and humidity
- LDR module analog out on A0, reported as a percent, not lux
- MQ smoke module analog out on A1, raw 0–1023
- MQ alcohol module analog out on A2, raw 0–1023
- IR flame sensor: digital out on D24 (active low, 2 of 3 samples) and analog IR on A3
Outdoor values are dropped after 20 seconds with no HC-06 line. Every 3 seconds Serial2 (TX2 D16, RX2 D17, 9600) sends one checksummed packet to the NodeMCU. CHK is the sum of the ASCII bytes before ,CHK:, mod 10000. The NodeMCU rejects the line if a field is missing or the checksum does not match. That is what caught a bad divider on D16.
A 128×64 SSD1306 at I2C address 0x3C (SDA D20, SCL D21) cycles four pages every 4 seconds: indoor climate, outdoor climate, smoke / alcohol / flame, then light. The serial packet still goes out if the OLED is unplugged.
Web front (NodeMCU LoLin V3, ESP-12E)
UART0 stays on the USB serial port at 115200 for flashing and logs. The Arduino Mega 2560 link is SoftwareSerial on D5 (GPIO14, RX) and D6 (GPIO12, TX), 9600 8N1, 256-byte buffer. The NodeMCU LoLin V3 joins the 2.4 GHz SSID, serves / and /api/data, and marks the Mega link stale after 12 seconds without a valid packet.
The dashboard is plain HTML and a fetch loop. No framework on the device, no Google Fonts, no CDN. Everything has to fit in flash and still be readable on a phone on the same subnet.


The router is already the server
The access point is a Xiaomi AX3200. Last year I flashed the stock firmware off it and put OpenWrt on. That was for the house network, not for this project, but it is why the dashboard has a name instead of a bookmark full of DHCP addresses.
The NodeMCU asks for the hostname environment. OpenWrt's dnsmasq already expands DHCP leases into the .lan domain, so anything using the router for DNS can open http://environment.lan. The board also announces environment.local over mDNS, which is what Apple devices pick up. Android is hit or miss on .local, so .lan is the one I actually type. If a phone is using public DNS or private DNS, the name fails and the reserved IP still works.
An Onion Omega was the other option I already knew from 2018. It is a Linux module that runs its own OpenWrt image, with its own filesystem and its own web server. This page is one HTML file and one JSON route. The Xiaomi AX3200 already does DHCP, DNS, and NTP. The NodeMCU LoLin V3 only has to associate and serve those two routes. An Onion Omega would have been a second router for no extra sensor.
Wiring detail that actually mattered
- Arduino Mega 2560 TX2 (D16) is 5 V. NodeMCU D5 is 3.3 V. 1 kΩ from D16 to the midpoint, 2 kΩ from the midpoint to ground, midpoint to D5. Same divider on HC-05 RX and HC-06 RX.
- Mega GND tied to NodeMCU GND. MQ heater current comes from a 5 V 2 A supply on the Mega, not from the NodeMCU 3.3 V pin. NodeMCU stays on its own USB cable.
- HC-06 silence timeout is 20 seconds. The HTTP page calls the Mega link stale at 12 seconds. Those are different timers.
ADC, not ppm
The MQ smoke module and the MQ alcohol module are read as 10-bit ADC counts. The page marks Elevated at 400 and High at 700. Those bands are not a calibration, and they are not parts per million. The IR flame decision is the digital pin, not the A3 voltage.