Chapter 10. 외부 입력 통신 (내장 블루투스, IR 리모컨)

본 가이드북의 최종장입니다. 복잡한 통신 코드를 별도의 파일로 쪼개어 관리하는 모듈화 기법을 통해 내장 블루투스(BLE)를 제어하고, 실시간으로 리모컨 버튼을 데이터베이스(JSON)에 저장하는 스마트 자동 학습형 가전 제어 기술을 학습합니다.

리모컨 (NEC) IR DATA (32bit) Pico 2 W remote_db.json 학습 및 영구 저장
스마트폰 앱 "on" 무선 패킷 전송 Pico 2 (on_rx 함수) if msg == "on": led.value(1) 스마트폰에서 보낸 문자열에 따라 하드웨어가 즉각 반응합니다.

10.1 내장 블루투스(BLE) 원리와 라이브러리 세팅

Pico 2 W는 무선 칩셋 안에 Bluetooth Low Energy (BLE) 안테나를 기본 내장하고 있습니다. 전력을 극도로 아끼는 최신 BLE 프로토콜은 일반 Classic 블루투스처럼 스마트폰 기본 설정 창에서 잡히지 않습니다. 반드시 Serial Bluetooth Terminal 같은 전문 BLE 통신 앱을 다운받아 사용해야 합니다.

통신을 처리하는 핵심 엔진 코드가 매우 길기 때문에, 우리는 이 코드를 메인 코드와 분리하여 독립된 라이브러리 파일로 보드에 먼저 저장해 둘 것입니다.

⚠️ 블루투스 이름 중복 금지 경고!

학교 강의실이나 동아리방처럼 여러 사람이 동시에 실습할 때 블루투스 명칭이 같으면 스마트폰 앱에서 누구의 보드인지 전혀 구별할 수 없습니다!

아래 라이브러리 코드의 name="Pico2W_BLE" 파라미터 부분을 반드시 자신만의 고유한 이름(예: name="Pico2W_Sunghwan")으로 수정하여 저장해야 오작동을 피할 수 있습니다.

💾 [파일 1] 라이브러리 파일 저장 규칙

아래 코드를 Thonny 편집창에 복사한 뒤, [다른 이름으로 저장] -> [Raspberry Pi Pico]를 선택하고 파일 이름을 반드시 ble_uart.py로 저장하세요. (이 코드는 통신 엔진이므로 단독 실행해도 반응이 없는 것이 정상입니다.)

import bluetooth
import time
from micropython import const

_IRQ_CENTRAL_CONNECT = const(1)
_IRQ_CENTRAL_DISCONNECT = const(2)
_IRQ_GATTS_WRITE = const(3)

# 표준 Nordic UART 서비스(NUS) UUID 규격 정의
_UART_UUID = bluetooth.UUID("6E400001-B5A3-F393-E0A9-E50E24DCCA9E")
_UART_TX = (bluetooth.UUID("6E400003-B5A3-F393-E0A9-E50E24DCCA9E"), bluetooth.FLAG_NOTIFY,)
_UART_RX = (bluetooth.UUID("6E400002-B5A3-F393-E0A9-E50E24DCCA9E"), bluetooth.FLAG_WRITE_NO_RESPONSE,)
_UART_SERVICE = (_UART_UUID, (_UART_TX, _UART_RX),)

