Home IoT Hub Series · 02

[2편] 두뇌 만들기 - ESP8266 + OLED + 부저로 IoT 허브 본체 조립

[2편] 두뇌 만들기 - ESP8266 + OLED + 부저로 IoT 허브 본체 조립 대표 이미지

1편에서 전체 구조를 정리했습니다. 이번 편에서는 ESP8266 두뇌 본체를 실제로 구성합니다. 납땜 없이 배선하고 코드를 업로드하는 흐름으로 진행합니다.

준비물과 가격

부품수량개당 가격비고
NodeMCU v3 (ESP8266)1개2,500원두뇌 본체로 사용합니다. D1 Mini도 사용할 수 있습니다.
SSD1306 128×64 OLED (I2C)1개1,800원0.96인치 흰색
피에조 부저1개300원3~5V 능동형
점퍼 와이어 (암-암)6개500원
브레드보드1개1,500원없으면 점퍼선으로 직접 연결할 수 있습니다.
Micro USB 케이블1개1,000원전원 + 코드 업로드
합계7,600원

핀 연결

[2편] 두뇌 만들기 - ESP8266 + OLED + 부저로 IoT 허브 본체 조립 배선도

브레드보드 없이 점퍼 와이어로 직접 연결할 수 있습니다. NodeMCU 핀 번호는 보드 앞면에 D2, D5처럼 표시되어 있으므로 표의 핀 번호에 맞춰 연결합니다.

센서/모듈보드 연결
VCC3.3V
GNDGND
SDAD2 (GPIO4)
SCLD1 (GPIO5)
부저 (+)D7 (GPIO13)
부저 (-)GND
GPIO4와 GPIO5는 NodeMCU 보드에서 각각 D2, D1로 표시됩니다. D 라벨과 GPIO 번호가 다르므로 위 표를 기준으로 연결하는 것이 안전합니다.

선행 작업: 라이브러리 설치

Arduino IDE를 열고 툴 → 라이브러리 관리에서 다음 두 라이브러리를 설치합니다.

라이브러리검색어
Adafruit SSD1306Adafruit SSD1306
Adafruit GFX LibraryAdafruit GFX

ESP8266 보드 매니저가 설치되어 있어야 합니다. 설치되어 있지 않다면 Arduino → Preferences → Additional Boards Manager URLs에 아래 주소를 넣고, 툴 → 보드 → 보드 매니저에서 ESP8266을 설치합니다.

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

🚨 코드 넣기 전에 반드시!!

아래 코드를 복사해서 Arduino IDE에 붙여넣은 다음, 두 군데를 반드시 수정합니다. 수정 없이 업로드하면 Wi-Fi에 연결되지 않습니다.

[2편] 두뇌 만들기 - ESP8266 + OLED + 부저로 IoT 허브 본체 조립 - 전체 코드
const char* ssid     = "YOUR_SSID";     // ← 여기에 사용 중인 공유기 이름
const char* password = "YOUR_PASSWORD"; // ← 여기에 사용 중인 공유기 비밀번호
YOUR_SSIDYOUR_PASSWORD라는 글자를 지우고, 큰따옴표는 그대로 둔 채 사용 중인 Wi-Fi 정보를 넣습니다.

>

예: const char* ssid = "MyHome_2G";

>

예: const char* password = "abcd1234";

>

ESP8266은 2.4GHz Wi-Fi만 잡는다. 5GHz 공유기면 2.4GHz도 함께 켜져 있는지 확인.

전체 코드

SSID와 비밀번호를 바꿨으면, 나머지 코드는 그대로 복사해 붙여넣습니다.

[2편] 두뇌 만들기 - ESP8266 + OLED + 부저로 IoT 허브 본체 조립 - 전체 코드
#include <ESP8266WiFi.h>
#include <ESP8266WebServer.h>
#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>

