Home IoT Hub Series · 17

[17편] 리모컨 없이 에어컨을 IoT 허브에서 제어하자 - IR 송신 노드

[17편] 리모컨 없이 에어컨을 IoT 허브에서 제어하자 - IR 송신 노드 대표 이미지
시스템 아키텍처 및 데이터 파이프라인 안내
이 글은 2편: 두뇌 만들기에서 구축한 중앙 NodeMCU 서버에 연결되는 우리집 IoT 허브 구성 문서입니다. IR 송수신 노드가 에어컨 제어 상태를 중앙 허브에 반영하고 수동 리모컨 조작과 자동 제어 사이의 간격을 줄입니다. 수집된 데이터는 로컬 무선망 IoT_Hub_Net을 통해 중앙 서버의 4번 에어컨 상태 슬롯(/set?no4=값)을 갱신하며, 20채널 스마트홈 통합 컨트롤러의 판단 근거로 사용됩니다.

이번 편에서는 모니터링을 넘어 제어로 확장합니다. 적외선 수신기(VS1838B)로 에어컨 리모컨 신호를 학습하고, IR LED로 다시 송신합니다. 두뇌에 /set?no4=ON을 요청하면 에어컨이 켜지고, /set?no4=OFF를 요청하면 꺼지는 구조입니다. 19편 자동 온도 제어의 마지막 구성 요소입니다.

준비물과 가격

부품수량개당 가격
Wemos D1 Mini1개2,000원
VS1838B IR 수신기1개300원
IR LED 940nm1개200원
2N2222 NPN 트랜지스터1개100원
100Ω 저항1개100원
10kΩ 저항1개100원
점퍼 와이어 6개
합계2,800원

핀 연결

[17편] 리모컨 없이 에어컨을 IoT 허브에서 제어하자 - IR 송신 노드 배선도
센서/모듈보드 연결
VCC3.3V
GNDGND
OUTD1 (GPIO5)
D2 (GPIO4)2N2222 콜렉터
2N2222 에미터GND
D22N2222 베이스
IR LED는 육안으로 보이지 않지만 스마트폰 카메라로 보면 보라색으로 빛나는 것을 확인할 수 있습니다. 트랜지스터는 신호를 증폭해 LED를 충분히 밝게 구동하기 위해 사용합니다. 저항 없이 직결하면 LED나 GPIO가 손상될 수 있으므로 반드시 넣어야 합니다.

선행 작업

IRremoteESP8266 라이브러리 설치. Arduino IDE 툴 → 라이브러리 관리에서 검색.

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

이 편은 다른 편과 다르게 리모컨 학습이 반드시 필요합니다. 업로드 후 시리얼 모니터를 켜고, 10초 안에 에어컨 리모컨의 ON 버튼과 OFF 버튼을 각각 센서에 송신해야 합니다. 캡처된 코드를 시리얼 모니터에서 확인하고, 그 값을 AC_ON_CODEAC_OFF_CODE에 넣은 뒤 다시 업로드해야 합니다. 한 번으로 끝나는 업로드가 아니라 학습용 업로드와 정식 동작용 업로드를 나눠 진행하는 구조입니다.

단계: 1. 코드 업로드 → 시리얼 모니터에서 ON 코드, OFF 코드 캡처 2. AC_ON_CODEAC_OFF_CODE에 캡처한 값 복사 3. 다시 업로드 → 이제 정식 동작

전체 코드

[17편] 리모컨 없이 에어컨을 IoT 허브에서 제어하자 - IR 송신 노드 - 전체 코드
#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);
}
위 코드는 NEC 프로토콜(sendNEC)을 기준으로 했습니다. 삼성·LG 에어컨은 NEC가 아닌 독자 프로토콜을 쓰는 경우가 많습니다. IRremoteESP8266 라이브러리에는 ir_Daikin.cpp, ir_LG.cpp, ir_Samsung.cpp 같은 전용 클래스가 내장되어 있으므로, 학습 모드에서 프로토콜 타입도 함께 확인하고(results.decode_type) 해당 프로토콜에 맞는 send 함수로 바꾸면 됩니다.

업로드 및 확인

1차 업로드 — 코드 학습

1. AC_ON_CODEAC_OFF_CODE0x0으로 둔 채 업로드. 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. 복사한 코드를 스케치 상단에 붙여넣는다:

[17편] 리모컨 없이 에어컨을 IoT 허브에서 제어하자 - IR 송신 노드 - 전체 코드
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 이하면 두뇌가 "밥 보충 필요" 경고를 울립니다.

준비물: Wemos D1 Mini (2,000원), HX711 로드셀 (3,000원)
우리집 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편] 집 안 상태가 한 화면에 보이기 시작했다 · 슬롯 전체