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.

Kotak perangkat IoT Orion berisi modul pemantau gas
Sketsa Firmware Siklus 120 detik

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

MenuNilai
BoardESP32 Dev Module
Board packageesp32 oleh Espressif, v3.x
PSRAMDisabled
Flash Size4 MB
CPU Frequency240 MHz
USB CDC On BootEnabled

USB CDC On Boot wajib Enabled agar log serial pembacaan sensor terlihat melalui kabel USB saat bench-test.

5.2 Pustaka

PustakaFungsiAPI kunci
Adafruit SCD41CO₂ NDIR fotoakustik via I²C 0x59begin(0x59), setAutoBaseline(false), measure(), readCO2()
BME68x Sensor LibrarySuhu, RH, tekanan via I²C 0x76#include "bme68xLibrary.h", begin(0x76, Wire), setTPH(), setHeaterProf(), setOpMode(BME68X_FORCED_MODE), fetchData(), getData(0)
SoftwareSerialUART lunak untuk SIM800LSoftwareSerial SIMSer(13, 18)
WireBus I²CWire.begin(21, 22)
WiFi + HTTPClientScan whitelist dan POST JSONWiFi.begin(), http.POST(payload)
PubSubClient F2MQTT untuk fase 2—
Driver AT (buatan sendiri)Seluruh dialog SIM800LsimSendAT(), establishGPRS(), transmitViaGPRS(), sendSmsAlarm()
Jangan pakai HardwareSerial(3). Pada core esp32 v3.x hanya UART 0, 1, dan 2 yang valid. UART 1 (pin 25/26) untuk LoRa E32 dan UART 2 (pin 17/16) untuk MAX485, sehingga SIM800L wajib memakai SoftwareSerial SIMSer(13, 18) — bukan port perangkat keras ketiga yang tidak ada.
Tentang BSEC. Alternatif pustaka BME688 berbasis BSEC menawarkan output IAQ siap pakai, tetapi berdistriibusi sebagai biner dengan lisensi terbatas. Rancangan ini memilih pustaka Bosch terbuka (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
  }
}
Rate-limit SMS 5 menit. Tanpa pembatasan, satu malam kondisi kritis dapat menghabiskan pulsa. Kooldown alarm 5 menit dan ambang 20 menit tanpa ACK memastikan SMS hanya keluar untuk keadaan yang benar-benar memerlukan intervensi manusia.

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)
−1,0 adalah penanda kesalahan, bukan data. Nilai H₂S −1,0 berarti CRC tidak cocok, timeout, atau frame rusak. Firmware menandai pembacaan itu gagal, memakai nilai valid terakhir untuk keputusan alarm, dan mencatatnya dalam payload agar base dapat memantau tingkat kegagalan bus.
Slave address 0xA2 masih terbuka. Nilai ini berasal dari asumsi terhadap firmware pabrik NDC30 dan harus diverifikasi dengan membaca register pada saat komisioning (lihat daftar item terbuka).

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.

ParameterWARN (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.

Kooldown 5 menit. Setelah alarm dikirim, alarm pada tingkat yang sama tidak dikirim ulang selama 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.
SMS adalah jaring pengaman terakhir. Rantainya: LoRa → ACK (utama) → Wi-Fi → GPRS → SMS. SMS dikeluarkan hanya ketika stasiun tidak berhasil mengirim payload sama sekali selama 20 menit sementara statusnya CRIT/MAX. Dengan demikian jalur telepon seluler tidak dipakai untuk data rutin, melainkan hanya untuk peristiwa.