Chapter 10. 무선 통신 & 입력 종합 (Wi-Fi, 웹 서버, BLE, IR)

Pico 2 W의 무선 기능 전체를 집중 학습합니다. 2.4GHz Wi-Fi 연결부터 IoT 웹 서버 구축, 내장 블루투스(BLE), 그리고 적외선(IR) 리모컨 자동 학습까지 다룹니다.


10.1 Wi-Fi 네트워크 연결

Pico 2 W는 2.4GHz 802.11n Wi-Fi를 내장합니다. network 모듈을 사용하면 몇 줄의 코드로 무선 공유기에 접속하고 IP 주소를 할당받을 수 있습니다.

단계별 와이파이 연결 실습

import network
import time

SSID = "your_wifi_name"
PASSWORD = "your_wifi_password"

def connect_wifi():
    wlan = network.WLAN(network.STA_IF)
    wlan.active(True)

    if wlan.isconnected():
        print("이미 연결됨:", wlan.ifconfig()[0])
        return wlan

    wlan.connect(SSID, PASSWORD)
    print("Wi-Fi 연결 시도 중...")

    for _ in range(20):
        if wlan.isconnected():
            ip = wlan.ifconfig()[0]
            print(f"✅ 연결 성공! IP: {ip}")
            return wlan
        time.sleep(0.5)

    raise RuntimeError("❌ Wi-Fi 연결 실패. SSID/PW를 확인하세요.")

wlan = connect_wifi()
print("네트워크 정보:", wlan.ifconfig())

💡 결과 확인

Shell 창에 ifconfig()의 출력이 표시됩니다. (IP, 서브넷, 게이트웨이, DNS) 순서로 나타납니다. IP 주소가 0.0.0.0이라면 연결이 아직 안 된 상태입니다.


10.2 IoT 웹 서버 구축 (LED 원격 제어)

Pico가 Wi-Fi에 접속한 뒤 소켓 서버를 열면, 같은 네트워크의 스마트폰/PC 브라우저로 접속해 GPIO를 원격 제어할 수 있습니다. POST 방식을 쓰면 브라우저의 새로고침 오작동(재전송) 문제를 방지할 수 있습니다.

단계별 웹 서버 빌드업

import network
import socket
import time
from machine import Pin

SSID = "your_wifi_name"
PASSWORD = "your_wifi_password"

led = Pin("LED", Pin.OUT)
led_state = False

def connect_wifi():
    wlan = network.WLAN(network.STA_IF)
    wlan.active(True)
    wlan.connect(SSID, PASSWORD)
    for _ in range(20):
        if wlan.isconnected():
            return wlan.ifconfig()[0]
        time.sleep(0.5)
    raise RuntimeError("Wi-Fi 연결 실패")

def make_html():
    state = "ON" if led_state else "OFF"
    color = "#22c55e" if led_state else "#64748b"
    return f"""<!DOCTYPE html>
<html lang="ko">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Pico LED 제어</title>
    <style>
        body{{font-family:sans-serif;text-align:center;margin-top:60px;}}
        .status{{font-size:3rem;font-weight:bold;color:{color};}}
        form button{{font-size:1.4rem;padding:12px 40px;border-radius:10px;
            border:none;background:#2563eb;color:white;cursor:pointer;margin-top:20px;}}
    </style>
</head>
<body>
    <h1>Pico 2 W LED 제어</h1>
    <p class="status">LED: {state}</p>
    <form method="POST" action="/toggle">
        <button type="submit">LED 켜기/끄기</button>
    </form>
</body>
</html>"""

def start_server(ip):
    addr = socket.getaddrinfo("0.0.0.0", 80)[0][-1]
    s = socket.socket()
    s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
    s.bind(addr)
    s.listen(1)
    s.settimeout(5)
    print(f"웹 서버 시작! 브라우저에서 http://{ip} 로 접속하세요.")
    return s

ip = connect_wifi()
server = start_server(ip)

while True:
    try:
        client, addr = server.accept()
        request = client.recv(1024).decode()

        global led_state
        if "POST /toggle" in request:
            led_state = not led_state
            led.value(led_state)
            # POST→Redirect→GET 패턴으로 새로고침 중복 전송 방지
            client.send("HTTP/1.1 303 See Other\r\nLocation: /\r\n\r\n")
        else:
            html = make_html()
            client.send("HTTP/1.1 200 OK\r\nContent-Type: text/html; charset=utf-8\r\n\r\n")
            client.send(html)

        client.close()

    except OSError:
        pass

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

