Ở 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_opendoor_closedmotion_detectedmotion_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.BOTHnghĩa là bắt cả hai chiều thay đổibouncetime=200giú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
bouncetimenế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


