Home IoT Hub Series · 02

Building the Brain — Assembling the IoT Hub Body with an ESP8266 + OLED + Buzzer

Building the brain — assembling the IoT Hub body with an ESP8266 + OLED + buzzer, cover image

In Part 1 we drew the whole picture. Now we actually build it. No soldering — copy-paste the code and you're done.

Parts and cost

PartQtyUnit price (approx. USD)Notes
NodeMCU v3 (ESP8266)1~$1.70The brain body. A D1 Mini works too
SSD1306 128×64 OLED (I2C)1~$1.200.96" white
Piezo buzzer1~$0.203–5V active type
Jumper wires (female-female)6~$0.35
Breadboard1~$1.00Skip it and wire direct
Micro USB cable1~$0.70Power + code upload
Total~$5.00

Pin wiring

Building the brain — ESP8266 + OLED + buzzer wiring diagram

No breadboard needed — wire it straight up with jumper wires. The NodeMCU pin numbers are printed right on the front of the board as D2, D5, and so on, so just plug them in as labeled.

Sensor/moduleBoard connection
VCC3.3V
GNDGND
SDAD2 (GPIO4)
SCLD1 (GPIO5)
Buzzer (+)D7 (GPIO13)
Buzzer (-)GND
GPIO4 and GPIO5 are printed on the NodeMCU as D2 and D1. Just remember that the D-prefixed labels and the GPIO numbers are different. Wire it straight from the table above and you won't get confused.

First: install the libraries

Open the Arduino IDE and, under Tools → Manage Libraries, install these two.

LibrarySearch term
Adafruit SSD1306Adafruit SSD1306
Adafruit GFX LibraryAdafruit GFX

You also need the ESP8266 board manager installed. If it isn't, paste the address below into Arduino → Preferences → Additional Boards Manager URLs, then install ESP8266 from Tools → Board → Boards Manager.

https://arduino.esp8266.com/stable/package_esp8266com_index.json

🚨 Before you upload — do this first!!

Copy the code below into the Arduino IDE, then change just two spots. Never just upload it as-is. If you don't change them, it won't connect to Wi-Fi.

Building the Brain — ESP8266 + OLED + buzzer — full code
const char* ssid     = "YOUR_SSID";     // ← your home router's name here
const char* password = "YOUR_PASSWORD"; // ← your home router's password here
Delete the words YOUR_SSID and YOUR_PASSWORD entirely, and — leaving the double quotes in place — drop in your own info.

>

Example: const char* ssid = "MyHome_2G";

>

Example: const char* password = "abcd1234";

>

The ESP8266 only picks up 2.4GHz Wi-Fi. If your router is 5GHz, make sure 2.4GHz is switched on too.

Full code

Once you've changed the SSID and password, copy-paste the rest exactly as-is.

Building the Brain — ESP8266 + OLED + buzzer — full code
#include <ESP8266WiFi.h>
#include <ESP8266WebServer.h>
#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>

// =============================================
// 1. Wi-Fi settings (fill these in yourself!)
// =============================================
const char* ssid     = "YOUR_SSID";     // home router name
const char* password = "YOUR_PASSWORD"; // home router password

// Own AP settings for the sensor nodes
const char* ap_ssid     = "IoT_Hub_Net";
const char* ap_password = "iothub1234"; // password for sensor nodes to connect

// =============================================
// 2. Hardware pins
// =============================================
#define BUZZER_PIN    13  // D7 (GPIO13)
#define SCREEN_WIDTH 128
#define SCREEN_HEIGHT 64
#define OLED_ADDR    0x3C

Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, -1);
ESP8266WebServer server(80);

// =============================================
// 3. 20 memory slots
// =============================================
#define SLOT_COUNT 20
String slot[SLOT_COUNT] = {
  "--", "--", "--", "--", "--",
  "--", "--", "--", "--", "--",
  "--", "--", "--", "--", "--",
  "--", "--", "--", "--", "--"
};

const char* label[SLOT_COUNT] = {
  "Living Temp","Living Hum","Outside Temp","AC",
  "Boiler","Stove","Gas Leak","Fire",
  "Front Door","Door Lock","Living Light","Fridge Temp",
  "CO2","Dust PM2.5","Power","Water",
  "Hot Water","Rain","Bath Hum","Pet Bowl"
};

// =============================================
// 4. Timer variables
// =============================================
unsigned long lastScroll = 0;
unsigned long lastBuzzer = 0;
int scrollOffset = 0;
const int ITEMS_PER_PAGE = 4;
const int SCROLL_INTERVAL = 3000;
bool warningActive = false;