Pico 2 W는 무선 칩셋 안에 Bluetooth Low Energy (BLE) 안테나를 기본 내장하고 있습니다. 반드시 Serial Bluetooth Terminal 같은 전문 BLE 통신 앱을 다운받아 사용해야 합니다.

스마트폰 앱 "on" 무선 패킷 전송 Pico 2 (on_rx 함수) if msg == "on": led.value(1) 스마트폰에서 보낸 문자열에 따라 하드웨어가 즉각 반응합니다.

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

학교 강의실처럼 여러 사람이 동시에 실습할 때 블루투스 명칭이 같으면 스마트폰 앱에서 누구의 보드인지 구별할 수 없습니다! 아래 코드의 name="Pico2W_BLE" 부분을 반드시 자신만의 고유한 이름으로 수정하세요.

💾 [파일 1] ble_uart.py 저장 규칙

아래 코드를 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.4 블루투스(BLE) 연동 응용 제어

방금 저장한 ble_uart.py를 불러와 내가 원하는 스마트홈 원격 제어 기능을 덧붙이는 메인 제어 코드입니다.

🚀 [파일 2] main.py 저장 규칙

아래 코드를 새 창에 복사한 뒤, 보드 내부에 main.py로 저장하세요.

from ble_uart import PicoBLEUART
from machine import Pin
import time

led = Pin("LED", Pin.OUT)

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}")

iot_server = CustomIoTController()

while True:
    time.sleep(1)

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

적외선(IR) 수신 모듈(VS1838B 기준)은 눈에 보이지 않는 940nm 파장의 적외선 빛 신호를 감지하여 전기 신호로 바꾸어 줍니다.

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

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.6 실전 IoT: 기초 리모컨 자동 학습 및 영구 저장

새로운 리모컨 버튼을 누르면 보드가 실시간으로 감지하여 이름을 물어보고, remote_db.json 파일로 영구 저장하는 스마트 자동 학습 시스템입니다.

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

마이크로파이썬 터미널 환경의 한계로 인해, Thonny 입력창에 '한글'을 입력하면 신호가 잘리거나 크래시가 발생할 수 있습니다. 버튼 이름은 반드시 영문이나 숫자(예: 1, btn_1, power, vol_up)로 입력하세요.

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}")
            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 = sorted(set(remote_mapping.values()))

            if existing_names:
                print("--- 📋 현재 등록된 버튼 목록 ---")
                for idx, name in enumerate(existing_names):
                    print(f" [{idx + 1}] {name}")
                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 최종 응용: 리모컨 데이터베이스 연동 실전 제어

앞선 과정에서 쌓인 remote_db.json을 기반으로 동작하는 최종 실전 구동 프로그램입니다.

import json
import time
from machine import Pin

ir_sensor = Pin(2, Pin.IN)
target_led = Pin("LED", Pin.OUT)

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 = {}

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_db:
            action_name = remote_db[cmd]
            print(f"▶ [명령 판독] '{action_name}' 버튼 입력 감지!")

            if action_name == "power" or action_name == "1":
                target_led.toggle()
                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)

10.9 과제 해결해보기!

과제 1. 웹 서버에 LED 상태를 보여주는 텍스트 페이지 추가하기

10.2의 웹 서버 코드를 참고하여, 브라우저에서 /status 경로로 접속하면 현재 내장 LED가 켜져 있는지 꺼져 있는지 텍스트로 "LED is ON" / "LED is OFF"를 보여주는 기능을 추가해 보세요. (힌트: request 문자열 안에 "GET /status"가 들어있는지 확인하면 됩니다.)

💻 정답 코드 보기
# 10.2의 while True 루프 안 요청 분기 부분을 아래처럼 수정합니다.

while True:
    try:
        client, addr = server.accept()
        request = client.recv(1024).decode()

        if "POST /toggle" in request:
            led_state = not led_state
            led.value(led_state)
            client.send("HTTP/1.1 303 See Other\r\nLocation: /\r\n\r\n")

        elif "GET /status" in request:
            status_text = "LED is ON" if led_state else "LED is OFF"
            client.send("HTTP/1.1 200 OK\r\nContent-Type: text/plain; charset=utf-8\r\n\r\n")
            client.send(status_text)

        else:
            html = make_html()
            client.send("HTTP/1.1 200 OK\r\nContent-Type: text/html; charset=utf-8\r\n\r\n")
            client.send(html)

        client.close()
    except OSError:
        pass

# 브라우저 주소창에 http:///status 로 접속하면
# "LED is ON" 또는 "LED is OFF" 텍스트만 표시됩니다.

과제 2. BLE로 밝기 최대 모드(깜빡임) 명령어 추가하기