class PicoBLEUART:
    # ⬇ 바로 아래의 name="Pico2W_BLE" 값을 고유한 이름으로 꼭 변경하세요!
    def __init__(self, name="Pico2W_BLE"):
        self._ble = bluetooth.BLE()
        self._ble.active(True)
        self._ble.irq(self._irq)
        ((self._tx_handle, self._rx_handle),) = self._ble.gatts_register_services((_UART_SERVICE,))
        self._connections = set()
        self._advertise(name)
        print(f"[{name}] 블루투스 엔진 가동... 페어링을 대기합니다.")

    def _irq(self, event, data):
        if event == _IRQ_CENTRAL_CONNECT:
            conn_handle, _, _ = data
            self._connections.add(conn_handle)
            print("\n⚡ 스마트폰 연결 성공!")
        elif event == _IRQ_CENTRAL_DISCONNECT:
            conn_handle, _, _ = data
            self._connections.remove(conn_handle)
            print("\n❌ 연결이 해제되었습니다. 다시 대기합니다.")
            self._advertise()
        elif event == _IRQ_GATTS_WRITE:
            conn_handle, value_handle = data
            if value_handle == self._rx_handle:
                msg = self._ble.gatts_read(self._rx_handle).decode('utf-8').strip()
                self.on_rx(msg)

    def _advertise(self, name="Pico2W_BLE"):
        payload = b'\x02\x01\x06' + bytes([len(name) + 1, 0x09]) + name.encode('utf-8')
        self._ble.gap_advertise(100000, adv_data=payload)

    def send(self, data):
        for conn_handle in self._connections:
            self._ble.gatts_notify(conn_handle, self._tx_handle, data + '\n')

    def on_rx(self, msg):
        print(f"수신 데이터: {msg}")

10.2 블루투스(BLE) 연동 응용 제어

방금 저장한 블루투스 엔진 파일(ble_uart.py)을 불러와서(Import), 내가 원하는 스마트홈 원격 제어 기능만 덧붙이는 실제 메인 제어 코드입니다. 클래스 상속 기법 덕분에 코드가 믿을 수 없을 정도로 간결해집니다.

🚀 [파일 2] 메인 구동 파일 저장 규칙

아래 코드를 Thonny의 완전히 새로운 창에 복사한 뒤, 보드 내부에 반드시 main.py라는 이름으로 저장하세요. 저장 후 실행 버튼을 누르면 스마트폰 앱에서 보드가 검색됩니다.

# 앞서 내부에 저장한 ble_uart 파일로부터 통신 엔진 클래스를 불러옵니다.
from ble_uart import PicoBLEUART  
from machine import Pin
import time

# 제어 대상인 내장 LED 설정
led = Pin("LED", Pin.OUT)

# 부모인 PicoBLEUART의 강력한 무선 통신 기능을 그대로 물려받습니다(상속).
class CustomIoTController(PicoBLEUART):
    def on_rx(self, msg):  # 스마트폰으로부터 글자가 도착했을 때 실행할 행동만 정의!
        print(f"📱 블루투스 명령 접수: {msg}")
        
        if msg == "on":
            led.value(1)
            self.send("Pico2W > LED가 켜졌습니다!")
            print("-> 하드웨어 처리: LED ON")
        elif msg == "off":
            led.value(0)
            self.send("Pico2W > LED가 꺼졌습니다!")
            print("-> 하드웨어 처리: LED OFF")
        else:
            self.send(f"알 수 없는 명령어: {msg}")

# 실전 스마트홈 서버 구동 (10.1에서 커스텀한 이름 클래스 생성이 연동됩니다)
iot_server = CustomIoTController()

while True:
    time.sleep(1) # 무선 이벤트 수신을 위해 루프를 유지합니다.

10.3 IR 적외선 수신기 기본 세팅 방법과 원리

적외선(IR) 수신 모듈(VS1838B 기준)은 눈에 보이지 않는 940nm 파장의 적외선 빛 신호를 감지하여 전기 신호로 바꾸어 줍니다. 핀 배선 시 플러스(VCC)와 마이너스(GND)가 반대로 교차하면 센서 내부 소자가 손상되므로 아래의 지도를 엄격하게 지켜 연결해야 합니다.

📍 하드웨어 배선 가이드 (둥근 감지 렌즈가 내 얼굴을 바라보는 기준)

1번 핀 (맨 왼쪽, OUT): 나의 GPIO 2번 (물리 핀 4번)
2번 핀 (가운데, GND): 나의 GND (물리 핀 3번)
3번 핀 (맨 오른쪽, VCC): 나의 3V3(OUT) (물리 핀 36번)

