Home IoT Hub Series · 02
[2편] 두뇌 만들기 - ESP8266 + OLED + 부저로 IoT 허브 본체 조립
![[2편] 두뇌 만들기 - ESP8266 + OLED + 부저로 IoT 허브 본체 조립 대표 이미지](/iot-lab/assets/home-iot-hub-brain-esp8266-hero.png)
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원 |
핀 연결
브레드보드 없이 점퍼 와이어로 직접 연결할 수 있습니다. NodeMCU 핀 번호는 보드 앞면에 D2, D5처럼 표시되어 있으므로 표의 핀 번호에 맞춰 연결합니다.
| 센서/모듈 | 보드 연결 |
|---|---|
| VCC | 3.3V |
| GND | GND |
| SDA | D2 (GPIO4) |
| SCL | D1 (GPIO5) |
| 부저 (+) | D7 (GPIO13) |
| 부저 (-) | GND |
D2, D1로 표시됩니다. D 라벨과 GPIO 번호가 다르므로 위 표를 기준으로 연결하는 것이 안전합니다.선행 작업: 라이브러리 설치
Arduino IDE를 열고 툴 → 라이브러리 관리에서 다음 두 라이브러리를 설치합니다.
| 라이브러리 | 검색어 |
|---|---|
| Adafruit SSD1306 | Adafruit SSD1306 |
| Adafruit GFX Library | Adafruit GFX |
ESP8266 보드 매니저가 설치되어 있어야 합니다. 설치되어 있지 않다면 Arduino → Preferences → Additional Boards Manager URLs에 아래 주소를 넣고, 툴 → 보드 → 보드 매니저에서 ESP8266을 설치합니다.
https://arduino.esp8266.com/stable/package_esp8266com_index.json
🚨 코드 넣기 전에 반드시!!
아래 코드를 복사해서 Arduino IDE에 붙여넣은 다음, 두 군데를 반드시 수정합니다. 수정 없이 업로드하면 Wi-Fi에 연결되지 않습니다.
const char* ssid = "YOUR_SSID"; // ← 여기에 사용 중인 공유기 이름 const char* password = "YOUR_PASSWORD"; // ← 여기에 사용 중인 공유기 비밀번호
>
const char* ssid = "MyHome_2G";>
const char* password = "abcd1234";>
전체 코드
SSID와 비밀번호를 바꿨으면, 나머지 코드는 그대로 복사해 붙여넣습니다.
#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-FiIoT_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 GFX와 Adafruit SSD1306 설치 |
Compilation error: ESP8266WiFi.h not found | ESP8266 보드 매니저 미설치 | 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 0x3C를 0x3D로 바꿔보자. 일부 SSD1306은 주소가 다르다 |
OLED에 "IoT Hub Ready"는 떴는데 STA IP: 0.0.0.0 | Wi-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을 통해 두뇌로 보내는 구조를 다룹니다.
이 글은 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편] 집 안 상태가 한 화면에 보이기 시작했다 · 슬롯 전체