// =============================================
// 1. Wi-Fi 설정 (직접 입력!)
// =============================================
const char* ssid     = "YOUR_SSID";     // 집 공유기 이름
const char* password = "YOUR_PASSWORD"; // 집 공유기 비밀번호

// 센서 노드용 자체 AP 설정
const char* ap_ssid     = "IoT_Hub_Net";
const char* ap_password = "iothub1234"; // 센서 노드 접속용 비밀번호

// =============================================
// 2. 하드웨어 핀
// =============================================
#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개 메모리 슬롯
// =============================================
#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. 타이머 변수
// =============================================
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);

  // 부저 초기화
  pinMode(BUZZER_PIN, OUTPUT);
  digitalWrite(BUZZER_PIN, LOW);

  // OLED 초기화
  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);

  // 부팅 로고
  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: STA 모드로 집 공유기에 접속 ──
  WiFi.mode(WIFI_AP_STA); // AP + STA 동시 모드
  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; // 줄바꿈 흉내
  }
  Serial.println();
  Serial.print("STA IP: ");
  Serial.println(WiFi.localIP());

  // ── Soft AP: 센서 노드용 자체 WiFi 생성 ──
  WiFi.softAP(ap_ssid, ap_password);
  Serial.print("AP  IP: ");
  Serial.println(WiFi.softAPIP());

  // ── 웹 서버 라우팅 ──
  server.on("/set", handleSet);
  server.on("/", handleRoot);
  server.on("/status", handleStatus);
  server.onNotFound(handleNotFound);
  server.begin();
  Serial.println("HTTP server started");

  // ── OLED에 IP 표시 ──
  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();
}

// =============================================
// 경고 상태 확인
// =============================================
bool checkWarning() {
  for (int i = 0; i < SLOT_COUNT; i++) {
    // 슬롯 7(가스누출), 8(화재), 9(정문열림) → 0이 아니면 경고
    if ((i == 6 || i == 7 || i == 8)
        && slot[i] != "0" && slot[i] != "--") {
      return true;
    }
    // 슬롯 20(반려견밥그릇) → 50g 미만이면 경고
    if (i == 19 && slot[i] != "--" && slot[i].toInt() < 50) {
      return true;
    }
  }
  return false;
}

// =============================================
// 부저 제어 (경고 시 500ms 간격 비프)
// =============================================
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=값&no2=값 ...
// =============================================
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 대시보드
// =============================================
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 자동 스크롤 + 경고 시 반전
// =============================================
void updateOLED() {
  unsigned long now = millis();
  if (now - lastScroll < SCROLL_INTERVAL) return;
  lastScroll = now;

  display.clearDisplay();

  // 상단 바
  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);

  // 본문: 4개씩
  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;
}

업로드

1. USB 케이블로 NodeMCU를 PC에 연결합니다. 2. Arduino IDE에서 툴 → 보드 → ESP8266 Boards → NodeMCU 1.0을 선택합니다. 3. 포트를 선택하고 업로드(→) 버튼을 누릅니다. 4. 업로드가 끝나면 툴 → 시리얼 모니터를 열고 보드레이트를 115200으로 맞춥니다.

확인할 것

시리얼 모니터에 이런 식으로 출력되면 성공이다:

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

OLED에도 두 개의 IP가 번갈아 표시됩니다.

  • STA IP (192.168.0.50 같은) → 집 공유기가 할당한 주소. 같은 네트워크의 PC/스마트폰에서 이 주소로 접속하면 HTML 대시보드가 표시됩니다.
  • AP IP (192.168.1.1) → 두뇌가 직접 만든 Wi-Fi IoT_Hub_Net의 주소입니다. 앞으로 센서 노드들은 이 주소로 데이터를 보냅니다.

테스트 (반드시 전부 해볼 것)

1) HTML 대시보드 확인: PC 브라우저에서 http://<STA IP> (예: http://192.168.0.50) 접속. 어두운 배경에 20개 카드가 그리드로 뜨고 전부 --로 표시되면 정상.

2) 값 넣어보기: 브라우저 주소창에 입력:

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

