Tutorial Lengkap Sistem IoT End-to-End
ESP32 + Cirkit Designer + MQTT over WSS + Eclipse Mosquitto + Node-RED + InfluxDB
Dikembangkan dari prototipe internal Teknolab (ESP32, simulasi dan pencatatan pengujian), kemudian diperbaiki melalui audit teknis serta pengujian lokal yang terdokumentasi.
Peta baca dan cara menggunakan modul
Dokumen ini merupakan panduan implementasi sistem IoT lengkap. Ikuti urutannya. Jangan meloncat dari wiring langsung ke dashboard, karena setiap lapisan memiliki tes lulus sebelum lanjut.
| Tahap | Bab | Hasil yang harus berhasil |
|---|---|---|
| 1. Persiapan | 1–3 | Asumsi, perangkat, sistem operasi dan pemetaan pin jelas |
| 2. Perangkat lokal | 4–7 | DHT, potensiometer, 2 tombol, 3 LED, LCD dan state berjalan |
| 3. Internet/MQTT | 8–11 | Mosquitto WSS, sertifikat, MQTTX, ESP32 publish JSON valid |
| 4. Backend | 12–14 | Node-RED memvalidasi, InfluxDB menyimpan, dashboard menampilkan |
| 5. Pengujian | 15–18 | 12 tes penerimaan, fail-safe, formulir dokumentasi teknis, laporan verifikasi |
Status audit & bukti pengujian yang dilakukan
| Jenis tes | Hasil | Batas bukti |
|---|---|---|
| 39 assertion logika C++ | PASS | Status, prioritas, ACK, debounce, overflow millis, LED |
| 77 assertion flow Node-RED (versi Teknolab) | PASS | Validator JSON, routing 2 sumber, field/tags InfluxDB |
| 15 pemeriksaan server statis | PASS | Compose YAML, ACL, listener, CA template |
| Kompilasi mock C++ sketsa | PASS | Sintaks & generasi JSON dengan API tiruan, BUKAN Arduino CLI |
| Arduino-ESP32/Cirkit asli | BELUM | Tidak tersedia toolchain/perangkat & akses simulator nyata untuk pengujian ini |
| Docker Mosquitto/Influx/Node-RED berjalan | BELUM | Docker dan broker tidak tersedia di lingkungan audit |
| Koneksi WSS end-to-end fisik | BELUM | Membutuhkan domain, sertifikat, jaringan dan ESP32 riil |
Hasil tes otomatis di atas tersimpan pada paket pendamping: tests/HASIL_TEST.txt. PASS statis/mock tidak berarti klaim bahwa seluruh sistem sudah lolos uji penerimaan fisik.
1. Audit prototipe awal Teknolab
File dasar yang diperiksa: TESS1.ino, screenshot Cirkit Designer, konfigurasi MQTTX, potongan flow Node-RED, konfigurasi broker EMQX, serta catatan konfigurasi dan pengujian. Tujuan audit adalah mempertahankan fungsi yang berhasil dan memperbaiki titik lemah yang terdeteksi.
| Temuan pada starting point | Dampak | Tindakan di revisi |
|---|---|---|
| PubSubClient + WiFiClient ke port 1883 | MQTT TCP biasa, bukan WSS | Gunakan PsychicMqttClient dengan URI wss:// dan validasi CA |
| Topic publish dari kode HW menuju jalur SIM | Data tertukar pada dashboard | Identitas TEKNOLAB_HW/SIM dan path hw/sim dipisah |
| peilud() dipanggil sebelum state dan ADC diperbarui | Payload dapat memakai status/limit sebelumnya | Urutan loop: baca -> evaluasi -> output -> publish |
| Tombol memakai while menunggu rilis | Loop terhenti jika tombol ditahan | Debounce berbasis millis; satu press satu event |
| Status dan ACK diatur berbagai fungsi | Potensi bug ketika threshold/status berubah | Satu kelas Alarm dengan prioritas teruji |
| Kode mengandung kredensial jaringan | Risiko kebocoran akses | Secrets.h dengan placeholder; ganti password lama |
| Gambar Node-RED menunjukkan real-time | Bukan bukti histori | Uji InfluxDB Data Explorer/Flux dan chart histori |
| formulir dokumentasi teknis mencatat pot GPIO32; program GPIO34 | Wiring tidak konsisten jika diikuti mentah | Gunakan profil pin jelas dan verifikasi pin di board |
2. Kontrak sistem dan batas fungsional
Satu laboratorium dipantau dua implementasi: hardware asli dan simulasi Cirkit. Dua klien mengirim telemetry independen, melalui WSS dengan JSON yang memiliki field sama. Node-RED tidak mengubah aturan alarm lokal; Node-RED hanya memvalidasi, menyimpan, dan menampilkan.
2.1 Aturan status sistem
| Prioritas | State | Kondisi | LED sebelum ACK | Sesudah ACK |
|---|---|---|---|---|
| 1 | SENSOR_ERROR | DHT gagal atau angka invalid | Kuning + merah kedip 250 ms | ACK diabaikan |
| 2 | CRITICAL | T ≥ limit + 3 °C ATAU RH ≥ 85 % | Merah kedip 250 ms | Merah stabil |
| 3 | WARNING | T ≥ limit ATAU RH ≥ 75 %, bukan critical | Kuning kedip 500 ms | Kuning stabil |
| 4 | NORMAL | Selain kondisi di atas | Hijau stabil | ACK = false |
ACK di-reset saat berubah ke status lain, termasuk WARNING→CRITICAL, dan jika NORMAL/ERROR. Ini adalah kebijakan latihan, bukan hysteresis. Nilai T_limit berasal dari potensiometer 20–35 °C. Sensor gagal: kirim null/null, sensor_ok=false, status=SENSOR_ERROR.
2.2 Jadwal fungsi
| Aktivitas | Periode | Alasan |
|---|---|---|
| Baca DHT11 | ≥2.000 ms | DHT11 tidak boleh dibaca berulang tanpa interval |
| Baca pot/ADC | 150 ms | Ambang cepat berubah |
| Refresh LCD | 500 ms | Tidak melakukan lcd.clear() tiap loop |
| Publish MQTT | 5.000 ms | Kontrak telemetry, sequence naik saat pesan diantrikan |
| Tombol | Tiap loop + debounce 30 ms | Tidak memakai delay dan while tahan tombol |
| Alarm lokal | Tiap loop | Tetap aktif saat broker/Wi-Fi OFFLINE |
3. Daftar alat, perangkat lunak dan data yang harus disiapkan
•Hardware: ESP32 Development Board sesuai profil, DHT11, LCD 16×2 I2C, 2 push button, 3 LED, 1 potensiometer/trimpot, breadboard, kabel jumper, resistor LED 220–330 Ω dan kabel USB DATA.
•Laptop operator: Arduino IDE, ESP32 Boards Manager, library DHT (Adafruit), library LiquidCrystal_I2C yang cocok, PsychicMqttClient, driver USB sesuai CP210x/CH340.
•Server Linux: Docker Engine + Compose plugin, domain/subdomain dengan DNS A record, sertifikat TLS dari CA publik, port TCP 8084 dapat diakses klien.
•MQTTX (GUI) sebagai alat verifikasi, bukan aplikasi backend wajib.
•Node-RED + InfluxDB; Node-RED Dashboard v1 hanya untuk mereplikasi screenshot trial, dengan catatan legacy/deprecated.
•Kredensial yang diperlukan: akun teknolab, akun monitor (read-only), dan token InfluxDB. Jangan menyebarkan kata sandi atau token.
3.1 Nilai konfigurasi yang harus sudah diketahui sebelum mulai
| Parameter | Contoh/Isian operator | Hasil cek |
|---|---|---|
| FQDN broker WSS | broker-latihan.DOMAIN-ANDA | DNS resolve ke IP server |
| Listener publik | TCP 8084 + TLS | WSS handshake dan CONNECT berhasil |
| Path Mosquitto langsung | / (URI wss://host:8084/) | JANGAN pakai /mqtt hanya karena contoh EMQX |
| Server Node-RED | localhost:1880 (SSH tunnel) | Editor dapat dibuka |
| Server InfluxDB | localhost:8086 (SSH tunnel) | UI Data Explorer dapat dibuka |
| Wi-Fi fisik | SSID 2,4 GHz; password valid | ESP32 mendapat IP |
| Wi-Fi simulasi | CirkitWifi open, jika simulator mendukung | Wi-Fi simulator connected |
4. Wiring hardware fisik — langkah terukur
4.1 Pin yang digunakan pada ESP32-WROOM (fisik)
| Komponen | Koneksi ESP32-WROOM | Catatan pemeriksaan |
|---|---|---|
| DHT11 DATA | GPIO4 | VCC sesuai modul; GND bersama; pull-up jika DHT bare |
| LCD SDA/SCL | GPIO21 / GPIO22 | Backpack I2C 0x27; lihat risiko pull-up 5 V |
| PB1 / PB2 | GPIO18 / GPIO19 | Satu kaki ke GPIO, kaki lain ke GND, INPUT_PULLUP |
| LED hijau/kuning/merah | GPIO25 / 26 / 27 | Resistor seri 220–330 Ω di tiap LED |
| Pot tengah/wiper | GPIO34 ADC1 | Dua ujung pot: 3.3 V & GND, bukan 5 V |
| GND semua modul | GND ESP32 | Wajib common ground |
4.2 Profil alternatif ESP32-S3 untuk simulasi
| Sinyal | GPIO contoh pada S3 | Tindakan wajib |
|---|---|---|
| DHT11 | 4 | Cocokkan dengan pinout board S3 yang tersedia di Cirkit |
| LCD SDA / SCL | 11 / 12 | Periksa I2C wiring simulator |
| PB1 / PB2 | 8 / 9 | INPUT_PULLUP dan GND |
| LED G / Y / R | 15 / 16 / 17 | Resistor seri tiap LED |
| Pot ADC | 1 | GPIO1 merupakan jalur ADC pada S3; bukan GPIO34 |
Profil S3 di atas merupakan rancangan pemetaan contoh yang tetap harus dicocokkan dengan pin fisik yang diekspos oleh komponen Cirkit. Source firmware memilih profil berdasarkan target kompilasi CONFIG_IDF_TARGET_ESP32S3.
4.3 Urutan perakitan yang aman
4.4 Pemeriksaan awal perangkat lokal
| Tes | Hasil lulus | Jika gagal |
|---|---|---|
| USB/COM | Port baru terlihat di Arduino IDE | Ganti kabel data/driver, jangan mengubah program dulu |
| DHT11 | Suhu dan RH valid setelah ≥2 detik | Cek VCC, pin data, pull-up dan tipe sensor |
| Pot | Limit berubah 20–35 saat diputar | Cek wiper, 3.3 V, GPIO dan ADC1 |
| LED | NORMAL hijau stabil | Cek resistor seri, polaritas, pin mapping |
| PB1 | Satu tekan -> satu pergantian halaman | Cek arah kaki tombol dan debounce |
| PB2 | WARNING/CRITICAL -> ACK stabil | ACK tidak boleh mengubah state sebenarnya |
| LCD | Dua baris tidak flicker | Cek 0x27 vs 0x3F, SDA/SCL dan catu daya |
5. Simulasi Cirkit Designer — dari proyek kosong
WiFi.begin("CirkitWifi", "") mendapat status connected jika didukung.Catatan platform: situs Cirkit mendokumentasikan kemampuan Wi-Fi, MQTT, WebSocket dan Arduino sketch pada ESP32-S3, tetapi ini bukan bukti bahwa setiap library pihak ketiga termasuk PsychicMqttClient pasti dapat dijalankan di setiap versi simulaturnya. Tahap kompilasi native Cirkit adalah pengujian wajib.
6. Memahami firmware hasil revisi
Kode revisi di paket berisi SmartLab_Teknolab.ino + SmartLabLogic.h + Secrets.h. Berkas Secrets.h dibuat dari Secrets.template.h. Kode tidak memakai ArduinoJson karena struktur field tetap dan dapat disusun menggunakan snprintf dengan tipe angka/boolean/null yang dijaga; Node-RED memvalidasi JSON kembali.
6.1 Urutan fungsi loop yang benar
void loop() {
uint32_t now=millis();
updateLimit(now); // baca trimpot
updateSensor(now); // baca DHT11 setiap >= 2s
alarmControl.update(sensorOk,temperatureC,humidityPct,limitC,now);
handleButtons(now); // debounce 30ms
updateLeds(now); // alarm lokal
updateLcd(now); // refresh ~500ms
updateNetwork(now); // WSS & WiFi
publishTelemetry(now); // publish ~5s
}Perhatikan pembacaan pot dan sensor dilakukan sebelum evaluasi state. Ini menghindari satu periode telemetry memakai batas/status lama yang menjadi salah satu risiko TESS1.ino.
6.2 Fungsi kontrol yang telah diuji
•State priority: SENSOR_ERROR > CRITICAL > WARNING > NORMAL.
•ACK hanya aktif di WARNING/CRITICAL, reset ketika transisi atau error, tidak mematikan pembacaan sensor.
•LED berkedip berdasarkan selisih millis unsigned, tetap benar pada overflow counter.
•Debounce membaca perubahan HIGH→LOW satu kali per tekan, tanpa while/delay.
•Pemetaan ADC 0–4095 ke 20–35 dibatasi agar tidak lewat range.
•Topic HW berbeda dari SIM dan device_id mengikuti identitas proyek serta sumber.
7. Persiapan Arduino IDE dan firmware
7.1 Makna hasil publish()
Pada client asynchronous, ID hasil publish bukan bukti pesan sudah masuk InfluxDB. Sukses minimal: pesan terlihat pada MQTTX; sukses backend: node Debug menerima objek JSON; sukses histori: titik terlihat di InfluxDB; sukses dashboard: widget berubah. Keempatnya harus diverifikasi terpisah.
8. MQTT over WSS: transport dan keamanan
| Istilah | Transport | Contoh |
|---|---|---|
| MQTT TCP | MQTT langsung di TCP | mqtt://host:1883 (tidak memenuhi syarat HW/SIM WSS) |
| MQTT TLS | MQTT di TLS langsung | mqtts://host:8883 (bukan WebSocket) |
| MQTT over WSS | WebSocket secure + MQTT + TLS | wss://FQDN:8084/ untuk Mosquitto direct |
| WebSocket EMQX publik | WSS dengan path provider | wss://broker.emqx.io:8084/mqtt (contoh trial, bukan Mosquitto lokal) |
8.1 Sertifikat & waktu sistem
WSS membutuhkan hostname dan sertifikat TLS yang cocok. FQDN dalam sertifikat harus sama dengan URI; sertifikat harus belum kedaluwarsa; RTC/NTP pada ESP32 harus mengetahui waktu cukup akurat. Program menunggu epoch masuk akal sebelum connect. Root CA template adalah ISRG Root X1, berlaku hanya bila chain server ditandatangani CA tersebut.
Jangan menyiasati masalah dengan setInsecure() atau mematikan SSL Secure pada MQTTX. Jika root salah, ambil chain CA yang benar dari penyedia sertifikat dan uji kembali.
8.2 Alur lapisan komunikasi (bukan hanya mengganti port)
ESP32 → Wi-Fi 2,4 GHz → DNS FQDN → koneksi TCP port 8084 → TLS handshake & verifikasi sertifikat → WebSocket Upgrade → MQTT CONNECT → MQTT PUBLISH JSON. Gagal di satu lapisan menyebabkan payload tidak sampai, walaupun LED lokal tetap menyala.
9. Instal Mosquitto WSS + Node-RED + InfluxDB tanpa CloudPanel
9.1 Persiapan DNS dan firewall
docker compose plugin sudah tersedia.docker compose config, lalu docker compose up -d. Periksa status dan log sampai tidak ada error fatal.docker version
docker compose version
cp .env.example .env
chmod 600 .env
# EDIT .env terlebih dahulu; jangan pakai placeholder!
docker compose config
docker compose up -d
docker compose ps
docker compose logs --tail 100 mosquitto9.2 Listener broker dan ACL
Perhatikan dua listener Mosquitto. Port 1883 hanya berada dalam jaringan Docker dan tidak dipublikasi ke host. Klien hardware dan simulasi memakai WSS 8084. Node-RED boleh menjadi subscriber internal MQTT TCP 1883 karena kedua pengirim HW dan SIM menggunakan WSS sesuai rancangan.
# Mosquitto 2.x: listener TCP hanya di jaringan Docker (tidak dipublish ke host).
listener 1883 0.0.0.0
protocol mqtt
# Listener WebSocket Secure: TLS selesai di Mosquitto, bukan NGINX/CloudPanel.
# Endpoint untuk klien WSS: wss://DNS_ANDA:8084/
listener 8084 0.0.0.0
protocol websockets
certfile /mosquitto/certs/fullchain.pem
keyfile /mosquitto/certs/privkey.pem
allow_anonymous false
password_file /mosquitto/config/passwords
acl_file /mosquitto/config/acl
persistence true
persistence_location /mosquitto/data/
log_dest stdout
connection_messages true# Satu akun publikasi untuk proyek Teknolab.
user teknolab
topic readwrite smartlab/teknolab/#
# Node-RED memantau seluruh jalur proyek tanpa hak publish.
user monitor
topic read smartlab/teknolab/#9.3 Docker Compose
Paket pendamping mencakup docker-compose.yml, mosquitto.conf, acl, template .env dan contoh panduan izin file. Container Node-RED dan InfluxDB di-bind ke 127.0.0.1 sehingga hanya dapat diakses secara langsung dari server atau SSH tunnel.
# Paket latihan internal - tidak memakai CloudPanel.
# WSS publik 8084, Node-RED dan InfluxDB hanya loopback server.
services:
mosquitto:
image: eclipse-mosquitto:2
restart: unless-stopped
ports:
- '8084:8084/tcp'
volumes:
- ./mosquitto/config:/mosquitto/config:ro
- ./mosquitto/certs:/mosquitto/certs:ro
- mosquitto_data:/mosquitto/data
networks: [iotnet]
influxdb:
image: influxdb:2.7
restart: unless-stopped
ports:
- '127.0.0.1:8086:8086/tcp'
environment:
DOCKER_INFLUXDB_INIT_MODE: setup
DOCKER_INFLUXDB_INIT_USERNAME: ${INFLUX_USER:?fill env}
DOCKER_INFLUXDB_INIT_PASSWORD: ${INFLUX_PASSWORD:?fill env}
DOCKER_INFLUXDB_INIT_ORG: ${INFLUX_ORG:?fill env}
DOCKER_INFLUXDB_INIT_BUCKET: ${INFLUX_BUCKET:?fill env}
DOCKER_INFLUXDB_INIT_ADMIN_TOKEN: ${INFLUX_ADMIN_TOKEN:?fill env}
volumes:
- influxdb_data:/var/lib/influxdb2
- influxdb_config:/etc/influxdb2
networks: [iotnet]
nodered:
image: nodered/node-red:4.1
restart: unless-stopped
ports:
- '127.0.0.1:1880:1880/tcp'
volumes:
- nodered_data:/data
depends_on: [mosquitto, influxdb]
networks: [iotnet]
volumes:
mosquitto_data:
influxdb_data:
influxdb_config:
nodered_data:
networks:
iotnet:9.4 Kendala sertifikat yang harus diuji
| Gejala | Penyebab sering | Tindakan terarah |
|---|---|---|
| Mosquitto restart loop | PEM tidak terbaca oleh UID service | Periksa docker logs, chown/chmod file cert |
| TLS error “unknown CA” | Root CA tidak cocok dengan chain | Periksa issuer dan gunakan root CA yang tepat |
| TLS error “hostname mismatch” | Terhubung dengan IP, SAN hanya FQDN | Gunakan FQDN yang tercantum dalam sertifikat |
| TLS handshake gagal / timeout | Firewall atau NAT port 8084 | Periksa TCP reachability, bukan ganti topic |
| MQTT auth refused | Username/password atau ACL salah | Tes MQTTX dengan kredensial proyek |
10. Verifikasi broker menggunakan MQTTX (sebelum ESP32)
smartlab/teknolab/# QoS 0. Publish satu JSON contoh ke smartlab/teknolab/hw/telemetry untuk menguji broker/Node-RED.{
"device_id":"TEKNOLAB_HW", "source":"hardware",
"temperature_c":28.0, "humidity_pct":67.0,
"threshold_temp_c":26, "status":"WARNING",
"ack":false, "sensor_ok":true,
"uptime_ms":120000, "sequence":24
}11. Menghubungkan firmware fisik serta simulasi
smartlab/teknolab/+/telemetry; jalankan kedua pengirim bergantian dahulu, lalu bersamaan.11.1 Source pemrograman MQTT
Pemakaian PsychicMqttClient mengikuti dokumentasi API resminya: setServer mendukung wss://, setCACert, setCredentials, setClientId, setAutoReconnect, setWill, connect, connected, publish. Library menyatakan dukungan Arduino core 2/3 dan semua varian ESP32, tetapi pengembang menyatakan Arduino IDE tidak diuji aktif dan Cirkit tidak disebut sebagai target yang telah diverifikasi. Karena itu tahap kompilasi native tidak boleh dilewati.
12. Node-RED: menerima, memvalidasi, memisahkan dan mengirim data
12.1 Mengakses editor
12.2 Struktur flow yang disarankan
MQTT IN (smartlab/teknolab/+/telemetry)
-> JSON (always output object)
-> Function: Validate Topic + Schema
-> Switch: teknolab/hw | teknolab/sim
-> Function 5 output:
1 suhu -> gauge
2 kelembapan -> gauge
3 threshold -> gauge
4 status/ACK -> text
5 [fields, tags] -> InfluxDB out (measurement smart_lab)
-> InfluxDB in (Flux query) -> function format -> ui_chart historisNode JSON harus disetel “always output object”, bukan toggle. Jika input sudah object lalu node JSON men-stringify kembali, validator yang mengharapkan objek akan menolak pesan.
12.3 Pengendalian kualitas data sebelum tersimpan
Validator mengecek path topic, 10 field, tipe boolean, angka, device_id sesuai topic, source sesuai HW/SIM, status hasil perhitungan terhadap suhu/kelembapan dan limit, serta error null/null. Telemetry salah tidak diteruskan ke dashboard atau InfluxDB. Ini mencegah “dashboard terlihat normal” meskipun payload salah.
// Node-RED Function, 1 output. Tempatkan setelah node JSON (action: always convert to object).
const m = /^smartlab\/(teknolab)\/(hw|sim)\/telemetry$/.exec(msg.topic || '');
let d = msg.payload;
if (!m || !d || typeof d !== 'object' || Array.isArray(d)) {
node.warn('Format topik / JSON bukan object'); return null;
}
const keys = ['device_id','source','temperature_c','humidity_pct',
'threshold_temp_c','status','ack','sensor_ok','uptime_ms','sequence'];
if (!keys.every(k => Object.prototype.hasOwnProperty.call(d,k))) {
node.warn('JSON kehilangan field wajib'); return null;
}
const expectedId = m[1].toUpperCase()+'_'+m[2].toUpperCase();
const expectedSource = m[2] === 'hw' ? 'hardware' : 'software';
if (d.device_id !== expectedId || d.source !== expectedSource) {
node.warn('Mismatch antara topic dan identitas pada JSON'); return null;
}
if (typeof d.ack !== 'boolean' || typeof d.sensor_ok !== 'boolean' ||
!Number.isInteger(d.threshold_temp_c) || d.threshold_temp_c < 20 || d.threshold_temp_c > 35 ||
!Number.isInteger(d.uptime_ms) || d.uptime_ms < 0 ||
!Number.isInteger(d.sequence) || d.sequence < 1) {
node.warn('Tipe atau rentang parameter tidak benar'); return null;
}
const valid = d.sensor_ok;
if (valid) {
if (!Number.isFinite(d.temperature_c) || !Number.isFinite(d.humidity_pct) ||
d.temperature_c < -40 || d.temperature_c > 80 ||
d.humidity_pct < 0 || d.humidity_pct > 100) {
node.warn('Sensor dinyatakan OK tapi angka tidak valid'); return null;
}
} else if (d.temperature_c !== null || d.humidity_pct !== null || d.ack) {
node.warn('SENSOR_ERROR wajib null/null, ack=false'); return null;
}
const expectedStatus = !valid ? 'SENSOR_ERROR' :
(d.temperature_c >= d.threshold_temp_c+3 || d.humidity_pct>=85 ? 'CRITICAL' :
(d.temperature_c >= d.threshold_temp_c || d.humidity_pct>=75 ? 'WARNING' : 'NORMAL'));
if (d.status !== expectedStatus || ((d.status==='NORMAL'||d.status==='SENSOR_ERROR')&&d.ack)) {
node.warn('Status / ACK tidak mengikuti prioritas kasus'); return null;
}
msg.team = m[1];
msg.src = m[2];
msg.payload = d;
return msg;// Node-RED Function (5 outputs): suhu, RH, limit, status, titik InfluxDB.
const d=msg.payload;
const temp=d.sensor_ok?{topic:msg.topic,payload:d.temperature_c}:null;
const hum=d.sensor_ok?{topic:msg.topic,payload:d.humidity_pct}:null;
const threshold={topic:msg.topic,payload:d.threshold_temp_c};
const status={topic:msg.topic,payload:d.status+' | ACK:'+(d.ack?'YA':'TIDAK')};
const fields={status:d.status,ack:d.ack,sensor_ok:d.sensor_ok,
threshold_temp_c:d.threshold_temp_c,uptime_ms:Number(d.uptime_ms),
sequence:Number(d.sequence)};
if(d.sensor_ok){fields.temperature_c=d.temperature_c;fields.humidity_pct=d.humidity_pct;}
const tags={team:msg.team,source:msg.src,device_id:d.device_id};
const influx={payload:[fields,tags]};
return [temp,hum,threshold,status,influx];13. InfluxDB 2.x dan line protocol
Data dari ESP32 tetap dikirim sebagai JSON lewat MQTT WSS. Node-RED mengubah objek menjadi measurement + tags + fields yang ditulis ke InfluxDB. Ini bukan berarti perangkat harus langsung mengirim teks Line Protocol ke MQTT.
| Bagian | Nilai yang dipakai | Fungsi |
|---|---|---|
| Bucket | teknolab_lab | Kumpulan data histori |
| Measurement | smart_lab | Jenis pengukuran |
| Tags | team, source, device_id | Filter dua jalur perangkat: hardware dan simulasi |
| Fields | temperature_c, humidity_pct, status, ack, threshold, uptime, sequence | Nilai data |
| Timestamp | Waktu tulis server | Untuk grafik historis; bisa diperluas menjadi waktu capture |
smart_lab,team=teknolab,source=hw,device_id=TEKNOLAB_HW temperature_c=28.0,humidity_pct=67.0,threshold_temp_c=26i,status="WARNING",ack=false,sensor_ok=true13.1 Query data di Data Explorer
from(bucket:"teknolab_lab")
|> range(start:-1h)
|> filter(fn:(r)=>r._measurement=="smart_lab")
|> filter(fn:(r)=>r._field=="temperature_c")
|> filter(fn:(r)=>r.team=="teknolab" and r.source=="hw")
|> sort(columns:["_time"])14. Dashboard Node-RED: real-time dan grafik histori
Untuk mengikuti contoh dashboard latihan, flow memakai ui_gauge, ui_text, dan ui_chart dari node-red-dashboard v1. Paket ini sudah deprecated; untuk sistem permanen disarankan migrasi ke FlowFuse Dashboard 2, tetapi format node tidak kompatibel satu-banding-satu.
| Widget | Sumber | Format output |
|---|---|---|
| Suhu | Function output 1 | msg.payload = Number temperatur |
| Kelembapan | Function output 2 | msg.payload = Number RH |
| T Limit | Function output 3 | msg.payload = angka 20–35 |
| Status/ACK | Function output 4 | msg.payload = teks ringkas |
| Grafik historis | InfluxDB in → format history → ui_chart | Time series benar-benar berasal dari database |
Pisahkan dua panel: TEKNOLAB/HW dan TEKNOLAB/SIM. Client fisik dan simulasi harus berbeda device_id dan topic; jangan membuat satu gauge menerima seluruh pesan wildcard tanpa pemisahan, karena nilai dapat saling menimpa.
15. Uji penerimaan — memastikan semua fungsi bekerja
Berikut adalah 12 skenario pengujian yang digunakan untuk memverifikasi spesifikasi sistem. Pada simulasi, nilai sensor dapat diatur untuk menghasilkan trigger. Pada hardware, gunakan nilai asli dan ubah limit secara aman; jangan memalsukan data sensor agar cocok tabel.
| ID | Trigger (contoh) | Hasil WAJIB |
|---|---|---|
| T01 | T=25, RH=60, limit=28 | NORMAL, LED hijau stabil |
| T02 | T=28, RH=60, limit=28 | WARNING, LED kuning kedip |
| T03 | T=31, RH=60, limit=28 | CRITICAL, LED merah kedip |
| T04 | T=25, RH=76, limit=28 | WARNING karena kelembapan |
| T05 | T=25, RH=86, limit=28 | CRITICAL karena kelembapan |
| T06 | T=31, RH=76, limit=28 | CRITICAL menang terhadap WARNING |
| T07 | Tekan PB1 satu kali | LCD pindah halaman sekali, tanpa ganda |
| T08 | Tekan PB2 saat WARNING | ack=true, status tetap WARNING, kuning stabil |
| T09 | Naik WARNING → CRITICAL | ACK reset false, merah berkedip |
| T10 | Putus sensor DHT11 | SENSOR_ERROR, dua LED berkedip, JSON null/null |
| T11 | WSS off 30 s, kemudian pulih | Sensor/LED/LCD tetap; koneksi dan publish kembali |
| T12 | HW + SIM bersamaan | Dua client/topic berbeda; JSON valid, InfluxDB & dashboard terpisah |
15.1 Catatan bukti untuk setiap tes
16. Troubleshooting berdasarkan gejala, bukan coba-coba
| Gejala | Lakukan pemeriksaan pertama | Tindakan tepat |
|---|---|---|
| Arduino IDE: file PsychicMqttClient.h not found | Library path / Library Manager | Pasang library resmi versi cocok sebelum mengubah kode |
| Compile gagal di Cirkit | Board target, library custom di simulator | Uji kemampuan library; jangan otomatis klaim kompatibel |
| DHT menghasilkan NaN | VCC/DATA/GND, interval 2 detik, pull-up | Uji DHT tanpa jaringan terlebih dahulu |
| LCD blank | Alamat 0x27/0x3F, I2C SDA SCL | I2C scanner dan cek level listrik |
| Pot tidak berubah | GPIO pot pada WROOM vs S3 | Perbaiki mapping; ukur wiper 0–3.3 V |
| LED salah status | Prioritas CRITICAL/WARNING | Lihat uji T01–T06 dan logika SmartLabLogic.h |
| PB double-trigger | Wiring dan debounce | Satu input pull-up; edge 30 ms |
| Wi-Fi gagal | SSID 2.4 GHz + password / CirkitWifi | Jalankan contoh Wi-Fi minimal |
| WSS timeout | DNS, port 8084, NAT, firewall | Tes dari MQTTX dan periksa docker logs |
| WSS cert invalid | FQDN SAN, root CA dan jam NTP | Perbaiki chain CA, jangan matikan TLS verify |
| MQTT MQTTX sukses, Node-RED kosong | Broker internal/ACL topic | Cek MQTT node connected + Debug |
| Node-RED gauge kosong | msg.payload salah tipe / validator reject | Periksa node JSON action obj dan Function Debug |
| InfluxDB kosong | URL, token, org, bucket, storage write | Cek Data Explorer dan node out error |
| Graph flat saja | Cache chart lama, query salah filter | Konfirmasi query membaca Influx, bukan inject contoh |
17. Formulir dokumentasi implementasi
| Identitas | Isi |
|---|---|
| Identitas perangkat | ☐ TEKNOLAB_HW ☐ TEKNOLAB_SIM |
| Pengelola proyek | ________________________________________ |
| Pemeriksaan sistem | ____________ / ____________ |
| URL projek Cirkit publik | ________________________________________ |
| Pin sinyal | HW GPIO | SIM GPIO | Catatan |
|---|---|---|---|
| DHT11 | _______ | _______ | _________________ |
| LCD SDA/SCL | ___ / ___ | ___ / ___ | _________________ |
| PB1/PB2 | ___ / ___ | ___ / ___ | _________________ |
| LED Hijau/Kuning/Merah | ___ / ___ / ___ | ___ / ___ / ___ | _________________ |
| Potensiometer ADC | _______ | _______ | _________________ |
| MQTT/JSON | HW | SIM |
|---|---|---|
| WSS URL | ________________________ | ________________________ |
| Client ID | ________________________ | ________________________ |
| Telemetry topic | ________________________ | ________________________ |
| Device ID | ________________________ | ________________________ |
| QoS / period | 0 / 5 detik | 0 / 5 detik |
Payload JSON yang harus dicentang: ☐ device_id ☐ source ☐ temperature_c ☐ humidity_pct ☐ threshold_temp_c ☐ status ☐ ack ☐ sensor_ok ☐ uptime_ms ☐ sequence
Bukti WSS connect: __________________ | Bukti InfluxDB: __________________ | Dashboard URL: __________________
Tanda tangan anggota: __________________ / __________________ Jam penyerahan: __________
18. Alur kerja paralel hardware dan simulasi
| Tahap | Perangkat fisik (hardware) | Simulasi Cirkit Designer |
|---|---|---|
| Fase 1 | Sepakati GPIO, state, JSON dan pembagian tugas | Sepakati kontrak; buat project kosong |
| Fase 2 | Wiring aman + pemeriksaan 3.3V | Skematik virtual + kecocokan pin board |
| Fase 3 | DHT/ADC/LCD/LED/PB lokal | Uji sensor/LED/LCD/PB dalam simulator |
| Fase 4 | State machine, ACK, debounce | State, ACK, debounce setara |
| Fase 5 | WSS hardware + JSON valid | WSS simulasi + JSON valid |
| Fase 6 | Offline/reconnect, cek MQTTX | Cek 4 panel, Influx & graph |
| Fase 7 | Pemeriksaan akhir sensor dan wiring | Isi dokumentasi dan salin URL simulasi |
| Fase 8 | Simpan dan beri versi kode yang sudah diuji | Pemeriksaan kelengkapan dokumentasi |
Urutan kerja berikut merupakan contoh alur implementasi yang dapat disesuaikan dengan kebutuhan. Hardware dan simulasi dikerjakan paralel, tetapi masing-masing perlu lulus tes yang sama.
19. Laporan hasil audit engineering (apa yang telah dan belum diuji)
Pengujian otomatis terhadap paket dilakukan di lingkungan Linux dengan g++ C++17, Node.js 22, Python 3/PyYAML dan OpenSSL. Docker, Arduino CLI, ESP32 fisik, Cirkit Designer dan broker WSS nyata tidak tersedia pada lingkungan penulis.
| ID bukti | Pengujian | Hasil aktual |
|---|---|---|
| AUTO-C++ | 39 kontrol state/ACK/debounce/rollover | PASS |
| AUTO-JS | 139 validator/topic/JSON/Node-RED flow | PASS |
| AUTO-SERVER | 15 static YAML, ACL dan cert template | PASS |
| MOCK-SKETCH | Kompilasi C++ dengan stub API + snprintf JSON | PASS, bukan compiler ESP32 asli |
| TLS PEM | OpenSSL parse Root CA ISRG Root X1 | PASS, root berlaku hingga 2035 |
| ESP32 COMPILE | Arduino Core + Psychic + LCD + DHT | BELUM DIUJI NYATA |
| CIRKIT RUN | Library WSS & GPIO sesuai simulator | BELUM DIUJI NYATA |
| MOSQUITTO LIVE | Handshake WSS dan ACL nyata | BELUM DIUJI NYATA |
| INFLUX LIVE | Histori masuk lewat Node-RED | BELUM DIUJI NYATA |
20. Referensi teknis dan batas kompatibilitas
•PsychicMqttClient API dan metode WSS/TLS: https://github.com/theelims/PsychicMqttClient/blob/main/documentation.md
•ESP-MQTT (Espressif, WS dan WSS): https://docs.espressif.com/projects/esp-mqtt/en/latest/esp32/
•Cirkit ESP32-S3 simulation: https://www.cirkitdesigner.com/docs/platforms/esp32-s3
•Cirkit Wi-Fi virtual: https://www.cirkitdesigner.com/docs/guides/wifi-networking
•Mosquitto configuration: https://mosquitto.org/man/mosquitto-conf-5.html
•Node-RED InfluxDB node: https://flows.nodered.org/node/node-red-contrib-influxdb
•Dashboard v1 deprecated: https://flows.nodered.org/node/node-red-dashboard
•FlowFuse Dashboard 2 (untuk migrasi jangka panjang): https://dashboard.flowfuse.com/
LAMPIRAN A. Firmware lengkap untuk praktik mandiri
Tiga file harus berada dalam folder sketch yang sama: SmartLab_Teknolab.ino, SmartLabLogic.h, Secrets.h. Untuk praktis, salin dari paket ZIP yang menyertai dokumen ini. Blok berikut disajikan agar artikel ini tetap mandiri dan bisa diaudit.
/* TEKNOLAB - Smart Lab Environmental Guard
Prototipe Teknolab: state, ACK, tombol, JSON dan MQTT over WSS.
TARGET_SIM: 0 HW asli; 1 simulator. BOARD S3 memakai pin berbeda dari WROOM.
Library: DHT sensor library (Adafruit), LiquidCrystal_I2C sesuai modul,
PsychicMqttClient (theelims), Arduino-ESP32 core 3.x.
Secrets.h wajib dibuat dari Secrets.template.h di folder sketch.
*/
#include <Arduino.h>
#include <WiFi.h>
#include <Wire.h>
#include <DHT.h>
#include <LiquidCrystal_I2C.h>
#include <PsychicMqttClient.h>
#include <time.h>
#include "SmartLabLogic.h"
#include "Secrets.h"
// EDIT TARGET_SIM SEBELUM UPLOAD / SIMULASI:
#define TARGET_SIM 0 // 0=ESP32 fisik; 1=Cirkit Designer
#if TARGET_SIM
static const char* ACTIVE_WIFI_SSID="CirkitWifi";
static const char* ACTIVE_WIFI_PASSWORD="";
#else
static const char* ACTIVE_WIFI_SSID=WIFI_SSID;
static const char* ACTIVE_WIFI_PASSWORD=WIFI_PASSWORD;
#endif
#define TEAM_SLUG "teknolab"
#define TEAM_NAME "TEKNOLAB"
#if TARGET_SIM
#define SOURCE_SLUG "sim"
#define SOURCE_NAME "software"
#define SOURCE_ID TEAM_NAME "_SIM"
#else
#define SOURCE_SLUG "hw"
#define SOURCE_NAME "hardware"
#define SOURCE_ID TEAM_NAME "_HW"
#endif
#define TOPIC_TELEM "smartlab/" TEAM_SLUG "/" SOURCE_SLUG "/telemetry"
#define TOPIC_AVAIL "smartlab/" TEAM_SLUG "/" SOURCE_SLUG "/availability"
// WROOM klasik berbeda dari ESP32-S3. Cocokkan label GPIO pada board nyata/simulasi.
#if defined(CONFIG_IDF_TARGET_ESP32S3)
static constexpr uint8_t PIN_DHT=4, PIN_PB1=8, PIN_PB2=9;
static constexpr uint8_t PIN_G=15, PIN_Y=16, PIN_R=17;
static constexpr uint8_t PIN_POT=1, PIN_SDA=11, PIN_SCL=12;
#else
static constexpr uint8_t PIN_DHT=4, PIN_PB1=18, PIN_PB2=19;
static constexpr uint8_t PIN_G=25, PIN_Y=26, PIN_R=27;
static constexpr uint8_t PIN_POT=34, PIN_SDA=21, PIN_SCL=22;
#endif
static constexpr uint32_t DHT_MS=2000, POT_MS=150, LCD_MS=500, MQTT_MS=5000;
static constexpr uint32_t WIFI_RETRY_MS=10000;
DHT dht(PIN_DHT, DHT11);
LiquidCrystal_I2C lcd(0x27,16,2);
PsychicMqttClient mqtt;
SmartLab::Alarm alarmControl;
SmartLab::DebouncedButton pbPage, pbAck;
float temperatureC=NAN, humidityPct=NAN;
bool sensorOk=false, mqttStarted=false, lastMqttConnected=false;
uint8_t limitC=28, page=1;
uint32_t tDht=0, tPot=0, tLcd=0, tPublish=0, tWifi=0, sequence=0;
void displayLine(uint8_t row, const String &s) {
String line=s.substring(0,16);
while(line.length()<16) line+=' ';
lcd.setCursor(0,row); lcd.print(line);
}
void updateSensor(uint32_t now) {
if ((uint32_t)(now-tDht)<DHT_MS) return;
tDht=now;
const float t=dht.readTemperature();
const float h=dht.readHumidity();
sensorOk=SmartLab::validSensor(t,h);
temperatureC=sensorOk?t:NAN;
humidityPct=sensorOk?h:NAN;
}
void updateLimit(uint32_t now) {
if ((uint32_t)(now-tPot)<POT_MS) return;
tPot=now;
limitC=SmartLab::thresholdFromAdc((uint16_t)analogRead(PIN_POT));
}
void handleButtons(uint32_t now) {
if(pbPage.pressed(digitalRead(PIN_PB1)==HIGH,now)) page=(page==1)?2:1;
if(pbAck.pressed(digitalRead(PIN_PB2)==HIGH,now)) alarmControl.acknowledge();
}
void updateLeds(uint32_t now) {
auto l=alarmControl.leds(now);
digitalWrite(PIN_G,l.green?HIGH:LOW);
digitalWrite(PIN_Y,l.yellow?HIGH:LOW);
digitalWrite(PIN_R,l.red?HIGH:LOW);
}
void updateLcd(uint32_t now) {
if ((uint32_t)(now-tLcd)<LCD_MS) return;
tLcd=now;
if(page==1) {
if(sensorOk) displayLine(0,"T:"+String(temperatureC,1)+" H:"+String(humidityPct,0)+"%");
else displayLine(0,"SENSOR ERROR");
displayLine(1,"T_Limit:"+String(limitC)+"C");
} else {
String s=(alarmControl.state==SmartLab::State::SensorError)?"ERR":
((alarmControl.state==SmartLab::State::Critical)?"CRIT":
((alarmControl.state==SmartLab::State::Warning)?"WARN":"NORM"));
displayLine(0,"ST:"+s+" ACK:"+(alarmControl.ack?"Y":"N"));
bool wifi=WiFi.status()==WL_CONNECTED;
displayLine(1,String("WF:")+(wifi?"ON ":"OFF")+" MQ:"+(wifi&&mqtt.connected()?"ON":"OFF"));
}
}
void updateNetwork(uint32_t now) {
if(WiFi.status()!=WL_CONNECTED) {
if((uint32_t)(now-tWifi)>=WIFI_RETRY_MS) {
tWifi=now; WiFi.reconnect();
}
lastMqttConnected=false;
return;
}
// Sertifikat TLS: jangan memulai sebelum NTP memperoleh waktu masuk akal.
if(!mqttStarted && time(nullptr)>1700000000) {
mqttStarted=true;
mqtt.connect(); // PsychicMqttClient mengelola auto-reconnect.
}
bool up=mqtt.connected();
if(up&&!lastMqttConnected) mqtt.publish(TOPIC_AVAIL,1,true,"online");
lastMqttConnected=up;
}
bool prepareJson(char* out,size_t cap,uint32_t now,uint32_t seq) {
char t[24],h[24];
if(sensorOk){ snprintf(t,sizeof(t),"%.1f",temperatureC); snprintf(h,sizeof(h),"%.1f",humidityPct); }
else {strcpy(t,"null");strcpy(h,"null");}
int n=snprintf(out,cap,
"{\"device_id\":\"%s\",\"source\":\"%s\",\"temperature_c\":%s,"
"\"humidity_pct\":%s,\"threshold_temp_c\":%u,\"status\":\"%s\","
"\"ack\":%s,\"sensor_ok\":%s,\"uptime_ms\":%lu,\"sequence\":%lu}",
SOURCE_ID,SOURCE_NAME,t,h,(unsigned)limitC,SmartLab::text(alarmControl.state),
alarmControl.ack?"true":"false",sensorOk?"true":"false",
(unsigned long)now,(unsigned long)seq);
return n>0 && (size_t)n<cap;
}
void publishTelemetry(uint32_t now) {
if((uint32_t)(now-tPublish)<MQTT_MS) return;
tPublish=now;
if(WiFi.status()!=WL_CONNECTED||!mqtt.connected()) return;
char json[340];
if(!prepareJson(json,sizeof(json),now,sequence+1)){
Serial.println("ERROR: JSON buffer terlalu kecil");return;
}
// Return value != -1 berarti berhasil diantrikan oleh client, BUKAN konfirmasi tersimpan di DB.
int id=mqtt.publish(TOPIC_TELEM,0,false,json);
if(id!=-1){sequence++;Serial.println(json);}
else Serial.println("ERROR: MQTT publish gagal diantrikan");
}
void setup() {
Serial.begin(115200);
pinMode(PIN_PB1,INPUT_PULLUP); pinMode(PIN_PB2,INPUT_PULLUP);
pinMode(PIN_G,OUTPUT);pinMode(PIN_Y,OUTPUT);pinMode(PIN_R,OUTPUT);
pinMode(PIN_POT,INPUT);analogReadResolution(12);
Wire.begin(PIN_SDA,PIN_SCL);lcd.init();lcd.backlight();
displayLine(0,"TEKNOLAB SMART LAB");displayLine(1,"Starting ...");
dht.begin();
WiFi.mode(WIFI_STA);WiFi.setAutoReconnect(true);WiFi.begin(ACTIVE_WIFI_SSID,ACTIVE_WIFI_PASSWORD);
configTime(0,0,"pool.ntp.org","time.google.com");
mqtt.setServer(MQTT_WSS_URI);mqtt.setClientId(SOURCE_ID);
mqtt.setCredentials(MQTT_USERNAME,MQTT_PASSWORD);
mqtt.setCACert(MQTT_ROOT_CA);mqtt.setKeepAlive(60);mqtt.setAutoReconnect(true);
mqtt.setWill(TOPIC_AVAIL,1,true,"offline");
mqtt.onConnect([](bool sessionPresent){(void)sessionPresent;Serial.println("MQTT WSS CONNECTED");});
mqtt.onDisconnect([](bool sessionPresent){(void)sessionPresent;Serial.println("MQTT WSS DISCONNECTED");});
uint32_t now=millis();
tDht=now-DHT_MS;tPot=now-POT_MS;
tWifi=now-WIFI_RETRY_MS;
}
void loop() {
uint32_t now=millis();
updateLimit(now);updateSensor(now);
alarmControl.update(sensorOk,temperatureC,humidityPct,limitC,now);
handleButtons(now);updateLeds(now);updateLcd(now);
updateNetwork(now);publishTelemetry(now);
}LAMPIRAN B. Logika teruji C++ (SmartLabLogic.h)
#ifndef TEKNOLAB_SMART_LAB_LOGIC_H
#define TEKNOLAB_SMART_LAB_LOGIC_H
#include <stdint.h>
#include <math.h>
namespace SmartLab {
enum class State : uint8_t { SensorError, Normal, Warning, Critical };
inline const char* text(State s) {
switch (s) {
case State::SensorError: return "SENSOR_ERROR";
case State::Normal: return "NORMAL";
case State::Warning: return "WARNING";
case State::Critical: return "CRITICAL";
}
return "SENSOR_ERROR";
}
inline bool validSensor(float c, float rh) {
return isfinite(c) && isfinite(rh) && c >= -40.0f && c <= 80.0f && rh >= 0.0f && rh <= 100.0f;
}
inline uint8_t thresholdFromAdc(uint16_t adc) {
if (adc > 4095) adc = 4095;
return static_cast<uint8_t>(20 + (static_cast<uint32_t>(adc) * 15 + 2047) / 4095);
}
inline State evaluate(bool ok, float temperatureC, float humidityPct, uint8_t threshold) {
if (!ok || !validSensor(temperatureC, humidityPct)) return State::SensorError;
if (temperatureC >= (threshold + 3.0f) || humidityPct >= 85.0f) return State::Critical;
if (temperatureC >= threshold || humidityPct >= 75.0f) return State::Warning;
return State::Normal;
}
struct Leds { bool green; bool yellow; bool red; };
struct Alarm {
State state = State::SensorError;
bool ack = false;
uint32_t transitionAtMs = 0;
void update(bool ok, float temp, float humidity, uint8_t threshold, uint32_t now) {
State next = evaluate(ok, temp, humidity, threshold);
if (next != state) {
state = next;
ack = false; // Termasuk NORMAL, WARNING->CRITICAL, ERROR.
transitionAtMs = now;
}
if (state == State::Normal || state == State::SensorError) ack = false;
}
void acknowledge() {
if (state == State::Warning || state == State::Critical) ack = true;
}
Leds leds(uint32_t now) const {
if (state == State::Normal) return {true, false, false};
const uint32_t interval = (state == State::Warning) ? 500u : 250u;
const bool blink = (((uint32_t)(now - transitionAtMs) / interval) % 2u == 0u);
if (state == State::Warning) return {false, ack || blink, false};
if (state == State::Critical) return {false, false, ack || blink};
return {false, blink, blink};
}
};
struct DebouncedButton {
bool lastRawHigh = true;
bool stableHigh = true;
uint32_t lastChangedAtMs = 0;
bool pressed(bool rawHigh, uint32_t now, uint32_t debounceMs = 30) {
if (rawHigh != lastRawHigh) { lastRawHigh = rawHigh; lastChangedAtMs = now; }
if ((uint32_t)(now - lastChangedAtMs) >= debounceMs && stableHigh != rawHigh) {
stableHigh = rawHigh;
return !stableHigh; // event hanya pada HIGH -> LOW
}
return false;
}
};
}
#endifLAMPIRAN C. Secrets.h & sertifikat CA
File Secrets.template.h di ZIP memiliki contoh CA publik ISRG Root X1. Ini bukan private key. Ubah SSID, password, URI dan nama user di salinan Secrets.h. Jangan menempel password yang pernah terlihat pada sumber TESS1.ino; ganti kredensial tersebut dahulu.
#define WIFI_SSID "ISI_SSID_24GHZ"
#define WIFI_PASSWORD "ISI_PASSWORD_WIFI"
#define MQTT_WSS_URI "wss://broker-latihan.DOMAIN-ANDA:8084/"
#define MQTT_USERNAME "teknolab"
#define MQTT_PASSWORD "ISI_PASSWORD_AKUN_TEKNOLAB"
static const char MQTT_ROOT_CA[] = R"ROOTCA(-----BEGIN CERTIFICATE-----
...SALIN PEM ROOT CA VALID YANG SESUAI SERVER...
-----END CERTIFICATE-----
)ROOTCA";LAMPIRAN D. Konfigurasi backend dan skrip uji
Paket ZIP melampirkan compose YAML, mosquitto.conf, ACL, flow Node-RED JSON, validator JS, splitter JS, run_tests.sh, serta output PASS. Jika mengubah struktur JSON, perbarui validator dan lakukan pengujian ulang sebelum integrasi.
cd TEKNOLAB_SMART_LAB/tests
./run_tests.sh
# TARGET: PASS 39, PASS 139, PASS 15,
# dan satu mock compile+JSON. Output rinci: HASIL_TEST.txt.
Komentar (0)