10.4의 CustomIoTController를 참고하여, 스마트폰에서 "bright"라는 문자열을 보내면 내장 LED를 0.2초 간격으로 3번 깜빡인 뒤 "밝기 최대 모드입니다"라는 응답을 돌려주는 명령어를 추가해 보세요.

  • 기존의 "on", "off" 명령어는 그대로 유지합니다.
  • elif 분기를 하나 추가하면 됩니다.
💻 정답 코드 보기
from ble_uart import PicoBLEUART
from machine import Pin
import time

led = Pin("LED", Pin.OUT)

class CustomIoTController(PicoBLEUART):
    def on_rx(self, msg):
        print(f"📱 블루투스 명령 접수: {msg}")

        if msg == "on":
            led.value(1)
            self.send("Pico2W > LED가 켜졌습니다!")
        elif msg == "off":
            led.value(0)
            self.send("Pico2W > LED가 꺼졌습니다!")
        elif msg == "bright":
            for _ in range(3):
                led.value(1)
                time.sleep(0.2)
                led.value(0)
                time.sleep(0.2)
            self.send("Pico2W > 밝기 최대 모드입니다")
        else:
            self.send(f"알 수 없는 명령어: {msg}")

iot_server = CustomIoTController()

while True:
    time.sleep(1)

10.10 Wokwi 시뮬레이션으로 실습하기

Chapter 1.8에서 소개한 Wokwi 온라인 시뮬레이터로 이번 챕터의 일부 실습을 미리 확인해 볼 수 있습니다.

⚠️ 중요: Wi-Fi와 BLE는 Wokwi 무료 플랜에서 시뮬레이션 불가

Wokwi의 무료(Free) 플랜은 Pico 프로젝트에서 실제 인터넷/Wi-Fi 접속과 블루투스(BLE) 통신을 지원하지 않습니다. 즉, 10.1~10.2의 network.WLAN Wi-Fi 연결 및 소켓 웹 서버, 10.3~10.4의 BLE UART 코드는 Wokwi 위에서 실제로 동작을 확인할 수 없습니다. 이 부분은 반드시 실제 Pico 2 W 보드로 실습해야 합니다.

반면 10.5~10.8의 IR 리모컨 학습 부분은 Wokwi에서 시뮬레이션이 가능합니다. Wokwi가 제공하는 wokwi-ir-remote(가상 리모컨)와 wokwi-ir-receiver(가상 IR 수신기) 부품을 이용하면 실제 리모컨과 수신 모듈 없이도 IR 신호 수신 로직을 테스트해 볼 수 있습니다.

💻 IR 리모컨 학습 파트 실습 순서

  1. wokwi.com에서 [New Project] → "Raspberry Pi Pico" (MicroPython) 템플릿을 선택합니다.
  2. 왼쪽 diagram.json 탭에 아래 코드를 붙여넣어 Pico, 가상 IR 리모컨, 가상 IR 수신기를 자동으로 연결합니다.
  3. 오른쪽 main.py 탭에 10.6의 기초 리모컨 자동 학습 코드를 붙여넣습니다.
  4. 화면 상단의 ▶️ (녹색 재생 버튼)을 누른 뒤, 시뮬레이터 화면의 가상 리모컨 버튼을 클릭하면 IR 신호가 전송되고 Shell 창에서 학습 결과를 확인할 수 있습니다.
{
  "version": 1,
  "author": "Pico 2 IoT Guide",
  "editor": "wokwi",
  "parts": [
    { "type": "wokwi-pi-pico", "id": "pico", "top": 0, "left": 0, "attrs": {} },
    { "type": "wokwi-ir-receiver", "id": "ir_recv", "top": -40, "left": 150, "attrs": {} },
    { "type": "wokwi-ir-remote", "id": "ir_remote", "top": -40, "left": 300, "attrs": {} }
  ],
  "connections": [
    [ "pico:GP2", "ir_recv:OUT", "green", [] ],
    [ "pico:3V3", "ir_recv:VCC", "red", [] ],
    [ "pico:GND", "ir_recv:GND", "black", [] ]
  ]
}

⚠️ 참고 사항

Wokwi의 wokwi-ir-remote, wokwi-ir-receiver 부품 이름과 속성은 버전에 따라 달라질 수 있습니다. 부품이 보이지 않거나 이름이 다르다면, 에디터 좌측의 부품 검색(parts search)에서 "ir"로 검색하여 정확한 부품명을 확인하고 type 값을 알맞게 수정하세요. 또한 가상 리모컨이 보내는 적외선 프로토콜(NEC 등)이 실제 리모컨과 100% 동일하지 않을 수 있으니, 최종 검증은 실제 하드웨어로 진행하세요.