입력 후 {"updated":2} 라고 응답이 뜨면 성공. 대시보드를 새로고침하면 1번, 2번 카드가 24.5, 58로 바뀌어 있다.

3) 경고 테스트:

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

7번(Gas Leak) 카드가 빨간색으로 강조되고, 부저가 0.5초 간격으로 삐-삐- 울립니다. OLED에서도 7번 라벨이 반전 표시됩니다.

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

0으로 되돌리면 부저가 꺼지고 카드도 원래 색으로 돌아온다.

4) 밥그릇 경고 테스트:

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

20번(Pet Bowl)에 30g을 넣으면 50g 미만이므로 경고가 작동합니다.

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

120g으로 올리면 경고 해제.

5) Wi-Fi AP 확인: 스마트폰 Wi-Fi 설정에서 IoT_Hub_Net이 보이는지 확인합니다. 비밀번호 iothub1234로 접속할 수 있어야 합니다. 접속만 확인하면 되며, 인터넷이 되지 않는 것은 의도된 동작입니다.

안 될 때 체크포인트

무작정 다시 업로드하지 말고 아래 순서대로 확인합니다. 대부분의 기본 문제는 이 단계에서 해결됩니다.

업로드 자체가 되지 않을 때

증상원인해결
Compilation error: Adafruit_GFX.h not found라이브러리 미설치툴 → 라이브러리 관리에서 Adafruit GFXAdafruit SSD1306 설치
Compilation error: ESP8266WiFi.h not foundESP8266 보드 매니저 미설치Arduino 설정에 https://arduino.esp8266.com/stable/package_esp8266com_index.json 추가 후 보드 매니저에서 ESP8266 설치
esptool.FatalError: Failed to connect잘못된 포트 / 드라이버 문제① USB 케이블이 데이터 케이블인지 확인 (충전 전용 케이블은 안 됨). ② 툴 → 포트에서 NodeMCU가 인식된 포트 선택. ③ 그래도 안 되면 PC 재부팅 후 재시도
Upload error: 0%에서 멈춤GPIO0 플래시 모드 진입 실패NodeMCU는 자동으로 플래시 모드에 들어가지만, 안 되면 업로드 직전에 보드의 FLASH 버튼을 누르고 있다가 업로드 시작되면 뗀다

업로드는 됐는데 동작이 이상하다

증상원인해결
OLED가 전혀 안 켜짐배선 오류VCC→3.3V, GND→GND 맞는지 확인. SDA/SCL이 D2/D1에 제대로 꽂혔는지 확인. 5V에 꽂으면 OLED가 탈 수 있으니 반드시 3.3V
OLED 켜졌는데 아무 글자도 안 보임I2C 주소 오류코드 83행 OLED_ADDR 0x3C0x3D로 바꿔보자. 일부 SSD1306은 주소가 다르다
OLED에 "IoT Hub Ready"는 떴는데 STA IP: 0.0.0.0Wi-Fi 연결 실패SSID/비밀번호 오타. 특히 대소문자, 특수문자 확인. 공유기가 5GHz만 지원하면 안 됨 (ESP8266은 2.4GHz 전용). 공유기 설정에서 2.4GHz 활성화 확인
STA IP는 나왔는데 / 접속이 안 됨방화벽 / 다른 서브넷PC가 같은 공유기에 연결되어 있는지 확인. PC의 IP가 192.168.0.xxx 대역인지 확인 (앞 세 자리가 같아야 함). 핫스팟으로 물려 있거나 VPN 켜져 있으면 안 됨
부저가 안 울림극성 반대 or 불량부저의 +D7에, -GND에 꽂혔는지 확인. 능동 부저가 아니라 수동 부저면 소리가 안 날 수 있으니 능동(active) 부저인지 확인
시리얼 모니터가 깨진 글자로 나옴보드레이트 불일치시리얼 모니터 우측 하단 보드레이트가 115200인지 확인. 9600으로 되어 있으면 죄다 깨진다
전부 정상인데 HTML 대시보드가 너무 느림ESP8266 CPU 과부하정상. ESP8266은 초당 수십 요청을 견디는 서버가 아니다. 대시보드는 30초 자동 갱신이니 괜찮다. /set 요청은 수십 ms 안에 처리된다

