IoTLabs

Nghiên cứu, Sáng tạo và Thử nghiệm

Hệ thống giám sát nhà thông minh mini với Raspberry Pi – Bài 3: Đọc trạng thái cửa và phát hiện chuyển động bằng Python

Ở bài trước, chúng ta đã đấu nối thành công cảm biến cửa từ và PIR với Raspberry Pi.
Từ thời điểm này, phần cứng đã sẵn sàng. Việc tiếp theo là viết chương trình Python để hệ thống có thể:

  • đọc trạng thái cửa mở/đóng
  • phát hiện có chuyển động hay không
  • nhận ra khi trạng thái thay đổi
  • ghi log sự kiện tại local

Đây là bước rất quan trọng, vì một hệ thống giám sát không chỉ cần “đọc giá trị hiện tại”, mà còn phải biết:

👉 khi nào có sự kiện mới xảy ra

Ví dụ:

  • cửa vừa mở
  • cửa vừa đóng
  • PIR vừa phát hiện chuyển động
  • chuyển động đã kết thúc

Trong bài này, chúng ta sẽ xây phần mềm nền tảng để Raspberry Pi chuyển tín hiệu GPIO thành event có ý nghĩa.

Bạn sẽ làm được gì sau bài này

Sau khi hoàn thành bài 24.3, bạn sẽ có thể:

  • đọc trạng thái cảm biến cửa từ bằng Python
  • đọc trạng thái PIR bằng Python
  • phát hiện thay đổi trạng thái theo thời gian
  • áp dụng polling hoặc interrupt
  • ghi log event local để làm dữ liệu cho rule engine ở bài tiếp theo

Mục tiêu của bài này

Ở mức đơn giản, bạn có thể viết một vòng lặp như sau:

  • đọc GPIO cửa
  • đọc GPIO PIR
  • in ra terminal

Nhưng nếu chỉ làm vậy, hệ thống sẽ in lặp đi lặp lại rất nhiều dòng giống nhau và khó dùng về sau.

Ví dụ:

Door CLOSED
Door CLOSED
Door CLOSED
Door CLOSED
No motion
No motion
No motion

Thông tin như vậy không giúp ích nhiều.

Thứ chúng ta thật sự cần là:

  • chỉ ghi lại khi trạng thái thay đổi
  • biến trạng thái thành event
  • lưu event với timestamp
  • chuẩn bị nền tảng cho cảnh báo thông minh

Vì thế, bài này sẽ đi theo hướng đúng hơn:

GPIO -> State -> Event -> Local Log

Tổng quan luồng xử lý

Trong bài này, hệ thống sẽ hoạt động theo flow sau:

Door Sensor + PIR -> Raspberry Pi -> Python Monitor -> Event Log

Ví dụ:

Door CLOSED -> Door OPEN -> tạo event door_open
PIR IDLE -> Motion DETECTED -> tạo event motion_detected

Thay vì chỉ nhìn sensor như tín hiệu điện, ta bắt đầu xem chúng như nguồn phát sinh sự kiện.

Chuẩn bị

Bạn cần có sẵn:

  • Raspberry Pi
  • cảm biến cửa từ đã nối vào GPIO
  • cảm biến PIR đã nối vào GPIO
  • Python 3
  • thư viện RPi.GPIO

Nếu chưa cài thư viện GPIO, cài bằng lệnh:

pip install RPi.GPIO

Sơ đồ chân dùng trong bài

Trong ví dụ này, ta sẽ dùng:

  • Door Sensor → GPIO17
  • PIR Sensor → GPIO27

Bạn có thể đổi sang chân khác, miễn sửa lại code tương ứng.

Cách 1: Đọc trạng thái bằng polling

Polling là cách đơn giản nhất: chương trình liên tục đọc trạng thái sensor theo chu kỳ, ví dụ mỗi 200ms hoặc 500ms.

Ưu điểm:

  • dễ hiểu
  • dễ debug
  • phù hợp cho người mới

Nhược điểm:

  • chạy lặp liên tục
  • có thể in log dư thừa nếu không xử lý tốt
  • không “event-driven” bằng interrupt

Với bài này, polling là cách rất phù hợp để bắt đầu.

Bước 1: Đọc trạng thái hiện tại của cửa và PIR

Tạo file:

sensor_monitor.py

Code cơ bản

import RPi.GPIO as GPIO
import time

DOOR_PIN = 17
PIR_PIN = 27

