Bagian 5 – 6
Firmware
Firmware ditulis dalam C++ Arduino untuk ESP32. Satu sketsa tunggal tanpa RTOS: pembacaan sensor dijadwalkan per siklus 120 detik, transmisi dicoba berurutan LoRa → Wi-Fi → GPRS, dan alarm dikirim sebagai SMS ketika tidak ada ACK dari base. Bagian ini memuat konfigurasi build, konstanta sketsa, driver tiap kanal, dan kebijakan alarm.
5Setup Build & Pustaka
Dikompilasi dengan Arduino IDE, board package esp32 v3.x. Tidak ada dependensi berbayar dan tidak ada pustaka vendor yang wajib — hanya pustaka publik untuk dua sensor I²C dan driver AT yang ditulis sendiri.
5.1 Konfigurasi board
| Menu | Nilai |
|---|---|
| Board | ESP32 Dev Module |
| Board package | esp32 oleh Espressif, v3.x |
| PSRAM | Disabled |
| Flash Size | 4 MB |
| CPU Frequency | 240 MHz |
| USB CDC On Boot | Enabled |
USB CDC On Boot wajib Enabled agar log serial pembacaan sensor terlihat melalui kabel USB saat bench-test.
5.2 Pustaka
| Pustaka | Fungsi | API kunci |
|---|---|---|
| Adafruit SCD41 | CO₂ NDIR fotoakustik via I²C 0x59 | begin(0x59), setAutoBaseline(false), measure(), readCO2() |
| BME68x Sensor Library | Suhu, RH, tekanan via I²C 0x76 | #include "bme68xLibrary.h", begin(0x76, Wire), setTPH(), setHeaterProf(), setOpMode(BME68X_FORCED_MODE), fetchData(), getData(0) |
| SoftwareSerial | UART lunak untuk SIM800L | SoftwareSerial SIMSer(13, 18) |
| Wire | Bus I²C | Wire.begin(21, 22) |
| WiFi + HTTPClient | Scan whitelist dan POST JSON | WiFi.begin(), http.POST(payload) |
| PubSubClient F2 | MQTT untuk fase 2 | — |
| Driver AT (buatan sendiri) | Seluruh dialog SIM800L | simSendAT(), establishGPRS(), transmitViaGPRS(), sendSmsAlarm() |
SoftwareSerial SIMSer(13, 18) — bukan port perangkat keras ketiga yang tidak ada.
bme68xLibrary.h) dan hanya memakai suhu/RH/tekanan — nilai suhu dipakai sebagai koreksi silang untuk SCD41.
5.3Arsitektur Sketsa & Konstanta
Seluruh parameter tuning dikumpulkan sebagai konstanta di bagian atas sketsa sehingga penyesuaian lapangan cukup mengubah satu blok dan memflash ulang.
// ── Pin ────────────────────────────────────────────────
#define LORA_RX 26 // UART1 ke E32
#define LORA_TX 25
#define RS485_RX 16 // UART2 ke MAX485
#define RS485_TX 17
#define RS485_DE 19
#define RS485_RE 33
#define SIM_RX 13 // SoftwareSerial dari SIM800L TX
#define SIM_TX 18 // SoftwareSerial ke SIM800L RX
#define SIM_PKEY 15
#define SIM_RESET 14
#define BATT_SENSE 34 // pembagi 12:1, ADC1_CH6
#define LED_RED_BUZZ 23 // satu BC547 untuk LED merah + buzzer
#define LED_GREEN 27
#define CONFIG_MODE 32 // jumper ke GND = mode konfigurasi
// SPI fase 2 (kartu SD): SCK=D4 MISO=D2 MOSI=D5 CS=D12 (+ pulldown 10 kΩ)
// Spare: D6, D7
// ── Ambang alarm ───────────────────────────────────────
#define H2S_WARN 5.0 // ppm
#define H2S_CRIT 10.0 // ppm
#define CO2_WARN 5000 // ppm
#define CO2_CRIT 20000 // ppm
#define CO2_MAX 40000 // ppm
#define BATT_MIN 6.5 // volt — di bawah ini = alarm
// ── Timing ─────────────────────────────────────────────
#define CYCLE_MS 120000UL // siklus baca + kirim
#define LORA_ACK_MS 5000 // tunggu ACK LoRa
#define WIFI_SCAN_MS 10000 // batas scan whitelist
#define HTTP_TIMEOUT_MS 15000
#define ALARM_COOLDOWN_MS 300000UL // 5 menit antar alarm
#define NO_ACK_SMS_MS 1200000UL // 20 menit tanpa ACK → SMS
// ── Identitas & jaringan ───────────────────────────────
#define STATION_ID "A-SIKIDANG" // atau "B-TIMBANG"
#define HTTP_HOST "iot.dieng.or.id"
#define GPRS_APN "internet"
#define WIFI_RSSI_MIN -70 // dBm
const char* SMS_RECIPIENTS[3] = {"+62xxx", "+62yyy", "+62zzz"};
Alur setup() dan loop():
setup() : Serial(115200) → pin → Wire(21,22) → bme688.begin + setTPH
→ scd41.begin + setAutoBaseline(false) → UART1/UART2
→ simPowerOn() → (jumper D32 = GND → mode konfigurasi)
loop() : tiap CYCLE_MS ──┬→ baca NDC30 (Modbus, CRC16)
├→ scd41.measure() → readCO2()
├→ bme688 forced mode → getData(0)
├→ hitung tegangan baterai (×12)
├→ kirim: LoRa → tunggu ACK → Wi-Fi → GPRS
├→ jika 20 menit tanpa ACK → sendSmsAlarm()
└→ evaluasi ambang → LED/buzzer → cooldown
5.4Tiga Jalur Transmisi
Setiap siklus, data dicoba dikirim melalui tiga jalur secara berurutan: LoRa ke base C (utama, hemat daya), lalu Wi-Fi bila ada AP whitelist dalam jangkauan, lalu GPRS SIM800L. SMS hanya untuk alarm yang tidak ter-ACK.
LoRa E32 — jalur utama
Payload biner padat via UART1. Setelah kirim, stasiun menunggu LORA_ACK_MS (5 detik). ACK dari base menghentikan rantai — jalur Wi-Fi dan GPRS tidak perlu dicoba. Bila ACK tidak datang dalam 20 menit berturut-turut, SMS alarm aktif.
Wi-Fi — fallback kedua
Dipakai ketika stasiun berada di area sinyal AP (misal dekat pos). Scan dua tahap terhadap whitelist + ambang RSSI −70 dBm, lalu POST JSON ke iot.dieng.or.id.
GPRS — fallback ketiga
TCP raw via AT command ke APN internet, POST HTTP manual dengan Content-Length dihitung. Dijalankan hanya jika LoRa dan Wi-Fi gagal, karena paling boros daya dan waktu.
5.4.1 Wi-Fi: scan dua tahap dan POST
// Tahap 1 — scan dan cocokkan dengan whitelist
int n = WiFi.scanNetworks();
for (int i = 0; i < n; i++) {
for (int w = 0; w < WIFI_LIST_COUNT; w++) {
if (WiFi.SSID(i) == WIFI_LIST[w] && WiFi.RSSI(i) >= WIFI_RSSI_MIN)
wIdx = w;
}
}
if (wIdx < 0) return false; // tidak ada AP yang lolos
// Tahap 2 — sambung dan POST
WiFi.begin(WIFI_LIST[wIdx], WIFI_PW[wIdx]);
// … tunggu connected (maks 15 s) …
HTTPClient http;
http.begin(String("http://") + HTTP_HOST + HTTP_PATH);
http.addHeader("Content-Type", "application/json");
http.setConnectTimeout(HTTP_TIMEOUT_MS);
int code = http.POST(payload);
bool ok = (code >= 200 && code < 300);
http.end();
WiFi.disconnect();
5.4.2 GPRS: rantai AT sampai POST 200
bool establishGPRS() {
simSendAT("AT+CREG?"); // harus 0,1 atau 0,5
simSendAT("AT+CSQ"); // RSSI — log saja
if (!simSendAT("AT+CGATT?").endsWith("+CGATT: 1"))
simSendAT("AT+CGATT=1"); // paksa attach bila perlu
return true;
}
bool transmitViaGPRS() {
simSendAT("AT+CIPSHUT"); // bersihkan
simSendAT("AT+CSTT=\"" GPRS_APN "\",\"\",\"\""); // set APN
simSendAT("AT+CIICR", 10000); // bawa wireless
String ip = simSendAT("AT+CIFSR"); // harus mengandung '.'
simSendAT("AT+CIPSTART=\"TCP\",\"" HTTP_HOST "\",80", 15000);
// bangun POST manual — Content-Length wajib tepat
String req = "POST " HTTP_PATH " HTTP/1.1\r\n"
"Host: " HTTP_HOST "\r\n"
"Content-Type: application/json\r\n"
"Content-Length: " + String(body.length()) + "\r\n\r\n" + body;
simSendAT("AT+CIPSEND=" + String(req.length()));
// tunggu '>' lalu tulis req + 0x1A
// sukses = balasan mengandung "200" dan "SEND OK"
simSendAT("AT+CIPSHUT");
}
5.4.3 SMS alarm: kirim ke tiga nomor
void sendSmsAlarm(float h2s, int co2, float t) {
simSendAT("AT+CMGF=1"); // mode teks
for (int i = 0; i < 3; i++) {
flushRx(); // buang sisa balasan
simSendAT("AT+CMGS=\"" + String(SMS_RECIPIENTS[i]) + "\"", 5000);
if (!gotPrompt) continue; // tanpa '>' → lewati
String body = "CRIT DIENG " STATION_ID
+ " H2S=" + String(h2s, 1)
+ " CO2=" + String(co2)
+ " T=" + String(t, 0)
+ " @" + timestamp();
SIMSer.print(body); SIMSer.write(0x1A); // 0x1A = kirim
// verifikasi "+CMGS" — bila gagal, catat dan lanjut
delay(500); // jeda antar nomor
}
}
5.6Driver NDC30 Modbus RTU
Sensor H₂S NDC30 diakses sebagai slave Modbus RTU 0xA2, register input 0x0000, satu register, skala 0,01 ppm/LSB. CRC16 dihitung dengan polinomial 0xA001, init 0xFFFF, dikirim little-endian (byte rendah dulu).
uint16_t crc16Modbus(const uint8_t *buf, int len) {
uint16_t crc = 0xFFFF;
for (int i = 0; i < len; i++) {
crc ^= buf[i];
for (int b = 0; b < 8; b++)
crc = (crc & 1) ? (crc >> 1) ^ 0xA001 : (crc >> 1);
}
return crc;
}
Urutan frame dan aturan arah:
Request (8 byte) : A2 03 00 00 00 01 CRCL CRCH Response (7 byte) : A2 03 02 HI LO CRCL CRCH digitalWrite(RE, LOW); // RE aktif LOW → penerima siap digitalWrite(DE, HIGH); // DE aktif HIGH → transmitter aktif SerialRS485.write(req, 8); SerialRS485.flush(); // tunggu sampai terkirim semua digitalWrite(DE, LOW); // lepas bus → dengarkan balasan // 800 ms window — baca 7 byte, lalu: // cek addr == 0xA2, fn == 0x03, len == 0x02 // hitung CRC atas 5 byte pertama, bandingkan CRCL/CRCH // lolos semua → ppm = (HI << 8 | LO) * 0.01 // gagal satu saja → return -1.0 (tidak dipakai sebagai data)
6Kebijakan Alarm Tiga Tingkat
Setiap pembacaan dibandingkan dengan ambang per parameter. Tingkat tertinggi yang terpicu menang — bila H₂S masuk CRIT saat CO₂ masih WARN, indikator dan pesan mengikuti H₂S.
| Parameter | WARN (kuning) | CRIT (merah + buzzer) | MAX |
|---|---|---|---|
| H₂S | > 5 ppm | > 10 ppm | — |
| CO₂ | > 5.000 ppm | > 20.000 ppm | ≥ 40.000 ppm |
| Baterai | — | — | < 6,5 V |
Prioritas
MAX > CRIT > WARN. Evaluasi dilakukan dari tingkat tertinggi ke terendah; tingkat pertama yang cocok menjadi status siklus. Hanya satu status yang ditampilkan dan dikirim per siklus.
Perilaku per tingkat
WARN: LED kuning, ikut payload. CRIT: LED merah + buzzer di D23, pesan alarm dalam payload, SMS bila 20 menit tanpa ACK. MAX: sama dengan CRIT dan ditandai sebagai keadaan darurat di pesan SMS.
ALARM_COOLDOWN_MS. Ini mencegah buzzer dan SMS berulang saat nilai berdenyut di sekitar ambang. Nilai yang melampaui ambang tetap terekam di setiap payload siklus.