Home IoT Hub Series · 17
[17편] 리모컨 없이 에어컨을 IoT 허브에서 제어하자 - IR 송신 노드
![[17편] 리모컨 없이 에어컨을 IoT 허브에서 제어하자 - IR 송신 노드 대표 이미지](/iot-lab/assets/air-conditioner-ir-control-hero.png)
이 글은 2편: 두뇌 만들기에서 구축한 중앙 NodeMCU 서버에 연결되는 우리집 IoT 허브 구성 문서입니다. IR 송수신 노드가 에어컨 제어 상태를 중앙 허브에 반영하고 수동 리모컨 조작과 자동 제어 사이의 간격을 줄입니다. 수집된 데이터는 로컬 무선망
IoT_Hub_Net을 통해 중앙 서버의 4번 에어컨 상태 슬롯(/set?no4=값)을 갱신하며, 20채널 스마트홈 통합 컨트롤러의 판단 근거로 사용됩니다.이번 편에서는 모니터링을 넘어 제어로 확장합니다. 적외선 수신기(VS1838B)로 에어컨 리모컨 신호를 학습하고, IR LED로 다시 송신합니다. 두뇌에 /set?no4=ON을 요청하면 에어컨이 켜지고, /set?no4=OFF를 요청하면 꺼지는 구조입니다. 19편 자동 온도 제어의 마지막 구성 요소입니다.
준비물과 가격
| 부품 | 수량 | 개당 가격 |
|---|---|---|
| Wemos D1 Mini | 1개 | 2,000원 |
| VS1838B IR 수신기 | 1개 | 300원 |
| IR LED 940nm | 1개 | 200원 |
| 2N2222 NPN 트랜지스터 | 1개 | 100원 |
| 100Ω 저항 | 1개 | 100원 |
| 10kΩ 저항 | 1개 | 100원 |
| 점퍼 와이어 6개 | — | — |
| 합계 | 2,800원 |
핀 연결
| 센서/모듈 | 보드 연결 |
|---|---|
| VCC | 3.3V |
| GND | GND |
| OUT | D1 (GPIO5) |
| D2 (GPIO4) | 2N2222 콜렉터 |
| 2N2222 에미터 | GND |
| D2 | 2N2222 베이스 |
선행 작업
IRremoteESP8266 라이브러리 설치. Arduino IDE 툴 → 라이브러리 관리에서 검색.
🚨 코드 넣기 전에 반드시!!
이 편은 다른 편과 다르게 리모컨 학습이 반드시 필요합니다. 업로드 후 시리얼 모니터를 켜고, 10초 안에 에어컨 리모컨의 ON 버튼과 OFF 버튼을 각각 센서에 송신해야 합니다. 캡처된 코드를 시리얼 모니터에서 확인하고, 그 값을 AC_ON_CODE와 AC_OFF_CODE에 넣은 뒤 다시 업로드해야 합니다. 한 번으로 끝나는 업로드가 아니라 학습용 업로드와 정식 동작용 업로드를 나눠 진행하는 구조입니다.
단계: 1. 코드 업로드 → 시리얼 모니터에서 ON 코드, OFF 코드 캡처 2. AC_ON_CODE와 AC_OFF_CODE에 캡처한 값 복사 3. 다시 업로드 → 이제 정식 동작
전체 코드
#include <ESP8266WiFi.h> #include <ESP8266HTTPClient.h> #include <IRremoteESP8266.h> #include <IRsend.h> #include <IRrecv.h> #include <IRutils.h> const char* ssid = "IoT_Hub_Net"; const char* password = "iothub1234"; const char* hubURL = "http://192.168.1.1"; #define IR_RECV 5 // D1 (GPIO5) - VS1838B 수신 #define IR_SEND 4 // D2 (GPIO4) - IR LED 송신 #define SLOT_NO 4 // 두뇌 4번 슬롯 = AC // ============================================= // 리모컨 학습 후 이 값을 바꿔야 한다! // ============================================= // 첫 업로드 후 시리얼 모니터에 찍힌 코드를 // 아래에 복사하고 다시 업로드합니다. unsigned long long AC_ON_CODE = 0x0; // ← 여기에 ON 코드 unsigned long long AC_OFF_CODE = 0x0; // ← 여기에 OFF 코드 IRrecv irrecv(IR_RECV); IRsend irsend(IR_SEND); decode_results results; String lastState = "--"; bool codesLearned = false; // ============================================= // setup // ============================================= void setup() { Serial.begin(115200); irsend.begin(); irrecv.enableIRIn(); WiFi.mode(WIFI_STA); WiFi.begin(ssid, password); Serial.print("Connecting to IoT_Hub_Net"); while (WiFi.status() != WL_CONNECTED) { delay(500); Serial.print("."); } Serial.println("\nConnected! IP: " + WiFi.localIP().toString()); // 이미 코드가 입력되어 있으면 학습 스킵 if (AC_ON_CODE != 0x0 && AC_OFF_CODE != 0x0) { codesLearned = true; irrecv.disableIRIn(); Serial.println("=== CODES ALREADY SET, SKIPPING LEARNING ==="); Serial.print("AC_ON_CODE: 0x"); Serial.println((unsigned long)AC_ON_CODE, HEX); Serial.print("AC_OFF_CODE: 0x"); Serial.println((unsigned long)AC_OFF_CODE, HEX); Serial.println("Ready. Waiting for /set?no4=ON or OFF..."); return; } // ── IR 학습 모드 ── Serial.println(); Serial.println("=========================================="); Serial.println(" IR LEARNING MODE"); Serial.println("=========================================="); Serial.println("1. Point AC remote at VS1838B sensor."); Serial.println("2. Press ON button on the remote."); Serial.println("3. Press OFF button on the remote."); Serial.println("4. Copy the codes to AC_ON_CODE / AC_OFF_CODE"); Serial.println(" in this sketch and re-upload."); Serial.println("=========================================="); Serial.println(); bool gotOn = false; bool gotOff = false; unsigned long startTime = millis(); // 최대 30초 동안 ON/OFF 신호 대기 while (!gotOn || !gotOff) { if (millis() - startTime > 30000) { Serial.println("TIMEOUT: Learning mode ended."); break; } if (irrecv.decode(&results)) { unsigned long long code = results.value; uint16_t protocol = results.decode_type; if (!gotOn) { AC_ON_CODE = code; gotOn = true; Serial.print("AC_ON_CODE: 0x"); Serial.println((unsigned long)code, HEX); Serial.println(" Now press OFF button..."); } else if (!gotOff) { // OFF 코드가 ON과 같으면 (토글형 리모컨) 경고 if (code == AC_ON_CODE) { Serial.println(" WARNING: OFF code same as ON code!"); Serial.println(" Your remote is toggle-type. Use the captured code for both."); } AC_OFF_CODE = code; gotOff = true; Serial.print("AC_OFF_CODE: 0x"); Serial.println((unsigned long)code, HEX); } irrecv.resume(); } delay(100); } irrecv.disableIRIn(); codesLearned = (gotOn && gotOff); if (codesLearned) { Serial.println(); Serial.println("=== LEARNING COMPLETE ==="); Serial.println("Copy these two lines into the sketch and re-upload:"); Serial.println(); Serial.print("unsigned long long AC_ON_CODE = 0x"); Serial.println((unsigned long)AC_ON_CODE, HEX); Serial.print("unsigned long long AC_OFF_CODE = 0x"); Serial.println((unsigned long)AC_OFF_CODE, HEX); } else { Serial.println("=== LEARNING FAILED ==="); Serial.println("Restart the device and try again."); } } // ============================================= // loop // ============================================= void loop() { if (!codesLearned) { Serial.println("Waiting for IR codes to be learned..."); delay(5000); return; } // ── 두뇌의 /status 를 읽어서 no4 값 확인 ── WiFiClient client; HTTPClient http; String url = String(hubURL) + "/status"; http.begin(client, url); int code = http.GET(); if (code == 200) { String resp = http.getString(); // no4 값 추출 (간단한 문자열 파싱) int pos = resp.indexOf("\"no4\":\""); if (pos > 0) { int start = pos + 7; int end = resp.indexOf("\"", start); String val = resp.substring(start, end); // 상태가 바뀌었을 때만 IR 송출 if (val != lastState) { Serial.print("State changed: "); Serial.print(lastState); Serial.print(" -> "); Serial.println(val); if (val == "ON") { Serial.print("Sending IR: AC ON (0x"); Serial.print((unsigned long)AC_ON_CODE, HEX); Serial.println(")"); irsend.sendNEC(AC_ON_CODE, 32); delay(100); } else if (val == "OFF") { Serial.print("Sending IR: AC OFF (0x"); Serial.print((unsigned long)AC_OFF_CODE, HEX); Serial.println(")"); irsend.sendNEC(AC_OFF_CODE, 32); delay(100); } // IR 송출 후 상태 업데이트 lastState = val; // 현재 상태를 두뇌에 피드백 (no4 값을 재확인) WiFiClient c2; HTTPClient h2; String fbUrl = String(hubURL) + "/set?no4=" + val; h2.begin(c2, fbUrl); h2.GET(); h2.end(); Serial.println("Feedback -> " + fbUrl); } } } else { Serial.print("HTTP "); Serial.print(code); Serial.println(" - retrying..."); } http.end(); delay(3000); }
sendNEC)을 기준으로 했습니다. 삼성·LG 에어컨은 NEC가 아닌 독자 프로토콜을 쓰는 경우가 많습니다. IRremoteESP8266 라이브러리에는 ir_Daikin.cpp, ir_LG.cpp, ir_Samsung.cpp 같은 전용 클래스가 내장되어 있으므로, 학습 모드에서 프로토콜 타입도 함께 확인하고(results.decode_type) 해당 프로토콜에 맞는 send 함수로 바꾸면 됩니다.업로드 및 확인
1차 업로드 — 코드 학습
1. AC_ON_CODE와 AC_OFF_CODE를 0x0으로 둔 채 업로드. 2. 시리얼 모니터 115200bps. 3. "Press ON button" 메시지가 뜨면 에어컨 리모컨의 ON 버튼을 VS1838B에 가까이 대고 누른다. 4. "Press OFF button" 메시지가 뜨면 OFF 버튼을 누른다. 5. 시리얼 모니터에 찍힌 16진수 코드를 복사한다:
AC_ON_CODE: 0x880804F AC_OFF_CODE: 0x880846B
2차 업로드 — 정식 동작
1. 복사한 코드를 스케치 상단에 붙여넣는다:
unsigned long long AC_ON_CODE = 0x880804F; // 복사한 값 unsigned long long AC_OFF_CODE = 0x880846B; // 복사한 값
2. 다시 업로드. 3. 시리얼 모니터에 CODES ALREADY SET, SKIPPING LEARNING이 뜨면 OK. 4. 두뇌에 요청을 보내본다:
http://192.168.1.1/set?no4=ON → 에어컨 켜짐 http://192.168.1.1/set?no4=OFF → 에어컨 꺼짐
5. 시리얼 모니터에 Sending IR: AC ON / Sending IR: AC OFF가 찍히고, 실제 에어컨이 반응하는지 확인.
시리얼 모니터
업로드가 끝나면 Arduino IDE의 툴 → 시리얼 모니터를 열고 보드레이트를 115200으로 맞춥니다. 다음은 정상 동작을 확인하기 위한 시리얼 모니터 출력 예시입니다. 이 로그는 실제 캡처가 아니라, IR 송신과 4번 슬롯 상태 갱신을 확인하기 위한 시리얼 모니터 출력 예시입니다.
[BOOT] Home IoT Node #17 - Air conditioner IR control [IR] protocol loaded, target=LG_COOL_24C [COMMAND] request=AC_ON [IR] send result=OK, repeat=2 [FEEDBACK] vent_temp_drop=DETECTED [HTTP] GET /set?no4=1 [HUB] 200 OK - slot no4=AC_ON
Connected또는connected가 보이면 Wi-Fi 연결 단계는 통과한 것입니다.HTTP 200또는200 OK가 보이면 두뇌 서버에 값이 전달된 것입니다.- 값이 갱신되지 않으면 먼저 URL의 슬롯 번호와 센서 배선을 함께 확인합니다.
안 될 때 체크포인트
| 증상 | 원인 | 해결 |
|---|---|---|
| 리모컨 코드가 안 잡힘 | VS1838B 거리/각도 | 리모컨을 센서 10cm 이내 정면에 대고 누릅니다. 어두운 곳에서 진행하면 인식률이 올라갑니다. |
| 코드는 잡혔는데 에어컨 반응 없음 | NEC 아닌 프로토콜 | 시리얼 모니터에 찍힌 decode_type을 확인합니다. 삼성=SAMSUNG, LG=LG, 대우=DAIKIN처럼 프로토콜에 맞는 send 함수로 교체합니다. |
| ON/OFF 둘 다 켜지기만 함 | 토글형 리모컨 | 일부 에어컨은 ON/OFF가 같은 코드를 사용합니다. 학습 시 OFF 코드가 ON과 동일하면 경고 메시지가 표시됩니다. 이 경우 두 코드를 동일하게 넣고, 에어컨이 꺼져 있을 때만 ON을 보내도록 로직을 추가해야 합니다. |
| IR LED가 안 보임 | 육안 불가 | 스마트폰 카메라 앱으로 IR LED를 비추면 보라색 점멸이 보입니다. 카메라에 보이지 않으면 배선을 확인합니다. |
| HTTP 200인데 AC 반응 없음 | GPIO→IR LED 배선 | 100Ω 저항, 트랜지스터 극성, 10kΩ 베이스 저항 순서 확인 |
현장 추가 체크포인트
이 노드는 코드만 보면 짧아 보일 수 있지만, 실제 집에 붙이면 센서 위치·전원·무선망·노이즈 때문에 값이 쉽게 흔들립니다. 아래 항목은 두뇌 서버에 잘못된 판단 근거가 들어가지 않도록 현장에서 먼저 점검할 내용입니다.
| 증상 | 가능한 원인 | 현장 해결 방법 |
|---|---|---|
| IR 명령이 가끔 먹지 않음 | 적외선은 직진성이 강하고 실내 각도·반사 조건에 민감함 | IR LED를 에어컨 정면이 아니라 천장/맞은편 벽 반사 방향으로 테스트해 가장 안정적인 각도를 찾습니다. |
| 리모컨으로 직접 켜면 서버 상태가 틀림 | IR 제어는 명령을 보냈다는 사실과 실제 동작 결과가 다를 수 있음 | 송신 후 토출구 온도나 소비전력 변화 같은 피드백 센서를 함께 써 실제 ON/OFF를 보정합니다. |
| 특정 모델만 코드가 안 맞음 | 에어컨 리모컨 프로토콜이 제조사·모델마다 다름 | 기존 리모컨 신호를 먼저 수신해 raw code를 저장하고, 라이브러리의 모델별 프로토콜을 확인합니다. |
| IR LED 출력이 약함 | ESP8266 핀 직접 구동 전류가 부족함 | 트랜지스터와 저항을 써 IR LED를 별도 전류로 구동하고, GND는 공통으로 묶습니다. |
다음 글 예고
마지막 편. 반려견 밥그릇. HX711 로드셀로 사료 무게를 측정하고, 50g 이하면 두뇌가 "밥 보충 필요" 경고를 울립니다.
이 글은 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편] 집 안 상태가 한 화면에 보이기 시작했다 · 슬롯 전체