GPIO.setmode(GPIO.BCM)
GPIO.setup(DOOR_PIN, GPIO.IN, pull_up_down=GPIO.PUD_UP)
GPIO.setup(PIR_PIN, GPIO.IN)

try:
    while True:
        door_state = GPIO.input(DOOR_PIN)
        pir_state = GPIO.input(PIR_PIN)

        if door_state == 0:
            print("Door: CLOSED")
        else:
            print("Door: OPEN")

        if pir_state == 1:
            print("Motion: DETECTED")
        else:
            print("Motion: IDLE")

        print("------")
        time.sleep(1)

except KeyboardInterrupt:
    print("Stopping...")
finally:
    GPIO.cleanup()

Kết quả khi chạy

Ví dụ đầu ra:

Door: CLOSED
Motion: IDLE
------
Door: CLOSED
Motion: IDLE
------
Door: OPEN
Motion: DETECTED
------

Code này giúp bạn xác nhận cảm biến đang hoạt động, nhưng vẫn còn một vấn đề:

👉 nó in đi in lại liên tục, kể cả khi trạng thái không đổi.

Bước 2: Chỉ phát hiện khi trạng thái thay đổi

Đây là bước nâng cấp quan trọng nhất.

Ta sẽ lưu lại trạng thái trước đó, rồi so sánh với trạng thái mới.
Chỉ khi nào khác nhau thì mới tạo event.

Code phát hiện thay đổi trạng thái

import RPi.GPIO as GPIO
import time

DOOR_PIN = 17
PIR_PIN = 27

GPIO.setmode(GPIO.BCM)
GPIO.setup(DOOR_PIN, GPIO.IN, pull_up_down=GPIO.PUD_UP)
GPIO.setup(PIR_PIN, GPIO.IN)

prev_door_state = GPIO.input(DOOR_PIN)
prev_pir_state = GPIO.input(PIR_PIN)

def get_door_label(state):
    return "OPEN" if state == 1 else "CLOSED"

def get_pir_label(state):
    return "DETECTED" if state == 1 else "IDLE"

try:
    print("Monitoring started...")

    while True:
        door_state = GPIO.input(DOOR_PIN)
        pir_state = GPIO.input(PIR_PIN)

        if door_state != prev_door_state:
            print(f"Door changed: {get_door_label(prev_door_state)} -> {get_door_label(door_state)}")
            prev_door_state = door_state

        if pir_state != prev_pir_state:
            print(f"Motion changed: {get_pir_label(prev_pir_state)} -> {get_pir_label(pir_state)}")
            prev_pir_state = pir_state

        time.sleep(0.2)

except KeyboardInterrupt:
    print("Stopping...")
finally:
    GPIO.cleanup()

Kết quả tốt hơn nhiều

Ví dụ khi cửa đang đóng và bạn mở ra:

Door changed: CLOSED -> OPEN

Khi PIR phát hiện chuyển động:

Motion changed: IDLE -> DETECTED

Khi chuyển động kết thúc:

Motion changed: DETECTED -> IDLE

Đây là kiểu dữ liệu đúng cho một hệ thống giám sát:
không spam log, chỉ ghi khi có biến động thực sự.

Biến thay đổi trạng thái thành event

Thay vì chỉ in câu chữ, ta nên chuẩn hóa thành event để dễ dùng lại ở các bài sau.

Ví dụ:

  • door_open
  • door_closed
  • motion_detected
  • motion_idle

Hàm chuyển trạng thái thành event

def door_event_name(state):
    return "door_open" if state == 1 else "door_closed"

def pir_event_name(state):
    return "motion_detected" if state == 1 else "motion_idle"

Khi đó đoạn xử lý sẽ thành:

if door_state != prev_door_state:
    event = door_event_name(door_state)
    print("Event:", event)
    prev_door_state = door_state

if pir_state != prev_pir_state:
    event = pir_event_name(pir_state)
    print("Event:", event)
    prev_pir_state = pir_state

Thêm timestamp cho event

Một event mà không có thời gian thì gần như không đủ giá trị.
Vì vậy, ta sẽ thêm timestamp vào log.

Code có timestamp

from datetime import datetime

def now_str():
    return datetime.now().strftime("%Y-%m-%d %H:%M:%S")

Khi log:

print(f"[{now_str()}] Event: {event}")

Ví dụ đầu ra:

[2026-04-11 08:42:10] Event: door_open
[2026-04-11 08:42:14] Event: motion_detected

Ghi log event ra file local

Đây là phần rất hữu ích. Thay vì chỉ in ra terminal, ta sẽ ghi event xuống file để xem lại sau.