// =============================================
// setup
// =============================================
void setup() {
  Serial.begin(115200);

  // Buzzer init
  pinMode(BUZZER_PIN, OUTPUT);
  digitalWrite(BUZZER_PIN, LOW);

  // OLED init
  Wire.begin(4, 5); // SDA=D2(GPIO4), SCL=D1(GPIO5)
  if (!display.begin(SSD1306_SWITCHCAPVCC, OLED_ADDR)) {
    Serial.println("OLED init failed");
    while (1) yield();
  }
  display.clearDisplay();
  display.setTextColor(WHITE);
  display.setTextWrap(false);

  // Boot logo
  display.setTextSize(1);
  display.setCursor(10, 20);
  display.println("IoT Hub v2.0");
  display.setCursor(15, 40);
  display.println("iothub.co.kr");
  display.display();
  delay(1000);

  // ── Wi-Fi: connect to the home router in STA mode ──
  WiFi.mode(WIFI_AP_STA); // AP + STA simultaneous mode
  WiFi.begin(ssid, password);

  display.clearDisplay();
  display.setCursor(0, 0);
  display.println("WiFi connecting...");
  display.display();

  Serial.print("Connecting to WiFi");
  int dots = 0;
  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(".");
    display.setCursor(dots * 6, 16);
    display.print(".");
    display.display();
    dots++;
    if (dots > 20) dots = 0; // fake a line break
  }
  Serial.println();
  Serial.print("STA IP: ");
  Serial.println(WiFi.localIP());

  // ── Soft AP: create own WiFi for the sensor nodes ──
  WiFi.softAP(ap_ssid, ap_password);
  Serial.print("AP  IP: ");
  Serial.println(WiFi.softAPIP());

  // ── Web server routing ──
  server.on("/set", handleSet);
  server.on("/", handleRoot);
  server.on("/status", handleStatus);
  server.onNotFound(handleNotFound);
  server.begin();
  Serial.println("HTTP server started");

  // ── Show the IP on the OLED ──
  display.clearDisplay();
  display.setTextSize(1);
  display.setCursor(0, 0);
  display.println("IoT Hub Ready");
  display.setCursor(0, 14);
  display.print("LAN: ");
  display.println(WiFi.localIP());
  display.setCursor(0, 28);
  display.print("AP : ");
  display.println(WiFi.softAPIP());
  display.setCursor(0, 46);
  display.println("/set?no1=25.5");
  display.println("  to send data");
  display.display();
  delay(3000);
}

// =============================================
// loop
// =============================================
void loop() {
  server.handleClient();
  updateOLED();
  updateBuzzer();
}

// =============================================
// Check warning state
// =============================================
bool checkWarning() {
  for (int i = 0; i < SLOT_COUNT; i++) {
    // slots 7 (gas leak), 8 (fire), 9 (front door open) → warn if not 0
    if ((i == 6 || i == 7 || i == 8)
        && slot[i] != "0" && slot[i] != "--") {
      return true;
    }
    // slot 20 (dog food bowl) → warn if under 50g
    if (i == 19 && slot[i] != "--" && slot[i].toInt() < 50) {
      return true;
    }
  }
  return false;
}

// =============================================
// Buzzer control (beep every 500ms when warning)
// =============================================
void updateBuzzer() {
  warningActive = checkWarning();
  if (warningActive) {
    unsigned long now = millis();
    if (now - lastBuzzer < 500) return;
    lastBuzzer = now;
    digitalWrite(BUZZER_PIN, !digitalRead(BUZZER_PIN));
  } else {
    digitalWrite(BUZZER_PIN, LOW);
  }
}

// =============================================
// GET /set?no1=value&no2=value ...
// =============================================
void handleSet() {
  int updated = 0;
  for (int i = 1; i <= SLOT_COUNT; i++) {
    String paramName = "no" + String(i);
    if (server.hasArg(paramName)) {
      slot[i - 1] = server.arg(paramName);
      updated++;
      Serial.printf("Slot %d (%s) <- %s\n",
                    i, label[i - 1], slot[i - 1].c_str());
    }
  }
  String msg = "{\"updated\":" + String(updated) + "}";
  server.send(200, "application/json", msg);
}