지금까지 구성한 흐름

  • ESP8266 하나에 OLED와 부저를 연결했습니다.
  • 웹 서버가 동작하며, /set?no1=값으로 20개 슬롯에 데이터를 입력할 수 있습니다.
  • HTML 대시보드가 표시되고, OLED는 4개씩 자동 스크롤됩니다.
  • 가스·화재·침입·밥그릇 경고에 부저가 반응합니다.
  • 센서 노드가 연결될 자체 Wi-Fi IoT_Hub_Net도 준비되었습니다.

아직 -- 값이 많지만, 두뇌 역할을 하는 본체는 완성된 상태입니다. 다음 단계부터 여기에 센서를 하나씩 붙입니다.

이제 이 두뇌 서버의 20개 메모리 슬롯을 실시간으로 채워 줄 분산 엣지 노드 제작으로 이어집니다. 각 센서 노드는 IoT_Hub_Net에 접속해 /set?noX=값 형식으로 중앙 NodeMCU 서버의 슬롯을 갱신합니다.

다음 글 예고

다음 편에서는 거실 온도·습도 센서를 붙입니다. DHT22와 Wemos D1 Mini 조합으로, 두뇌와 떨어진 거실 구석의 온습도 값을 IoT_Hub_Net을 통해 두뇌로 보내는 구조를 다룹니다.

준비물: Wemos D1 Mini (2,000원), DHT22 (2,000원), Micro USB 케이블, 충전기
우리집 IoT HUB 20슬롯 시스템 맵

이 글은 2편의 중앙 두뇌 서버와 3~19편의 분산 엣지 노드가 함께 구성하는 20채널 홈 IoT 시스템의 일부입니다. 필요한 편으로 이동해 슬롯과 센서 흐름을 함께 확인합니다.

[1편] 우리집 IoT 허브 전체 그림[2편] 중앙 두뇌 서버 만들기 · 슬롯 1~20[3편] 집 안 온습도, 감으로 보지 말고 숫자로 보자 · 슬롯 1·2[4편] 문 닫았나 계속 신경 쓰일 때 · 슬롯 9[5편] 냉장고가 제대로 돌고 있는지 눈으로 확인하자 · 슬롯 12[6편] 창문 열어도 될까? · 슬롯 3[7편] 욕실 곰팡이 생기기 전에 습도부터 잡자 · 슬롯 19[8편] 보일러가 실제로 도는지 배관 온도로 확인하자 · 슬롯 5·17[9편] 외출했는데 가스레인지가 켜져 있지 않을까? · 슬롯 6[10편] 가스 냄새가 나기 전에 먼저 경고받자 · 슬롯 7[11편] 집을 비웠을 때 불꽃 징후를 먼저 잡자 · 슬롯 8[12편] 머리가 멍한 이유가 공기 때문인지 확인하자 · 슬롯 13·14[13편] 전기세 무지 나오네, 이번 기회에 눈으로 보자 · 슬롯 15[14편] 물 새는지, 얼마나 쓰는지 숫자로 보자 · 슬롯 16[15편] 비 오는 줄 모르고 창문 열어두지 않게 · 슬롯 18[16편] 불 껐나, 문 잠갔나 한 번에 확인하자 · 슬롯 11·10[17편] 리모컨 없이 에어컨을 IoT 허브에서 제어하자 · 슬롯 4[18편] 강아지 밥그릇이 비었는지 눈으로 보자 · 슬롯 20[19편] 사람 있을 때만 시원하게, 없을 땐 아끼자 · 슬롯 1·4[20편] 집 안 상태가 한 화면에 보이기 시작했다 · 슬롯 전체