- Published on
Kenapa Firmware C di ESP32 Jadi Spaghetti Setelah 6 Bulan?
- Authors
π Artikel 1: Kenapa Firmware C di ESP32 Jadi Spaghetti Setelah 6 Bulan?
Posisi: Artikel 1 dari 8 Domain Keputusan: Problem Mapping & Complexity Identification Entry Context: IndustrialNode/IndustrialNode.ino (Arduino-based ESP32)
- π Artikel 1: Kenapa Firmware C di ESP32 Jadi Spaghetti Setelah 6 Bulan?
- 4. Implementation Pattern (ESP32 Context)
- 5. Constraint & Embedded Impact
- 6. Failure Scenario (Detail)
- 7. Anti-Pattern (Artikel 1 Context)
- 8. Freeze Point
- π Freeze 1 β Firmware ESP32 Arduino Bukan Program Linear
- π Freeze 2 β Global Mutable Tanpa Owner Adalah Sumber Spaghetti
- π Freeze 3 β Tanpa Dependency Direction, Sistem Akan Cyclic
- π Freeze 4 β Fitur Baru Tanpa Boundary Akan Menghancurkan Firmware
- π Freeze 5 β Production Problem Bukan Karena C, Tapi Karena Struktur
- 9. Engineering Checklist (Self-Audit Artikel 1)
- 10. Summary (5 Bullet Maksimal)
1. Problem Reality
Mari kita mulai dari realitas, bukan teori.
Anda punya project ESP32 berbasis Arduino. Struktur awal sangat sederhana:
IndustrialNode/
βββ IndustrialNode.ino
Isinya:
- Setup WiFi
- Setup MQTT
- Baca sensor
- Kontrol relay
- Kirim telemetry
Firmware 300β800 baris. Demo sukses. Client senang. Deploy 10 unit.
Enam bulan kemudian:
- Tambah OTA
- Tambah reconnect WiFi
- Tambah retry MQTT
- Tambah mode AUTO/MANUAL
- Tambah watchdog
- Tambah logging
- Tambah interlock relay
- Tambah buffer telemetry saat offline
- Tambah remote command
Sekarang firmware menjadi 2.000β4.000 baris.
Dan Anda mulai mendengar:
- βKadang device hang.β
- βKadang relay nyala sendiri.β
- βKalau broker restart, device nggak balik.β
- βOTA gagal, unit harus dicabut listrik.β
- βPerubahan kecil bikin bug di tempat lain.β
Secara teknis firmware masih βjalanβ.
Tapi secara struktural, ia sudah rapuh.
Contoh Real di IndustrialNode.ino
Awalnya seperti ini:
bool relayState = false;
bool wifiConnected = false;
void setup() {
pinMode(5, OUTPUT);
WiFi.begin(ssid, password);
}
void loop() {
if (!wifiConnected) {
reconnectWiFi();
}
float temp = readTemp();
if (temp > 30) {
relayState = true;
digitalWrite(5, HIGH);
}
}
Masalah belum terlihat.
Lalu MQTT ditambahkan:
void onMqttMessage(char* topic, byte* payload, unsigned int length) {
if (payload[0] == '1') {
relayState = true;
digitalWrite(5, HIGH);
}
}
Sekarang:
- Relay bisa diubah dari loop.
- Relay bisa diubah dari MQTT callback.
Tidak ada boundary.
Tambahkan ISR:
void IRAM_ATTR buttonISR() {
relayState = !relayState;
digitalWrite(5, relayState);
}
Sekarang relay bisa diubah dari:
- Loop
- MQTT callback
- ISR
Tidak ada ownership. Tidak ada guard. Tidak ada arbitration.
Gejala Produksi
Mari kita lihat dari sudut pandang engineer lapangan.
1οΈβ£ Relay Chatter
- MQTT kirim ON.
- Loop mendeteksi suhu turun β OFF.
- ISR tombol ditekan β ON lagi.
Relay switching cepat.
Akibat:
- Kontak mekanikal aus.
- Noise listrik.
- Potensi arcing.
Masalah bukan di relay.
Masalah di struktur.
2οΈβ£ Device Stuck Setelah Broker Restart
MQTT reconnect logic di satu fungsi. WiFi reconnect di tempat lain. Loop juga cek koneksi.
Tiga mekanisme berbeda mencoba βmemperbaikiβ koneksi.
Hasil:
- Reconnect race.
- Client stuck di state tidak jelas.
- Watchdog reset.
3οΈβ£ OTA Brick
OTA dipanggil langsung dari MQTT callback.
Jika WiFi drop di tengah update:
- Tidak ada state machine.
- Tidak ada guard.
- Tidak ada recovery path.
Unit harus diflash ulang manual.
Intinya
Firmware tidak gagal karena C. Firmware gagal karena:
Kompleksitas bertambah, tetapi struktur tidak berubah.
Itu yang membuatnya menjadi spaghetti.
2. Root Cause Analysis
Kita bedah akar masalahnya secara sistemik.
2.1 Global Mutable State
Dalam firmware Arduino, sangat mudah membuat global variable.
bool relayState;
int mode;
float lastTemperature;
bool wifiConnected;
Masalahnya bukan global itu sendiri.
Masalahnya:
- Tidak ada owner.
- Tidak ada boundary.
- Tidak ada policy siapa yang boleh ubah.
Global mutable state adalah dependency implicit.
Semakin banyak global, semakin banyak coupling tak terlihat.
2.2 Implicit Dependency Graph
Jika kita gambarkan dependency sebenarnya:
Loop β Relay
MQTT Callback β Relay
ISR β Relay
WiFi Event β Mode
Mode β Relay
Tidak ada direction rule.
Semua boleh panggil semua.
Dependency graph menjadi cyclic.
Itulah spaghetti.