Pico 2 GP2 GND 3V3 IR 수신기 (VS1838B) ①OUT ②GND ③VCC ① OUT (맨 왼쪽) ② GND (가운데) ③ VCC (맨 오른쪽) ← IR 신호 수신(IN)

렌즈가 나를 향하도록 세운 기준: ①OUT→GP2, ②GND→GND, ③VCC→3V3. VCC·GND 역결선 시 센서 소손 위험이 있으니 반드시 핀 방향을 확인하세요.

10.4 적외선(IR) 리모컨의 데이터 송신 구조

가장 대중적인 NEC 프로토콜 규격의 리모컨은 버튼을 누르면 무조건 맨 앞에 "나 신호 보낸다!"라고 노크하는 9000us 크기의 거대한 리더 펄스(Leader Pulse)를 예외 없이 쏘아 보냅니다. 진짜 어떤 버튼인지 식별하는 유니크한 데이터는 이 리더 펄스 뒤에 눈 깜짝할 사이에 지나가는 미세한 32비트 데이터 배열 속에 숨어 있습니다.

10.5 IR 수신기 미세 오차 및 수치 출렁임의 비밀

테스트 중 동일한 버튼을 눌러도 측정 시간이 8900us, 9150us 등으로 조금씩 다르게 측정되는 것은 매우 정상적인 현상입니다. 리모컨을 조준하는 각도, 배터리 잔량, 방 안의 형광등 빛(노이즈), 그리고 보드 내부의 백그라운드 연산 타이밍 때문에 ±100~500us 수준의 하드웨어적 오차가 발생하기 때문입니다. 따라서 하드웨어 코딩 시에는 정확한 정수 매칭 대신 범위(Range) 조건문을 활용해야 합니다.

10.6 실전 IoT: 기초 리모컨 자동 학습 및 영구 저장

번거롭게 리모컨 코드를 일일이 확인하여 하드코딩할 필요가 없습니다. 새로운 리모컨 버튼을 누르면 보드가 실시간으로 감지하여 Thonny 셸 창을 통해 이름을 물어보고, 이를 마이크로파이썬 내부 공간에 JSON 형태의 데이터베이스 파일(remote_db.json)로 저장하는 스마트 자동 영구 학습 시스템 소스 코드입니다.

⚠️ 터미널 한글 입력 제한 주의사항

마이크로파이썬 터미널 환경의 한계로 인해, Thonny 입력창에 '한글'을 입력하면 신호가 잘리거나 크래시가 발생할 수 있습니다.

새로운 버튼 이름 등록 질문이 나오면 반드시 영문이나 숫자(예: 1, btn_1, power, vol_up)로 타이핑해 주어야 안전하게 데이터베이스 파일이 빌드됩니다.

import json
import os
import time
from machine import Pin

# IR 수신기 설정
ir_sensor = Pin(2, Pin.IN)
DB_FILE = "remote_db.json"

# --- [저장소 관리 함수 (마이크로파이썬 경량 규격)] ---
def load_remote_db():
    if DB_FILE in os.listdir():
        with open(DB_FILE, "r") as f:
            try: return json.load(f)
            except: return {}
    return {}

def save_remote_db(data):
    with open(DB_FILE, "w") as f:
        json.dump(data, f)

# 시스템 구동 시 기존 데이터 자동 로드
remote_mapping = load_remote_db()
print(f"💾 기존 학습 데이터베이스 로드 완료: 총 {len(remote_mapping)}개의 코드를 기억하고 있습니다.")

def read_ir_command():
    while ir_sensor.value() == 1: pass
    start = time.ticks_us()
    while ir_sensor.value() == 0: pass
    leader_duration = time.ticks_diff(time.ticks_us(), start)

    if not (8000 < leader_duration < 10000): return None
    while ir_sensor.value() == 1: pass

    data = 0
    for i in range(32):
        while ir_sensor.value() == 0: pass
        t_start = time.ticks_us()
        while ir_sensor.value() == 1: pass
        high_duration = time.ticks_diff(time.ticks_us(), t_start)
        if high_duration > 1000:
            data |= (1 << i)
    return hex(data)

