Pendahuluan
Setelah Home Assistant (#21), ESPHome (#22), dan Node-RED (#23), sekarang kita tambahkan sensor gerak PIR — komponen klasik lampu koridor, garasi, dan ruang penyimpanan.
Artikel ini fokus pada firmware ESP32: baca PIR lewat interrupt, terapkan debounce agar tidak spam event, dan hold time (hysteresis) supaya lampu tidak berkedip saat orang diam di ruangan. Status gerak + lampu dipublish ke broker Mosquitto pribadi (#16) — siap ditampilkan di HA atau Node-RED.
Prasyarat: Sudah paham MQTT dasar (#7), kontrol relay MQTT (#8), broker Mosquitto + auth (#16) jalan, dan opsional sudah integrasi Home Assistant (#21) atau Node-RED (#23).
Yang Kamu Butuhkan
- ESP32 DevKit
- Modul PIR HC-SR501 (atau setara) — output digital HIGH saat gerak
- Modul relay 1 channel (sama seperti #8)
- Lampu latihan kecil (LED / lampu desk) — bukan AC 220V tanpa pengalaman
- Arduino IDE + library PubSubClient, ArduinoJson
Estimasi biaya: Modul PIR HC-SR501 ~Rp 10–20 rb + relay yang sudah dipakai di proyek sebelumnya.
Mengapa Debounce & Hold Time?
| Masalah | Penyebab | Solusi di artikel ini |
|---|---|---|
| Lampu kedip-kedip cepat | PIR memicu berkali-kali dalam milidetik | Debounce 50 ms setelah interrupt |
| Lampu mati saat orang diam | PIR sudah LOW padahal orang masih di ruangan | Hold time (hysteresis) 60 detik sejak gerak terakhir |
| Event MQTT berlebihan | Publish di setiap ping ISR | Publish hanya saat transisi ON/OFF lampu |
Arsitektur: PIR → ESP32 → Mosquitto → Smart Home
[ HC-SR501 PIR ]
| GPIO interrupt (RISING)
v
[ ESP32 ]
| relay GPIO 26 → lampu
| publish: kodingindonesia/esp32/pir/gerak (JSON)
| subscribe: kodingindonesia/esp32/lampu/kontrol (ON/OFF/AUTO)
v
[ Mosquitto #16 ]
|
+-- Home Assistant (#21) — binary_sensor + switch
+-- Node-RED (#23) — automasi visual
Topic MQTT (konsisten Seri 1):
- Gerak:
kodingindonesia/esp32/pir/gerak— JSON{"gerak":true,"lampu":"ON"}/{"gerak":false,"lampu":"OFF"} - Kontrol manual:
kodingindonesia/esp32/lampu/kontrol—ON/OFF/AUTO(sama #8/#9)
Wiring PIR + Relay
- PIR VCC → 5V · GND → GND · OUT → GPIO 27
- Relay IN → GPIO 26 · VCC → 5V · GND → GND
- Potensiometer delay & sensitivitas di modul HC-SR501 — atur di hardware (lihat troubleshooting). Mode retrigger (H) disarankan agar output tetap HIGH selama ada gerak.
GPIO aman: Hindari GPIO 6–11 (flash). GPIO 27 cocok untuk input PIR (modul HC-SR501 output 3.3V/5V). Pin input-only murni di ESP32: GPIO 34–39. Relay tetap di GPIO 26 seperti seri sebelumnya.
Kode Lengkap: Interrupt + Debounce + Hold Time
Ganti WiFi, IP broker, dan password MQTT (sesuai #16):
#include <WiFi.h>
#include <PubSubClient.h>
#include <ArduinoJson.h>
const char* ssid = "NamaWiFiKamu";
const char* password = "PasswordWiFiKamu";
const char* mqttServer = "192.168.1.50";
const int mqttPort = 1883;
const char* mqttUser = "kindo_esp32";
const char* mqttPass = "GANTI_PASSWORD_MQTT";
const char* topicPir = "kodingindonesia/esp32/pir/gerak";
const char* topicKontrol = "kodingindonesia/esp32/lampu/kontrol";
#define PIR_PIN 27
#define RELAY_PIN 26
const bool RELAY_ON = LOW;
const bool RELAY_OFF = HIGH;
const unsigned long DEBOUNCE_MS = 50;
const unsigned long HOLD_MS = 60000; // lampu tetap nyala 60 detik setelah gerak terakhir
volatile bool pirFlag = false;
unsigned long lastIsrMs = 0;
unsigned long lastMotionMs = 0;
bool lampuMenyala = false;
bool otomasiAktif = true;
WiFiClient espClient;
PubSubClient mqttClient(espClient);
void publishStatus() {
bool gerakAktif = digitalRead(PIR_PIN) == HIGH;
StaticJsonDocument<96> doc;
doc["gerak"] = gerakAktif;
doc["lampu"] = lampuMenyala ? "ON" : "OFF";
char buffer[96];
serializeJson(doc, buffer);
if (mqttClient.publish(topicPir, buffer)) {
Serial.print("Publish ");
Serial.println(buffer);
}
}
void setLampu(bool nyala) {
if (lampuMenyala == nyala) return;
lampuMenyala = nyala;
digitalWrite(RELAY_PIN, nyala ? RELAY_ON : RELAY_OFF);
publishStatus();
Serial.println(nyala ? "Lampu: ON" : "Lampu: OFF");
}
void IRAM_ATTR pirISR() {
pirFlag = true;
}
void callbackMQTT(char* topic, byte* payload, unsigned int length) {
String pesan;
for (unsigned int i = 0; i < length; i++) {
pesan += (char)payload[i];
}
pesan.trim();
pesan.toUpperCase();
if (pesan == "ON") {
otomasiAktif = false;
setLampu(true);
} else if (pesan == "OFF") {
otomasiAktif = false;
setLampu(false);
} else if (pesan == "AUTO") {
otomasiAktif = true;
Serial.println("Mode otomasi PIR aktif kembali");
}
}
void koneksiWiFi() {
WiFi.begin(ssid, password);
while (WiFi.status() != WL_CONNECTED) {
delay(500);
Serial.print(".");
}
Serial.println("\nWiFi OK");
}
void koneksiMQTT() {
mqttClient.setServer(mqttServer, mqttPort);
mqttClient.setCallback(callbackMQTT);
mqttClient.setBufferSize(256);
while (!mqttClient.connected()) {
if (mqttClient.connect("ESP32-PIR", mqttUser, mqttPass)) {
mqttClient.subscribe(topicKontrol);
Serial.println("MQTT OK");
} else {
delay(5000);
}
}
}
void handlePirEvent() {
unsigned long now = millis();
if (now - lastIsrMs < DEBOUNCE_MS) {
// Gerak berulang cepat — tetap perpanjang hold time jika lampu sudah nyala
if (otomasiAktif && lampuMenyala) {
lastMotionMs = now;
}
return;
}
lastIsrMs = now;
lastMotionMs = now;
if (otomasiAktif && !lampuMenyala) {
setLampu(true);
}
}
void setup() {
Serial.begin(115200);
pinMode(RELAY_PIN, OUTPUT);
pinMode(PIR_PIN, INPUT);
digitalWrite(RELAY_PIN, RELAY_OFF);
attachInterrupt(digitalPinToInterrupt(PIR_PIN), pirISR, RISING);
koneksiWiFi();
koneksiMQTT();
}
void loop() {
if (WiFi.status() != WL_CONNECTED) koneksiWiFi();
if (!mqttClient.connected()) koneksiMQTT();
mqttClient.loop();
if (pirFlag) {
pirFlag = false;
handlePirEvent();
}
if (otomasiAktif && lampuMenyala) {
if (digitalRead(PIR_PIN) == HIGH) {
lastMotionMs = millis(); // pin masih HIGH — perpanjang hold tanpa interrupt baru
} else if (millis() - lastMotionMs > HOLD_MS) {
setLampu(false);
}
}
}
Penjelasan Bagian Kritis
IRAM_ATTR pirISR()— ISR sependek mungkin: hanya set flag. Jangandelay()atau MQTT di interrupt.- Debounce —
DEBOUNCE_MSdicek dihandlePirEvent(), bukan di ISR. - Hold time (hysteresis) —
lastMotionMsdiperbarui tiap gerak valid; lampu mati jika sudahHOLD_MStanpa gerak baru. Saat lampu sudah nyala, gerak berulang (meski kena debounce) tetap memperpanjanglastMotionMs. - Override manual — MQTT
ON/OFFmematikan otomasi sementara; kirimAUTOuntuk kembali ke mode PIR. - Publish hemat —
setLampu()hanya publish saat status lampu benar-benar berubah; fieldgerakdi JSON dibaca langsung dari pin PIR (digitalRead), bukan disamakan dengan status lampu. mqttClient.loop()— wajib dipanggil diloop()agar perintah MQTT manual diterima (sama seperti #8).- Polling pin PIR di
loop()— selamadigitalRead(PIR_PIN)masih HIGH,lastMotionMsdiperbarui. Ini mencegah lampu mati prematur saat orang diam tapi sensor masih mendeteksi gerak (mode retrigger HC-SR501).
Pro tip: Untuk ruang ramai (koridor sekolah), naikkan
HOLD_MSke 2–5 menit. Untuk ruang kecil, 30–60 detik biasanya cukup.
Integrasi Home Assistant (#21)
Tambahkan ke configuration.yaml (sesuaikan broker):
mqtt:
binary_sensor:
- name: "ESP32 PIR Gerak"
unique_id: esp32_pir_gerak_kindo
state_topic: "kodingindonesia/esp32/pir/gerak"
value_template: "{{ value_json.gerak }}"
payload_on: true
payload_off: false
device_class: motion
Switch lampu bisa pakai entitas yang sama seperti #21 — topic lampu/kontrol tidak berubah.
Automasi contoh — nyalakan lampu saat gerak terdeteksi (Settings → Automations → Edit in YAML):
alias: Lampu koridor nyala saat PIR
trigger:
- platform: mqtt
topic: "kodingindonesia/esp32/pir/gerak"
condition:
- condition: template
value_template: "{{ trigger.payload_json.gerak == true }}"
action:
- service: switch.turn_on
target:
entity_id: switch.lampu_esp32_relay
Untuk matikan otomatis, andalkan HOLD_MS di firmware — atau tambah automasi HA dengan for: beberapa menit tanpa gerak.
Integrasi Node-RED (#23)
- mqtt in — topic
kodingindonesia/esp32/pir/gerak - json → function — filter gerak aktif:
if (msg.payload.gerak === true) { return msg; } return null; - Sambungkan ke notifikasi, ui_text, atau mqtt out — pola wiring sama Langkah 4–6 di #23
Uji Coba (Checklist)
- Sketch mengarah ke broker pribadi Mosquitto (#16) — bukan
test.mosquitto.org - Upload sketch → Serial Monitor 115200 — WiFi & MQTT harus OK
- Tunggu warm-up PIR ~30–60 detik setelah power on (HC-SR501 stabil)
- Gerakkan tangan di depan PIR — lampu nyala + log publish JSON
- Diam 60 detik — lampu mati otomatis (sesuai
HOLD_MS) - Verifikasi dari terminal:
mosquitto_sub -h 192.168.1.50 -p 1883 \ -u kindo_esp32 -P 'PASSWORD_ANDA' \ -t "kodingindonesia/esp32/pir/gerak" -v - Override manual:
mosquitto_pub -h 192.168.1.50 -p 1883 \ -u kindo_esp32 -P 'PASSWORD_ANDA' \ -t "kodingindonesia/esp32/lampu/kontrol" -m "ON"mosquitto_pub -h 192.168.1.50 -p 1883 \ -u kindo_esp32 -P 'PASSWORD_ANDA' \ -t "kodingindonesia/esp32/lampu/kontrol" -m "OFF" - Kirim
AUTOuntuk aktifkan mode PIR lagi:mosquitto_pub -h 192.168.1.50 -p 1883 \ -u kindo_esp32 -P 'PASSWORD_ANDA' \ -t "kodingindonesia/esp32/lampu/kontrol" -m "AUTO" - Gerak berulang saat lampu nyala — hold time harus reset (lampu tidak mati prematur)
Tips & Troubleshooting
- PIR selalu HIGH: Atur potensiometer sensitivitas & delay di modul; jauhkan dari AC / angin panas langsung
- Tidak ada interrupt: Cek wiring OUT ke GPIO 27; HC-SR501 butuh warm-up ~30–60 detik setelah power on
- Lampu flicker: Naikkan
DEBOUNCE_MSatauHOLD_MS; jangan publish di ISR - MQTT tidak connect: ESP32 harus ke broker pribadi (#16) — bukan
test.mosquitto.org - Relay tidak klik: Cek active LOW/HIGH — sama troubleshooting #8
- WiFi 2.4 GHz: ESP32 tidak support jaringan 5 GHz saja
Keamanan & Produksi
- Jangan hardcode password MQTT di sketch yang di-share — gunakan build flag atau NVS (#12)
- Sensor gerak + lampu di area publik — pertimbangkan notifikasi HA, bukan hanya lampu lokal
- MQTT over internet → wajib TLS (#17)
Langkah Selanjutnya (Seri 2)
- Artikel #17: MQTT TLS — amankan Mosquitto
- Artikel #18: Simpan histori event PIR ke MySQL via subscriber Python
- Artikel #34: NTP — timestamp akurat di log gerak
- Capstone greenhouse (#39) — PIR + sensor + pompa relay
Sensor PIR melengkapi Jalur C smart home: dari dashboard HA/Node-RED hingga automasi gerak di firmware ESP32. Lanjutkan di halaman artikel Koding Indonesia.