2.3 ISR Coupling
ISR adalah domain real-time.
Ketika ISR memanggil:
- digitalWrite
- fungsi service
- bahkan logging
Anda sudah kehilangan determinism.
ISR seharusnya:
- Sesingkat mungkin
- Tanpa blocking
- Tanpa logic berat
Tetapi dalam firmware spaghetti, ISR menjadi pintu belakang ke semua modul.
2.4 Reconnect Logic Tersebar
WiFi reconnect:
if (WiFi.status() != WL_CONNECTED) {
WiFi.begin(ssid, password);
}
MQTT reconnect:
if (!client.connected()) {
client.connect(...);
}
Loop juga cek flag lain.
Tidak ada state machine tunggal.
Hasilnya:
- Backoff tidak konsisten.
- Retry tidak terkontrol.
- Device bisa terus-menerus reconnect tanpa jeda.
2.5 Lifecycle Tidak Terdefinisi
Dalam firmware kecil, init sequence jarang dipikirkan.
Tapi setelah sistem kompleks:
- Apakah MQTT boleh publish sebelum sensor ready?
- Apakah relay boleh ON sebelum mode ditentukan?
- Apakah OTA boleh jalan saat control loop aktif?
Tanpa lifecycle formal:
Boot sequence menjadi implicit dan fragile.
2.6 Communication Bypass Control
MQTT callback langsung mengubah hardware.
Itu berarti:
Communication layer memiliki otoritas lebih tinggi daripada control loop.
Ini pelanggaran domain.
2.7 Debug-Driven Development
Ketika masalah muncul, solusi sering berupa:
Serial.println("Debug here");
Logging tersebar. Tidak terstruktur. Tidak bisa diaudit.
Firmware menjadi reaktif, bukan terdesain.
3. Design Principle (Rule yang Dikunci)
Artikel 1 belum mengunci solusi teknis.
Tapi kita mengunci kesadaran desain berikut:
Rule 1 β Firmware adalah Sistem Terdistribusi Internal
Walaupun hanya satu ESP32, di dalamnya ada:
- Control loop
- ISR
- WiFi stack
- MQTT callback
- Timer
- OTA handler
Ini bukan βsatu program linearβ.
Ini sistem multi-domain.
Jika tidak diperlakukan seperti sistem, ia akan rusak seperti sistem yang tidak diarsitektur.
Rule 2 β Setiap State Harus Memiliki Owner
Jika Anda tidak bisa menjawab:
βSiapa yang memiliki relay state?β
Maka desain sudah salah.
Ownership harus eksplisit.
Rule 3 β Dependency Tanpa Direction Akan Menjadi Cyclic
Jika semua modul boleh panggil semua, maka Anda tidak punya arsitektur.
Anda punya jaringan liar.
Rule 4 β Fitur Baru Tanpa Boundary = Kompleksitas Eksponensial
Menambah fitur tidak linear.
Jika struktur tidak disiplin, kompleksitas tumbuh eksponensial.
Itulah kenapa firmware terlihat baik di bulan pertama, dan rapuh di bulan keenam.
4. Implementation Pattern (ESP32 Context)
Di bagian ini kita tidak menawarkan solusi. Kita memetakan pola spaghetti yang nyata terjadi di firmware ESP32 berbasis Arduino.
Kita gunakan konteks folder yang sudah kita sepakati:
IndustrialNode/
βββ IndustrialNode.ino
Semua logika ada di satu file atau beberapa file helper tanpa boundary jelas.
4.1 Pola βGod .inoβ
IndustrialNode.ino menjadi pusat segalanya:
#include <WiFi.h>
#include <PubSubClient.h>
bool relayState = false;
bool wifiConnected = false;
bool mqttConnected = false;
WiFiClient espClient;
PubSubClient client(espClient);
void reconnectWiFi();
void reconnectMqtt();
void onMqttMessage(char*, byte*, unsigned int);
void setup() {
pinMode(5, OUTPUT);
WiFi.begin(ssid, password);
client.setServer(broker, 1883);
client.setCallback(onMqttMessage);
}
void loop() {
if (WiFi.status() != WL_CONNECTED) {
reconnectWiFi();
}
if (!client.connected()) {
reconnectMqtt();
}
client.loop();
float temp = readTemp();
if (temp > 30) {
relayState = true;
digitalWrite(5, HIGH);
}
}
Masalah struktural:
.inomemegang:- WiFi
- MQTT
- Control logic
- Hardware control
- Reconnect
Tidak ada layer.
Tidak ada ownership.
File entry Arduino menjadi βGod objectβ.
4.2 Pola Callback Mengontrol Hardware
void onMqttMessage(char* topic, byte* payload, unsigned int length) {
if (payload[0] == '1') {
relayState = true;
digitalWrite(5, HIGH);
}
}
Apa yang salah secara engineering?
- MQTT callback berjalan dalam konteks network loop.
- Callback langsung ubah GPIO.
- Tidak ada safety guard.
- Tidak ada arbitration dengan control loop.
Anda kehilangan determinism.
4.3 Pola ISR Mengakses State Global
void IRAM_ATTR buttonISR() {
relayState = !relayState;
digitalWrite(5, relayState);
}
Masalah embedded serius:
digitalWritedi ISR memperpanjang latency.relayStatebisa diubah bersamaan dengan loop.- Race condition tidak terlihat.
Pada beban tinggi WiFi + MQTT, ini bisa memicu glitch.
4.4 Pola Reconnect Tersebar
Reconnect WiFi:
void reconnectWiFi() {
WiFi.begin(ssid, password);
}
Reconnect MQTT:
void reconnectMqtt() {
client.connect("node01");
}
Loop juga cek status.
Jika broker restart:
- MQTT reconnect dipanggil.
- WiFi masih OK.
- Loop tetap memanggil
client.loop(). - Callback mungkin masuk saat reconnect.
State tidak eksplisit. Tidak ada backoff. Tidak ada ERROR mode.
4.5 Pola Dynamic Allocation Tersembunyi
String payload = "{ \"temp\": " + String(temp) + " }";
client.publish("node/data", payload.c_str());
Yang terjadi:
- Allocation String
- Reallocation saat concat
- Fragmentation heap
Pada awalnya tidak terasa. Setelah 2β3 minggu, heap minimum turun perlahan.
TLS handshake mulai gagal. Device restart.
Tidak ada yang sadar penyebabnya.
4.6 Pola Logging Liar
Serial.println("WiFi reconnect...");
Serial.println("MQTT fail");
Serial.println("Temp error");
Tidak ada:
- Log level
- Format standar
- Buffering
- Integrasi telemetry
Ketika device di lapangan, Serial tidak terlihat. Log hilang.
4.7 Pola Fail-Open
Misal sensor rusak:
if (!readTemp()) {
Serial.println("Sensor error");
}
Relay tetap ON.
Tidak ada fail-safe.
Dalam sistem mekanikal:
- Pompa bisa dry-run.
- Heater bisa overheat.
- Motor bisa overcurrent.
Masalah bukan lagi software. Masalah menjadi fisik.
5. Constraint & Embedded Impact
Sekarang kita lihat dari sisi constraint nyata ESP32.
Firmware spaghetti bukan hanya masalah estetika. Ia berdampak pada resource keras.
5.1 RAM Impact
ESP32 punya heap terbatas.
WiFi + TLS + MQTT sudah memakan banyak memori.
Jika kita tambahkan:
- String dynamic
- Buffer tanpa batas
- Object dibuat di loop
Heap menjadi tidak stabil.
Gejala:
ESP.getFreeHeap()turun perlahan.- Setelah waktu tertentu, TLS gagal.
- OTA gagal.
Ini bukan teori. Ini kejadian umum di deployment jangka panjang.
5.2 Stack Impact
Callback bertingkat:
MQTT β JSON parse β Logging β Publish β Retry
Call stack makin dalam.
Tanpa audit stack per task, overflow bisa terjadi sporadis.
5.3 Determinism Impact
Control loop harus stabil.
Jika loop memanggil:
- reconnect
- publish TLS
- logging blocking
Latency control menjadi variabel.
Dalam sistem kontrol nyata:
- Valve bisa overshoot.
- Relay switching tidak tepat waktu.
- Motor delay start.
5.4 Power & Electrical Impact
Ini sering dilupakan.
Relay chatter karena race condition:
- Inrush current berulang.
- Kontaktor cepat aus.
- EMI meningkat.
Software spaghetti bisa merusak hardware.
5.5 Field Maintenance Impact
Ketika bug muncul:
- Tidak ada error code formal.
- Tidak ada health telemetry.
- Tidak ada state machine eksplisit.
Engineer lapangan hanya bisa:
- Cabut listrik.
- Restart.
Itu bukan production-grade system.
6. Failure Scenario (Detail)
Sekarang kita konkretkan.
Scenario 1 β Broker Restart di Jam Sibuk
Kondisi:
- Device aktif.
- Relay ON.
- Telemetry tiap 5 detik.
- Broker restart.
Yang terjadi:
- MQTT disconnect.
- Loop panggil reconnect.
- Callback error mungkin masih masuk.
- WiFi tetap connected.
- Reconnect dipanggil berkali-kali.
Dampak:
- Control loop delay.
- Watchdog reset.
- Relay mungkin glitch.
Akar masalah:
- Tidak ada state machine formal.
- Reconnect logic tersebar.
Scenario 2 β WiFi Flapping
WiFi drop 2β3 detik lalu kembali.
Firmware spaghetti biasanya:
- Reconnect WiFi.
- Reconnect MQTT.
- Reset client.
- Publish ulang.
Jika terjadi 10 kali dalam 1 menit:
- Heap churn.
- TLS handshake berulang.
- Fragmentation meningkat.
Beberapa hari kemudian:
- Device stuck.
- Tidak ada root cause.
Scenario 3 β ISR + MQTT Race
- ISR toggle relay.
- MQTT callback set relay ON.
- Loop set relay OFF karena suhu turun.