// =============================================
// GET /  → HTML dashboard
// =============================================
void handleRoot() {
  String html = "<!DOCTYPE html>\n"
  "<html lang=\"ko\">\n"
  "<head>\n"
  "<meta charset=\"UTF-8\">\n"
  "<meta name=\"viewport\" content=\"width=device-width,initial-scale=1.0\">\n"
  "<title>IoT Hub</title>\n"
  "<style>\n"
  "*{margin:0;padding:0;box-sizing:border-box;}\n"
  "body{font-family:system-ui,sans-serif;background:#1a1a2e;color:#eee;padding:16px;}\n"
  "h1{text-align:center;font-size:1.3rem;margin-bottom:4px;color:#e94560;}\n"
  ".sub{text-align:center;font-size:0.7rem;color:#555;margin-bottom:16px;}\n"
  ".grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(185px,1fr));gap:10px;}\n"
  ".card{background:#16213e;border-radius:8px;padding:12px;border-left:4px solid #0f3460;}\n"
  ".card.warn{border-left-color:#e94560;background:#1c0e1a;}\n"
  ".card .no{font-size:0.6rem;color:#555;margin-bottom:2px;}\n"
  ".card .label{font-size:0.8rem;color:#a0a0b0;margin-bottom:4px;}\n"
  ".card .value{font-size:1.2rem;font-weight:700;}\n"
  ".footer{text-align:center;margin-top:16px;font-size:0.7rem;color:#444;}\n"
  "</style>\n"
  "</head>\n"
  "<body>\n"
  "<h1>IoT Hub</h1>\n"
  "<div class=\"sub\">iothub.co.kr</div>\n"
  "<div class=\"grid\">\n";

  for (int i = 0; i < SLOT_COUNT; i++) {
    bool isWarn = false;
    String val = slot[i];
    if ((i == 6 || i == 7 || i == 8) && val != "0" && val != "--") isWarn = true;
    if (i == 19 && val != "--" && val.toInt() < 50) isWarn = true;

    html += "<div class=\"card" + String(isWarn ? " warn" : "") + "\">";
    html += "<div class=\"no\">#" + String(i + 1) + "</div>";
    html += "<div class=\"label\">" + String(label[i]) + "</div>";
    html += "<div class=\"value\">" + val + "</div>";
    html += "</div>\n";
  }

  html += "</div>\n"
  "<div class=\"footer\">30s auto-refresh | IoT Hub on ESP8266</div>\n"
  "<script>setTimeout(()=>location.reload(),30000);</script>\n"
  "</body>\n</html>";

  server.send(200, "text/html; charset=utf-8", html);
}

// =============================================
// GET /status → JSON
// =============================================
void handleStatus() {
  String json = "{";
  for (int i = 0; i < SLOT_COUNT; i++) {
    if (i > 0) json += ",";
    json += "\"no" + String(i + 1) + "\":\"" + slot[i] + "\"";
  }
  json += ",\"warning\":" + String(warningActive ? "true" : "false") + "}";
  server.send(200, "application/json", json);
}

// =============================================
// 404
// =============================================
void handleNotFound() {
  server.send(404, "text/plain",
    "404 - Use /set?no1=value  or  /  or  /status");
}

// =============================================
// OLED auto-scroll + invert on warning
// =============================================
void updateOLED() {
  unsigned long now = millis();
  if (now - lastScroll < SCROLL_INTERVAL) return;
  lastScroll = now;

  display.clearDisplay();

  // Top bar
  display.setTextSize(1);
  display.fillRect(0, 0, SCREEN_WIDTH, 10, WHITE);
  display.setTextColor(BLACK);
  display.setCursor(2, 1);
  display.printf("IoT Hub [%d/%d]",
    scrollOffset / ITEMS_PER_PAGE + 1,
    (SLOT_COUNT + ITEMS_PER_PAGE - 1) / ITEMS_PER_PAGE);
  display.setTextColor(WHITE);

  // Body: 4 at a time
  for (int i = 0; i < ITEMS_PER_PAGE; i++) {
    int idx = scrollOffset + i;
    if (idx >= SLOT_COUNT) break;

    int y = 14 + i * 12;
    String val = slot[idx];

    bool isWarn = false;
    if ((idx == 6 || idx == 7 || idx == 8) && val != "0" && val != "--")
      isWarn = true;
    if (idx == 19 && val != "--" && val.toInt() < 50)
      isWarn = true;

    display.setCursor(0, y);
    display.print(label[idx]);
    display.print(":");

    if (isWarn) {
      display.fillRect(80, y, SCREEN_WIDTH - 80, 8, WHITE);
      display.setTextColor(BLACK);
    }
    display.setCursor(82, y);
    display.print(val);
    display.setTextColor(WHITE);
  }

  display.display();

  scrollOffset += ITEMS_PER_PAGE;
  if (scrollOffset >= SLOT_COUNT) scrollOffset = 0;
}

Upload

1. Connect the NodeMCU to your PC with the USB cable. 2. In the Arduino IDE, choose Tools → Board → ESP8266 Boards → NodeMCU 1.0. 3. Select the port and click the Upload (→) button. 4. Once the upload finishes, open Tools → Serial Monitor. Set the baud rate to 115200.

What to check

If the serial monitor prints something like this, you're good:

Connecting to WiFi...
STA IP: 192.168.0.50
AP  IP: 192.168.1.1
HTTP server started

The OLED also shows the two IPs, alternating.

  • STA IP (something like 192.168.0.50) → the address assigned by your home router. Visit this address from a PC/phone on the same network and the HTML dashboard comes up.
  • AP IP (192.168.1.1) → the address of IoT_Hub_Net, the Wi-Fi the brain creates itself. From now on, the sensor nodes send their data here.

