Chapter 13. [종합] IoT 스마트 보안 시스템
지금까지 배운 모든 기술을 하나로 합칩니다. PIR 센서로 침입을 감지하고, 초음파 센서로 거리를 측정하며, LCD에 상태를 표시하고, 웹 서버를 통해 원격으로 경보를 해제하는 통합 시스템입니다.
초음파 센서는 GP17(Trig)과 GP16(Echo), LCD는 GP8/GP9 I2C, 부저는 GP14, PIR은 GP18을 사용합니다.
13.1 준비물과 연결 핀
아래 핀맵대로 연결하면 지금까지 배운 센서 입력, 출력 장치, 웹 제어가 한 프로젝트 안에서 함께 동작합니다.
| 부품 | Pico 2 W 핀 | 역할 |
|---|---|---|
| PIR 센서 OUT | GP18 |
사람 움직임 감지 |
| 초음파 Trig / Echo | GP17 / GP16 |
거리 측정 |
| I2C LCD SDA / SCL | GP8 / GP9 |
상태 메시지 표시 |
| 부저 + | GP14 |
침입 감지 시 경보음 |
🧩 이 프로젝트는 어떻게 조립되었나요?
이번 종합 프로젝트는 지금까지 배운 4가지 기술을 하나의 while True 반복문 안에서 순서대로 실행하는 구조입니다. 각 부분이 어디서 왔는지 정리하면 다음과 같습니다.
- PIR 센서 (Chapter 5):
machine.Pin(18, machine.Pin.IN)으로 움직임을 감지합니다. 값이1이면 사람이 움직였다는 뜻입니다. - 초음파 센서 HC-SR04 (Chapter 5):
trig에 짧은 펄스를 보내고echo가 반응하기까지 걸린 시간을 재서 거리(cm)로 환산합니다. - I2C LCD (Chapter 7/9):
I2cLcd객체로 현재 상태(정상/경고/Wi-Fi 정보)를 2줄짜리 화면에 표시합니다. - 웹 서버 (Chapter 10):
socket모듈로 80번 포트에서 요청을 받아,/arm,/disarm,/reset경로에 따라 시스템 상태를 원격으로 바꿉니다.
전체 흐름은 "센서 읽기 → 판단 → LCD 갱신 → (있다면) 웹 요청 처리 → 잠깐 대기"를 계속 반복하는 것입니다. 이렇게 하나의 반복문 안에서 여러 작업을 짧게 나누어 처리하는 방식을 협력적 멀티태스킹(cooperative multitasking)이라고 부르며, 마이크로컨트롤러처럼 운영체제 없이 동작하는 환경에서 자주 쓰이는 패턴입니다.
server.settimeout(0.05)로 accept() 대기시간을 아주 짧게 제한한 점이 핵심입니다. 만약 이 타임아웃이 없었다면 웹 브라우저가 접속하지 않는 동안 accept()에서 코드가 멈춰버려(blocking) PIR/초음파 센서를 실시간으로 확인하지 못했을 것입니다.
13.2 통합 시스템 메인 소스 코드
이 코드는 Ch 10(Web), Ch 5(Sensors), Ch 9(LCD)의 내용을 실제로 합친 예시입니다. 브라우저에서 Pico의 IP 주소로 접속하면 보안 모드 켜기/끄기, 경보 초기화, 현재 거리와 움직임 상태를 확인할 수 있습니다.
import machine
import network
import socket
import time
from pico_i2c_lcd import I2cLcd
# Wi-Fi 설정
SSID = "YOUR_WIFI_NAME"
PASSWORD = "YOUR_WIFI_PASSWORD"
# 보안 기준값
DIST_LIMIT_CM = 20
BUZZER_TIME = 0.25
# 핀 설정
pir = machine.Pin(18, machine.Pin.IN)
trig = machine.Pin(17, machine.Pin.OUT)
echo = machine.Pin(16, machine.Pin.IN)
buzzer = machine.Pin(14, machine.Pin.OUT)
# LCD 설정
i2c = machine.I2C(0, sda=machine.Pin(8), scl=machine.Pin(9))
lcd = I2cLcd(i2c, 0x27, 2, 16)
is_armed = True
alarm_on = False
last_distance = 0
last_motion = 0
def lcd_message(line1, line2=""):
lcd.clear()
lcd.move_to(0, 0)
lcd.putstr(line1[:16])
lcd.move_to(0, 1)
lcd.putstr(line2[:16])
def connect_wifi():
wlan = network.WLAN(network.STA_IF)
wlan.active(True)
wlan.connect(SSID, PASSWORD)
lcd_message("Wi-Fi Connect", "Please wait...")
for _ in range(20):
if wlan.isconnected():
ip = wlan.ifconfig()[0]
lcd_message("Web Server IP", ip)
print("Connected:", ip)
return ip
time.sleep(0.5)
lcd_message("Wi-Fi Failed", "Check SSID/PW")
raise RuntimeError("Wi-Fi connection failed")
def wait_pin_value(pin, target, timeout_us=30000):
start = time.ticks_us()
while pin.value() != target:
if time.ticks_diff(time.ticks_us(), start) > timeout_us:
return None
return time.ticks_us()
def get_distance():
trig.low()
time.sleep_us(2)
trig.high()
time.sleep_us(10)
trig.low()
start = wait_pin_value(echo, 1)
if start is None:
return 999
end = wait_pin_value(echo, 0)
if end is None:
return 999
return (time.ticks_diff(end, start) * 0.0343) / 2
def beep(count=1):
for _ in range(count):
buzzer.high()
time.sleep(BUZZER_TIME)
buzzer.low()
time.sleep(0.08)
def render_page():
armed_text = "ON" if is_armed else "OFF"
alarm_text = "DETECTED" if alarm_on else "CLEAR"
motion_text = "YES" if last_motion else "NO"
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 Security</title>
<style>
body {{ font-family: Arial, sans-serif; max-width: 560px; margin: 40px auto; color: #0f172a; }}
.box {{ border: 1px solid #e2e8f0; border-radius: 12px; padding: 20px; }}
.danger {{ color: #dc2626; font-weight: bold; }}
.ok {{ color: #16a34a; font-weight: bold; }}
a {{ display: inline-block; margin: 6px; padding: 10px 14px; border-radius: 8px; background: #0ea5e9; color: white; text-decoration: none; }}
a.warn {{ background: #f97316; }}
</style>
</head>
<body>
<h1>Pico 2 W Security</h1>
<div class="box">
<p>보안 모드: <strong>{armed_text}</strong></p>
<p>경보 상태: <span class="{'danger' if alarm_on else 'ok'}">{alarm_text}</span></p>
<p>움직임 감지: {motion_text}</p>
<p>거리: {last_distance:.1f} cm</p>
<a href="/arm">ARM</a>
<a href="/disarm" class="warn">DISARM</a>
<a href="/reset">RESET</a>
</div>
</body>
</html>"""
def handle_request(request):
global is_armed, alarm_on
if "GET /arm" in request:
is_armed = True
lcd_message("Security Mode", "ARMED")
elif "GET /disarm" in request:
is_armed = False
alarm_on = False
buzzer.low()
lcd_message("Security Mode", "DISARMED")
elif "GET /reset" in request:
alarm_on = False
buzzer.low()
lcd_message("Alarm Reset", "Monitoring...")
def start_server(ip):
addr = socket.getaddrinfo("0.0.0.0", 80)[0][-1]
server = socket.socket()
server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
server.bind(addr)
server.listen(1)
server.settimeout(0.05)
print("Open browser: http://" + ip)
return server
ip = connect_wifi()
server = start_server(ip)
lcd_message("System Ready", ip)
while True:
last_motion = pir.value()
last_distance = get_distance()
if is_armed:
if last_motion == 1 or last_distance < DIST_LIMIT_CM:
alarm_on = True
lcd_message("!! WARNING !!", "INTRUDER")
beep(2)
else:
lcd.move_to(0, 0)
lcd.putstr("Status: Secure ")
lcd.move_to(0, 1)
lcd.putstr(f"Dist:{last_distance:5.1f}cm ")
try:
client, addr = server.accept()
request = client.recv(1024).decode()
handle_request(request)
response = render_page()
client.send("HTTP/1.1 200 OK\r\nContent-Type: text/html\r\n\r\n")
client.send(response)
client.close()
except OSError:
pass
time.sleep(0.1)
💡 테스트 순서
먼저 SSID와 PASSWORD를 수정한 뒤 실행하세요. LCD에 표시된 IP 주소를 같은 Wi-Fi에 연결된 컴퓨터나 스마트폰 브라우저에 입력하면 제어 화면이 열립니다.
13.3 동작 흐름
시스템은 0.1초 간격으로 PIR과 초음파 센서를 읽습니다. 보안 모드가 켜져 있고 움직임이 감지되거나 거리가 20cm보다 가까워지면 LCD에 경고를 띄우고 부저를 울립니다. 웹 대시보드에서 DISARM을 누르면 감지를 잠시 끄고, ARM을 누르면 다시 감시를 시작합니다.
🚀 확장 과제. Wi-Fi 알림 기능 추가하기
현재 시스템은 침입이 감지되면 LCD와 부저로만 알려줍니다. 만약 집에 아무도 없다면 이 알림을 놓칠 수 있습니다. 침입이 감지되는 순간, Adafruit IO(Chapter 11)의 feed에 "INTRUDER_ALERT" 메시지를 발행(publish)해서 스마트폰 대시보드로 실시간 알림을 받도록 코드를 확장해 보세요.
13.4 Wokwi 시뮬레이션으로 실습하기
Chapter 1(1.8)에서 소개한 Wokwi 온라인 시뮬레이터에서 센서와 LCD 부분을 미리 연습해볼 수 있습니다.
⚠️ 웹 서버/Wi-Fi 부분은 시뮬레이터에서 테스트할 수 없어요
Wokwi 시뮬레이션 Pico 보드는 실제 인터넷에 연결되지 않으므로, network.WLAN 연결과 socket 웹 서버 부분은 동작을 확인할 수 없습니다. 대신 PIR 센서 + 초음파 센서 + LCD 조합의 감지·표시 로직은 Wokwi에서 그대로 실습할 수 있습니다. 완성된 코드는 반드시 실제 Pico 2 W와 Wi-Fi 환경에서 웹 서버까지 함께 테스트하세요.
아래 diagram.json을 Wokwi 프로젝트의 diagram 편집기에 붙여넣고, main.py에는 PIR·초음파·LCD 로직만 남긴 축소 버전(웹 서버 부분 제외)을 넣어 초록색 ▶️ 재생 버튼으로 실행해 보세요.
{
"version": 1,
"author": "Pico 2 Guide",
"editor": "wokwi",
"parts": [
{ "type": "wokwi-pi-pico", "id": "pico", "top": 0, "left": 0, "attrs": { "env": "micropython-20231005-v1.21.0" } },
{ "type": "wokwi-pir-motion-sensor", "id": "pir1", "top": -80, "left": -40, "attrs": {} },
{ "type": "wokwi-hc-sr04", "id": "us1", "top": -80, "left": 100, "attrs": {} },
{ "type": "wokwi-lcd1602", "id": "lcd1", "top": 60, "left": 40, "attrs": { "pins": "i2c" } }
],
"connections": [
[ "pico:GP18", "pir1:OUT", "green", [] ],
[ "pir1:VCC", "pico:3V3", "red", [] ],
[ "pir1:GND", "pico:GND", "black", [] ],
[ "pico:GP17", "us1:TRIG", "blue", [] ],
[ "pico:GP16", "us1:ECHO", "orange", [] ],
[ "us1:VCC", "pico:VBUS", "red", [] ],
[ "us1:GND", "pico:GND", "black", [] ],
[ "pico:GP8", "lcd1:SDA", "green", [] ],
[ "pico:GP9", "lcd1:SCL", "yellow", [] ],
[ "lcd1:VCC", "pico:3V3", "red", [] ],
[ "lcd1:GND", "pico:GND", "black", [] ]
]
}
💡 Tip: 정확한 부품 이름과 핀 속성은 Wokwi 편집기 좌측의 부품 검색창에서 다시 확인하는 것을 권장합니다. HC-SR04는 보통 5V 전원을 필요로 하므로 Wokwi에서도 VBUS(5V) 핀에 연결하는 것이 안전합니다.
🏁 완강을 축하드립니다!
이제 여러분은 하드웨어 제어, 통신, 클라우드 연동까지 가능한 Pico 2 마스터가 되었습니다. 이 가이드북의 기초를 바탕으로 여러분만의 더 멋진 발명품을 만들어보세요!