print("\n🤖 [기초 리모컨 자동 학습 모드 가동]")
while True:
    cmd = read_ir_command()
    if cmd:
        if cmd in remote_mapping:
            print(f"▶ [인식 성공] '{remote_mapping[cmd]}' 버튼이 눌렸습니다!")
        else:
            print(f"\n✨ [새로운 버튼 포착] 고유 코드: {cmd}")
            button_name = input("이 버튼의 이름을 정해주세요 (영문/숫자 입력, 취소는 엔터): ").strip()
            if button_name:
                remote_mapping[cmd] = button_name
                save_remote_db(remote_mapping)
                print(f"💾 '{button_name}' 버튼이 저장되었습니다!\n")
    time.sleep(0.2)

10.7 심화 IoT: 중복/변형 코드 대응 스마트 링크 학습 시스템

일부 리모컨은 오작동을 막기 위해 동일한 버튼을 누르더라도 매번 신호를 바꾸는 '토글 비트'를 전송하거나, 누르는 길이에 따라 미세한 '변형 코드'를 추가로 발생시킵니다. 이 경우 하나의 버튼이 여러 개의 고유 코드를 가질 수 있습니다.

아래 코드는 동일한 버튼에서 새로운 변형 코드가 포착되었을 때, 기존에 등록된 버튼 목록을 화면에 띄워주고 리스트 번호 선택만으로 하나의 버튼 이름에 복수의 코드를 중복 링크(다대일 매핑)해주는 고도화된 펌웨어 학습 시스템입니다.

import json
import os
import time
from machine import Pin

ir_sensor = Pin(2, Pin.IN)
DB_FILE = "remote_db.json"

def load_remote_db():
    if DB_FILE in os.listdir():
        with open(DB_FILE, "r") as f:
            try: return json.load(f)
            except: return {}
    return {}

def save_remote_db(data):
    with open(DB_FILE, "w") as f:
        json.dump(data, f)

remote_mapping = load_remote_db()
print(f"💾 기존 학습 데이터 로드 완료. (총 {len(remote_mapping)}개 코드 기억 중)")

def read_ir_command():
    while ir_sensor.value() == 1: pass
    start = time.ticks_us()
    while ir_sensor.value() == 0: pass
    leader_duration = time.ticks_diff(time.ticks_us(), start)

    if not (8000 < leader_duration < 10000): return None
    while ir_sensor.value() == 1: pass

    data = 0
    for i in range(32):
        while ir_sensor.value() == 0: pass
        t_start = time.ticks_us()
        while ir_sensor.value() == 1: pass
        high_duration = time.ticks_diff(time.ticks_us(), t_start)
        if high_duration > 1000:
            data |= (1 << i)
    return hex(data)

print("\n🤖 [업그레이드: 스마트 복수 코드 학습기 가동]")

while True:
    cmd = read_ir_command()
    if cmd:
        if cmd in remote_mapping:
            print(f"▶ [인식 성공] '{remote_mapping[cmd]}' 버튼이 눌렸습니다!")
        else:
            print(f"\n✨ [새로운 변형 코드 포착] 고유 코드: {cmd}")
            
            # 현재까지 등록된 유니크한 버튼 이름 목록 추출 후 정렬
            existing_names = list(set(remote_mapping.values()))
            existing_names.sort()
            
            if existing_names:
                print("--- 📋 현재 등록된 버튼 목록 ---")
                for idx, name in enumerate(existing_names):
                    print(f" [{idx + 1}] {name}")
                print("--------------------------------")
                print("👉 위 목록의 '번호'를 입력하면 해당 버튼의 서브 코드로 즉시 추가됩니다!")

            user_input = input("버튼 번호(숫자) 선택 또는 새 이름 입력 (취소는 엔터): ").strip()

            if user_input:
                # 사용자가 목록의 번호(숫자)를 선택한 경우 기존 이름 매핑
                if user_input.isdigit() and 0 < int(user_input) <= len(existing_names):
                    button_name = existing_names[int(user_input) - 1]
                else:
                    # 새로운 이름을 직접 타이핑한 경우
                    button_name = user_input

                remote_mapping[cmd] = button_name
                save_remote_db(remote_mapping)
                print(f"💾 코드 {cmd}가 '{button_name}' 버튼에 추가 매핑되었습니다!\n")
            else:
                print("❌ 등록이 취소되었습니다.\n")

    time.sleep(0.2)