Hàm ghi log file

def log_event(event_name):
    line = f"[{now_str()}] {event_name}\n"
    print(line.strip())

    with open("events.log", "a", encoding="utf-8") as f:
        f.write(line)

Khi đó:

if door_state != prev_door_state:
    event = door_event_name(door_state)
    log_event(event)
    prev_door_state = door_state

if pir_state != prev_pir_state:
    event = pir_event_name(pir_state)
    log_event(event)
    prev_pir_state = pir_state

Code hoàn chỉnh phiên bản polling + event log

import RPi.GPIO as GPIO
import time
from datetime import datetime

DOOR_PIN = 17
PIR_PIN = 27

GPIO.setmode(GPIO.BCM)
GPIO.setup(DOOR_PIN, GPIO.IN, pull_up_down=GPIO.PUD_UP)
GPIO.setup(PIR_PIN, GPIO.IN)

prev_door_state = GPIO.input(DOOR_PIN)
prev_pir_state = GPIO.input(PIR_PIN)

def now_str():
    return datetime.now().strftime("%Y-%m-%d %H:%M:%S")

def log_event(event_name):
    line = f"[{now_str()}] {event_name}"
    print(line)

    with open("events.log", "a", encoding="utf-8") as f:
        f.write(line + "\n")

def door_event_name(state):
    return "door_open" if state == 1 else "door_closed"

def pir_event_name(state):
    return "motion_detected" if state == 1 else "motion_idle"

try:
    print("Sensor monitoring started...")

    while True:
        door_state = GPIO.input(DOOR_PIN)
        pir_state = GPIO.input(PIR_PIN)

        if door_state != prev_door_state:
            event = door_event_name(door_state)
            log_event(event)
            prev_door_state = door_state

        if pir_state != prev_pir_state:
            event = pir_event_name(pir_state)
            log_event(event)
            prev_pir_state = pir_state

        time.sleep(0.2)

except KeyboardInterrupt:
    print("Stopping...")
finally:
    GPIO.cleanup()

Polling hay interrupt: nên chọn cách nào

Đây là câu hỏi rất hay.

Polling

Polling nghĩa là:

  • chương trình tự kiểm tra sensor liên tục theo chu kỳ

Ưu điểm:

  • dễ hiểu
  • dễ kiểm soát
  • dễ debug
  • hợp cho bài học ban đầu

Nhược điểm:

  • luôn phải loop
  • có độ trễ nhỏ theo chu kỳ sleep
  • không “reactive” bằng interrupt

Interrupt

Interrupt nghĩa là:

  • khi sensor đổi trạng thái, GPIO tự kích callback

Ưu điểm:

  • phản ứng nhanh hơn
  • đúng kiểu event-driven
  • code nhìn gọn hơn ở nhiều use case

Nhược điểm:

  • khó debug hơn với người mới
  • cần cẩn thận debounce
  • callback sai dễ gây bug khó thấy

👉 Với mini series này, bạn nên:

  • bắt đầu bằng polling
  • sau đó hiểu thêm về interrupt
  • chọn cách phù hợp khi hệ thống lớn hơn

Ví dụ dùng interrupt với cảm biến cửa

Nếu bạn muốn thử cách event-driven, có thể dùng add_event_detect.

Code ví dụ

import RPi.GPIO as GPIO
import time
from datetime import datetime

DOOR_PIN = 17

GPIO.setmode(GPIO.BCM)
GPIO.setup(DOOR_PIN, GPIO.IN, pull_up_down=GPIO.PUD_UP)

def now_str():
    return datetime.now().strftime("%Y-%m-%d %H:%M:%S")

def door_callback(channel):
    state = GPIO.input(channel)
    event = "door_open" if state == 1 else "door_closed"
    print(f"[{now_str()}] Event: {event}")

GPIO.add_event_detect(DOOR_PIN, GPIO.BOTH, callback=door_callback, bouncetime=200)

try:
    print("Door interrupt monitoring started...")
    while True:
        time.sleep(1)

except KeyboardInterrupt:
    print("Stopping...")
finally:
    GPIO.cleanup()

Ở đây:

  • GPIO.BOTH nghĩa là bắt cả hai chiều thay đổi
  • bouncetime=200 giúp giảm bounce cơ bản

Debounce là gì và vì sao cần

Khi công tắc cơ học hoặc reed switch thay đổi trạng thái, tín hiệu đôi khi không chuyển một cách “sạch” ngay lập tức. Nó có thể dao động rất nhanh trong vài mili giây, khiến chương trình hiểu nhầm là có nhiều lần đổi trạng thái.