Dalam 10 ms, relay berubah 3 kali.
Efek fisik:
- Contact bounce.
- Wear mekanikal.
Tidak ada arbitration layer.
Scenario 4 β Sensor Failure
Sensor I2C gagal.
readTemp() return invalid.
Loop tetap berjalan. Relay tetap ON.
Dalam sistem heater:
- Overheat.
Software tidak punya model fail-safe.
7. Anti-Pattern (Artikel 1 Context)
Artikel 1 belum bicara solusi, tapi kita kunci red flag berikut sebagai tanda spaghetti:
- β Global mutable runtime state
- β ISR memanggil logic aplikasi
- β Callback komunikasi mengontrol hardware
- β Reconnect logic tersebar
- β Tidak ada state machine eksplisit
- β Logging tidak terstruktur
- β Fail-open pada error
- β
.inomenjadi pusat semua domain
Jika β₯3 terjadi, firmware Anda sedang menuju kegagalan jangka panjang.
Berikutnya adalah:
- Freeze Point Artikel 1
- Engineering Checklist Artikel 1
- Summary tajam untuk menutup artikel
Saya lanjutkan ke Bagian 3 (Section 8β10) sekarang.
8. Freeze Point
Artikel 1 tidak mengunci solusi. Artikel 1 mengunci diagnosis.
Setelah membaca artikel ini, keputusan berikut dianggap final dan tidak boleh diperdebatkan lagi dalam seri ini:
π Freeze 1 β Firmware ESP32 Arduino Bukan Program Linear
Walaupun terlihat seperti:
setup();
loop();
Realitasnya adalah:
- Ada ISR.
- Ada WiFi task internal.
- Ada MQTT callback.
- Ada timer.
- Ada OTA handler.
- Ada reconnect event.
Artinya:
Firmware adalah sistem multi-domain internal.
Jika diperlakukan sebagai program linear sederhana, ia akan runtuh saat kompleksitas naik.
π Freeze 2 β Global Mutable Tanpa Owner Adalah Sumber Spaghetti
Jika satu state bisa diubah oleh:
- Loop
- Callback
- ISR
- Helper function
Maka itu bukan desain. Itu kebetulan yang belum meledak.
Setiap state harus punya owner.
π Freeze 3 β Tanpa Dependency Direction, Sistem Akan Cyclic
Jika semua modul bisa memanggil semua modul:
Maka dependency graph pasti menjadi cyclic.
Dan cyclic dependency adalah akar spaghetti.
π Freeze 4 β Fitur Baru Tanpa Boundary Akan Menghancurkan Firmware
Menambah fitur tanpa memperbaiki struktur bukan pertumbuhan linear.
Itu pertumbuhan eksponensial kompleksitas.
Firmware yang terlihat baik di 1.000 baris akan runtuh di 4.000 baris jika boundary tidak ada.
π Freeze 5 β Production Problem Bukan Karena C, Tapi Karena Struktur
Banyak engineer menyalahkan:
- Arduino
- C
- Library
- ESP32
Padahal masalahnya adalah:
Struktur tidak pernah didesain untuk tumbuh.
Ini penting.
Karena mulai Artikel 2, kita tidak menyalahkan bahasa. Kita akan menggunakan C++ sebagai alat untuk mengontrol kompleksitas.
9. Engineering Checklist (Self-Audit Artikel 1)
Gunakan checklist ini untuk menilai firmware Anda saat ini.
Jawab jujur.
Struktur
- Apakah
.inomengontrol WiFi, MQTT, sensor, relay sekaligus? - Apakah callback komunikasi langsung mengubah hardware?
- Apakah ISR memanggil logic selain set flag?
Jika ya β risiko tinggi.
State
- Apakah ada global mutable tanpa owner?
- Apakah lebih dari satu domain bisa mengubah relay?
- Apakah mode AUTO/MANUAL tersebar di banyak tempat?
Jika ya β dependency implicit.
Communication
- Apakah reconnect logic tersebar?
- Apakah publish bisa dipanggil dari mana saja?
- Apakah tidak ada state machine eksplisit?
Jika ya β sistem rentan.
Reliability
- Jika sensor gagal, apakah relay tetap ON?
- Apakah error hanya di-print ke Serial?
- Apakah tidak ada health metric formal?
Jika ya β bukan production-grade.
Embedded Constraint
- Apakah menggunakan
Stringsecara bebas? - Apakah tidak pernah audit heap minimum?
- Apakah callback berat berjalan di control path?
Jika ya β uptime jangka panjang berisiko.
Jika Anda menemukan 5 atau lebih βYAβ di atas, firmware Anda kemungkinan besar akan bermasalah dalam 3β6 bulan deployment.
10. Summary (5 Bullet Maksimal)
- Firmware ESP32 Arduino bukan program linear sederhana.
- Global mutable tanpa owner adalah akar spaghetti.
- Callback, ISR, dan loop tanpa boundary menciptakan chaos.
- Reconnect logic tanpa state machine adalah bom waktu.
- Masalah produksi muncul bukan karena bahasa, tetapi karena struktur tidak didesain untuk tumbuh.
Penutup Artikel 1
Artikel ini sengaja tidak memberi solusi.
Tujuan Artikel 1 adalah:
Membuat engineer sadar bahwa masalahnya bukan βbug kecilβ.
Masalahnya adalah:
Kompleksitas sistem meningkat, tetapi arsitektur tidak pernah dinaikkan levelnya.
Mulai Artikel 2, kita tidak lagi berbicara tentang teori OOP.
Kita akan menggunakan C++ sebagai alat disiplin untuk:
- Mengontrol dependency.
- Mengontrol ownership.
- Mengontrol lifecycle.
- Mengontrol kompleksitas sebelum ia meledak.
Catatan Penyusunan Artikel ini disusun sebagai materi edukasi dan referensi umum berdasarkan berbagai sumber pustaka, praktik lapangan, serta bantuan alat penulisan. Pembaca disarankan untuk melakukan verifikasi lanjutan dan penyesuaian sesuai dengan kondisi serta kebutuhan masing-masing sistem.