Tests (be sure to run all of them)

1) Check the HTML dashboard: From a PC browser, go to http://<STA IP> (e.g. http://192.168.0.50). If a grid of 20 cards appears on a dark background, all showing --, it's working.

2) Push a value in: Type this into the browser address bar:

http://<STA IP>/set?no1=24.5&no2=58

If it responds with {"updated":2}, it worked. Refresh the dashboard and cards #1 and #2 will have changed to 24.5 and 58.

3) Alert test:

http://<STA IP>/set?no7=1

Card #7 (Gas Leak) gets highlighted red, and the buzzer beeps every 0.5 seconds. On the OLED, the #7 label shows inverted too.

http://<STA IP>/set?no7=0

Set it back to 0 and the buzzer switches off and the card returns to its original color.

4) Food-bowl alert test:

http://<STA IP>/set?no20=30

Put 30g into #20 (Pet Bowl) — it's under 50g, so the alert fires.

http://<STA IP>/set?no20=120

Raise it to 120g and the alert clears.

5) Check the Wi-Fi AP: In your phone's Wi-Fi settings, check that IoT_Hub_Net shows up. You should be able to connect with the password iothub1234. Connecting is all that matters — there's no internet (that's by design).

Checkpoints when it doesn't work

Don't just re-upload blindly — check the following in order. 90% of problems get solved right here.

The upload itself won't go through

SymptomCauseFix
Compilation error: Adafruit_GFX.h not foundLibrary not installedIn Tools → Manage Libraries, install Adafruit GFX and Adafruit SSD1306
Compilation error: ESP8266WiFi.h not foundESP8266 board manager not installedAdd https://arduino.esp8266.com/stable/package_esp8266com_index.json to the Arduino preferences, then install ESP8266 from the boards manager
esptool.FatalError: Failed to connectWrong port / driver issue① Check the USB cable is a data cable (charge-only cables won't work). ② In Tools → Port, select the port where the NodeMCU shows up. ③ Still stuck? Reboot the PC and try again
Stalls at Upload error: 0%Failed to enter GPIO0 flash modeThe NodeMCU normally enters flash mode on its own, but if it doesn't, hold down the board's FLASH button just before uploading and release it once the upload starts

It uploaded, but it's behaving strangely

SymptomCauseFix
OLED doesn't light up at allWiring errorCheck VCC→3.3V, GND→GND. Check SDA/SCL are properly seated on D2/D1. Plugging into 5V can fry the OLED, so use 3.3V for sure
OLED lights up but shows no textI2C address errorTry changing OLED_ADDR 0x3C on line 83 to 0x3D. Some SSD1306s use a different address
OLED shows "IoT Hub Ready" but STA IP: 0.0.0.0Wi-Fi connection failedSSID/password typo. Check case and special characters especially. Won't work if the router only supports 5GHz (ESP8266 is 2.4GHz only). Make sure 2.4GHz is enabled in the router settings
Got a STA IP but can't reach /Firewall / different subnetCheck the PC is on the same router. Check the PC's IP is in the 192.168.0.xxx range (the first three groups must match). Won't work if you're on a hotspot or a VPN is on
Buzzer doesn't soundReversed polarity or defectiveCheck the buzzer's + is on D7 and - is on GND. A passive buzzer instead of an active one may make no sound, so make sure it's an active buzzer
Serial monitor shows garbled charactersBaud rate mismatchCheck the baud rate at the bottom-right of the serial monitor is 115200. If it's set to 9600, it all comes out garbled
Everything's fine but the HTML dashboard is very slowESP8266 CPU overloadNormal. The ESP8266 isn't a server built to handle dozens of requests a second. The dashboard auto-refreshes every 30 seconds, so it's fine. /set requests are handled within tens of ms

What we've done so far

  • We put an OLED and a buzzer on a single ESP8266.
  • A web server is up, and you can write data into the 20 slots with /set?no1=value.
  • The HTML dashboard comes up, and the OLED auto-scrolls four at a time.
  • The buzzer reacts to gas, fire, intrusion, and food-bowl alerts.
  • The brain's own Wi-Fi IoT_Hub_Net, for the sensor nodes to join, is ready too.

It's still full of --, but the brain is done. Starting tomorrow, we attach the sensors one at a time.

The next steps fill the 20 memory slots of this brain server with distributed edge nodes. Each sensor node joins IoT_Hub_Net and updates a central NodeMCU server slot through the /set?noX=value endpoint.

What's next

Next time we add the living-room temperature/humidity sensor. A DHT22 + Wemos D1 Mini combo. We'll cover the setup where a temperature reading taken in a far corner of the living room rides IoT_Hub_Net back to the brain.

What you'll need: Wemos D1 Mini (~$1.40), DHT22 (~$1.40), a Micro USB cable, a charger