Đó gọi là bounce.

Nếu không xử lý:

  • mở cửa một lần nhưng log 2–3 event
  • đóng cửa một lần nhưng hệ thống báo loạn

Cách đơn giản để debounce với polling

Ta có thể:

  • đọc nhiều lần liên tiếp
  • hoặc chờ một khoảng ngắn sau khi phát hiện đổi trạng thái

Ví dụ đơn giản:

if door_state != prev_door_state:
    time.sleep(0.05)
    stable_state = GPIO.input(DOOR_PIN)

    if stable_state == door_state:
        event = door_event_name(door_state)
        log_event(event)
        prev_door_state = door_state

Cách tổ chức event log hợp lý hơn

Khi hệ thống lớn dần, bạn không nên chỉ log dạng text. Tốt hơn là log theo cấu trúc dữ liệu rõ ràng.

Ví dụ:

event = {
    "ts": now_str(),
    "sensor": "door",
    "event": "door_open"
}

Hoặc:

event = {
    "ts": now_str(),
    "sensor": "pir",
    "event": "motion_detected"
}

Ở bài này, log text là đủ để bắt đầu. Nhưng từ bài sau trở đi, chúng ta sẽ cần nghĩ theo kiểu event object nhiều hơn.

Gợi ý cải tiến code thành class

Nếu bạn muốn code gọn hơn, có thể tách từng sensor thành class hoặc module riêng. Nhưng trong giai đoạn này, viết rõ ràng từng bước sẽ dễ học hơn.

Tới khi series phát triển thêm, bạn có thể tách:

project/
├── main.py
├── sensors/
│   ├── door_sensor.py
│   └── pir_sensor.py
├── logs/
│   └── event_logger.py
└── rules/
    └── engine.py

Cách này sẽ rất hữu ích ở bài sau.

Các lỗi thường gặp

Không nhận được event dù sensor đang hoạt động

Nguyên nhân có thể là:

  • sai chân GPIO
  • wiring chưa chắc
  • pull-up chưa bật cho door sensor
  • PIR chưa ổn định sau khi khởi động

Cách xử lý:

  • test lại bằng code đơn giản
  • in trực tiếp GPIO.input(pin)
  • kiểm tra lại dây nối

Event bị ghi lặp nhiều lần

Nguyên nhân:

  • polling quá nhanh
  • không so sánh với trạng thái cũ
  • bounce từ reed switch hoặc PIR

Cách xử lý:

  • lưu prev_state
  • thêm debounce
  • dùng bouncetime nếu chạy interrupt

PIR báo chuyển động quá lâu

Đây thường không phải bug code. Nhiều PIR như HC-SR501 có:

  • thời gian giữ tín hiệu
  • độ nhạy có thể chỉnh bằng biến trở

Cách xử lý:

  • chỉnh lại module PIR
  • test ở môi trường ít nhiễu
  • hiểu rằng PIR không phải sensor “chụp ảnh tức thời”, mà có độ trễ nhất định

File log không được tạo

Nguyên nhân:

  • chạy ở thư mục không có quyền ghi
  • tên file hoặc đường dẫn sai

Cách xử lý:

  • dùng đường dẫn rõ ràng
  • kiểm tra quyền thư mục
  • in exception nếu cần

Kết nối với bài tiếp theo

Đến đây, bạn đã có:

  • cảm biến cửa từ hoạt động
  • PIR hoạt động
  • Python đọc được trạng thái
  • hệ thống phát hiện được thay đổi
  • event được ghi log tại local

Đây chính là nền tảng để sang bước quan trọng tiếp theo:

👉 xây rule cảnh báo thông minh

Ở bài sau, chúng ta sẽ bắt đầu quyết định:

  • event nào chỉ log
  • event nào cần cảnh báo
  • hệ thống armed/disarmed hoạt động ra sao
  • làm sao để giảm cảnh báo giả

Kết luận

Bài 24.3 là bước chuyển từ “đọc cảm biến” sang “xử lý sự kiện”.

Thay vì chỉ nhìn GPIO là mức điện áp HIGH/LOW, chúng ta đã bắt đầu biến dữ liệu đó thành:

  • trạng thái
  • sự kiện
  • log có timestamp

Đây là nền móng rất quan trọng cho bất kỳ hệ thống giám sát nào. Một khi đã có event log ổn định, bạn có thể xây tiếp:

  • rule engine
  • notify
  • MQTT
  • dashboard
  • lưu lịch sử lâu dài