10.8 최종 응용: 리모컨 데이터베이스 연동 실전 제어

10.7 과정을 통해 변형 코드들까지 축적된 remote_db.json 지도를 기반으로 동작하는 최종 실전 구동 프로그램입니다. 32비트 디코딩 엔진 루프와 데이터베이스 조회 기능이 실시간으로 맞물려 작동하며, 어떤 변형 코드가 들어오더라도 자석처럼 필터링하여 매핑된 정형화 동작(내장 LED 토글, 서보모터 구동 등)을 안전하게 실행해 냅니다.

import json
import time
from machine import Pin

# IR 수신기 및 제어할 하드웨어(내장 LED) 설정
ir_sensor = Pin(2, Pin.IN)
target_led = Pin("LED", Pin.OUT)

# 1) 앞선 단계에서 영구 저장했던 리모컨 DB 파일(JSON)을 가집니다.
try:
    with open("remote_db.json", "r") as f:
        remote_db = json.load(f)
    print(f"💾 리모컨 DB 로드 완료! 학습된 버튼 목록: {list(remote_db.values())}")
except Exception as e:
    print("❌ 에러: remote_db.json 파일이 없습니다. 앞선 코드로 버튼을 먼저 등록해주세요!")
    remote_db = {}

# 2) 32비트 전체 데이터를 정확히 파싱하는 해독 엔진 함수
def read_ir_command():
    while ir_sensor.value() == 1: pass
    start = time.ticks_us()
    while ir_sensor.value() == 0: pass
    leader_duration = time.ticks_diff(time.ticks_us(), start)

    if not (8000 < leader_duration < 10000): return None
    while ir_sensor.value() == 1: pass

    data = 0
    for i in range(32):
        while ir_sensor.value() == 0: pass
        t_start = time.ticks_us()
        while ir_sensor.value() == 1: pass
        high_duration = time.ticks_diff(time.ticks_us(), t_start)
        if high_duration > 1000:
            data |= (1 << i)
    return hex(data)

# 3) 실시간 명령 감지 및 하드웨어 매핑 통제 루프
print("\n📡 [실전 가전 제어 모드 가동] 리모컨 버튼을 눌러보세요...")

while True:
    cmd = read_ir_command()  # 실시간으로 변수를 정의하여 수신합니다.

    if cmd:
        # 내가 누른 코드가 DB(JSON) 안에 등록되어 있는지 확인
        if cmd in remote_db:
            action_name = remote_db[cmd]
            print(f"▶ [명령 판독] '{action_name}' 버튼 입력 감지!")

            # -------------------------------------------------------------
            # 📌 [실전 응용 인터페이스] 등록한 이름에 맞게 동작을 조각하는 영역
            # -------------------------------------------------------------
            if action_name == "power" or action_name == "1":
                target_led.toggle()  # 내장 LED를 제어합니다.
                print("-> 🛠️ [하드웨어 작동] 내장 LED 상태를 반전했습니다.")
            elif action_name == "vol_up" or action_name == "2":
                print("-> ⚙️ [액추에이터 제어] 서보모터를 +10도 회전합니다. (동작 예시)")
            elif action_name == "vol_down" or action_name == "3":
                print("-> ⚙️ [액추에이터 제어] 서보모터를 -10도 회전합니다. (동작 예시)")
        else:
            print(f"❓ 등록되지 않은 버튼입니다. 고유 코드: {cmd}")

    time.sleep(0.1)