- Published on
3-Layer Firmware Architecture on ESP32
- Authors
π Foundation Article 6: 3-Layer Firmware Architecture on ESP32
Track: C++ Firmware Engineering Foundations for ESP32 Environment: ESP32 + VSCode + Arduino Community Edition Project Base: IndustrialNode/IndustrialNode.ino Use Case: Generic Sensor + Relay Node
- π Foundation Article 6: 3-Layer Firmware Architecture on ESP32
- 1οΈβ£ Kenapa Firmware Butuh Layer?
- 2οΈβ£ Model 3-Layer yang Digunakan
- 3οΈβ£ Visual Model Arsitektur
- 4οΈβ£ Driver Layer (drv_)
- 5οΈβ£ Service Layer (svc_)
- 6οΈβ£ Application Layer (app_)
- 7οΈβ£ System Module (sys_)
- 8οΈβ£ Struktur Folder (Flat & Konsisten)
- 9οΈβ£ Arah Dependency (Sangat Penting)
- π Sensor + Relay Node dalam Model Ini
- 11οΈβ£ Kenapa Layering Penting Sebelum Production
- 12οΈβ£ Mental Model yang Harus Dikunci
1οΈβ£ Kenapa Firmware Butuh Layer?
Firmware kecil sering dimulai dengan satu file .ino.
Awalnya sederhana:
void loop() {
int value = analogRead(34);
if (value > 500) {
digitalWrite(5, HIGH);
} else {
digitalWrite(5, LOW);
}
}
Masalah muncul saat sistem bertambah:
- MQTT
- WiFi reconnect
- OTA
- Logging
- ISR button
- Timer task
- Error handling
loop() berubah menjadi:
loop()
ββ read sensor
ββ control relay
ββ check WiFi
ββ reconnect MQTT
ββ handle OTA
ββ print debug
ββ process ISR flag
ββ handle timeout
Sekarang:
- State tersebar.
- Dependency silang.
- Refactor berisiko.
- Debug sulit.
Layering bukan soal βrapiβ.
Layering adalah:
Membatasi siapa boleh tahu siapa.
2οΈβ£ Model 3-Layer yang Digunakan
Model yang kita pakai sepanjang seri:
Application Layer
β
Service Layer
β
Driver Layer
Dengan modul pendukung:
System (sys_)
Ini bukan Clean Architecture penuh. Ini model minimal yang cukup untuk:
- Determinism
- Dependency control
- Evolusi ke Production
3οΈβ£ Visual Model Arsitektur

Konseptual yang kita pakai:
app_ β orchestration
svc_ β domain logic
drv_ β hardware boundary
sys_ β config & shared definition
Arah dependency selalu turun.
Tidak ada upward dependency.
4οΈβ£ Driver Layer (drv_)
Driver adalah:
Boundary terhadap hardware.
Contoh:
// drv_RelayDriver.h
class RelayDriver {
public:
explicit RelayDriver(int pin);
void set(bool on);
bool state() const;
private:
int pin_;
bool state_;
};
Driver:
- Tidak tahu MQTT.
- Tidak tahu sensor threshold.
- Tidak tahu WiFi.
- Tidak tahu app flow.
- Tidak tahu ISR.
Ia hanya tahu:
- Pin
- Register
- Hardware state
Failure Model β Driver Tahu Terlalu Banyak
Jika driver seperti ini:
void RelayDriver::set(bool on) {
if (WiFi.isConnected()) {
digitalWrite(pin_, on);
}
}
Driver sekarang tahu network.
Boundary rusak.
Refactor menjadi sulit.
Rule Driver Layer
- Tidak boleh include app_.
- Tidak boleh include svc_.
- Tidak boleh include MQTT/WiFi.
- Tidak boleh tahu business rule.
Driver hanya hardware boundary.
5οΈβ£ Service Layer (svc_)
Service adalah domain owner.
Ia:
- Menggabungkan beberapa driver.
- Mengimplementasikan business rule.
- Tidak tahu orchestration besar.
- Tidak tahu ISR detail.
Contoh:
class ControlService {
public:
ControlService(SensorService& sensor,
RelayDriver& relay);
void update();
private:
SensorService& sensor_;
RelayDriver& relay_;
};
ControlService:
- Membaca sensor.
- Menentukan threshold.
- Memanggil relay.set().
Ia tidak tahu MQTT. Ia tidak tahu reconnect. Ia tidak tahu loop.
Failure Model β Service Tahu Terlalu Banyak
Jika Service seperti ini:
void ControlService::update() {
if (!WiFi.isConnected()) {
reconnect();
}
int v = sensor_.read();
relay_.set(v > 500);
}
Service sekarang tahu network.
Domain tercampur dengan infrastructure.
Layer rusak.
6οΈβ£ Application Layer (app_)
Application adalah orchestrator.
Ia:
- Dipanggil dari loop().
- Mengatur flow.
- Mengatur state machine.
- Mengatur kapan service dipanggil.
- Mengatur komunikasi antar domain.
Contoh:
class ControlApp {
public:
explicit ControlApp(ControlService& service);
void run();
private:
ControlService& service_;
};
ControlApp::run() dipanggil dari loop().
Application:
- Tidak tahu pin.
- Tidak tahu analogRead.
- Tidak tahu register.
Ia hanya tahu service API.
7οΈβ£ System Module (sys_)
sys_ bukan layer aktif.
Ia bukan domain. Ia bukan orchestrator. Ia bukan driver.
Ia adalah:
Modul pendukung untuk shared definition & configuration.
Contoh:
// sys_Config.h
#pragma once
constexpr int RELAY_PIN = 5;
constexpr int SENSOR_PIN = 34;
constexpr int SENSOR_THRESHOLD = 500;
Hal yang boleh di sys_:
- constexpr
- enum class
- typedef
- shared struct
- error code
- compile-time config
Hal yang tidak boleh:
- Logic
- WiFi code
- Business rule
- Hardware control
Kenapa sys_ Diperlukan?
Tanpa sys_:
- Magic number tersebar.
- Constant duplikat.
- Include silang antar layer.
- Refactor sulit.
sys_ membantu dependency tetap bersih.
8οΈβ£ Struktur Folder (Flat & Konsisten)
Sejak awal Foundation kita pakai struktur ini:
IndustrialNode/
βββ IndustrialNode.ino
βββ app_ControlApp.h
βββ app_ControlApp.cpp
βββ svc_ControlService.h
βββ svc_ControlService.cpp
βββ svc_SensorService.h
βββ svc_SensorService.cpp
βββ drv_RelayDriver.h
βββ drv_RelayDriver.cpp
βββ sys_Config.h
Tidak ada nested folder.
Kenapa flat?
Karena:
Discipline harus muncul dari dependency, bukan dari folder hierarchy.
Prefix adalah boundary visual:
app_svc_drv_sys_
Flat folder memaksa engineer sadar dependency.
Nested folder sering memberi ilusi aman, padahal include bisa tetap silang.
9οΈβ£ Arah Dependency (Sangat Penting)
Aturan keras:
app_ β svc_ β drv_
Dan hanya turun.
Tidak ada:
drv_ β svc_
drv_ β app_
svc_ β app_
Visual Dependency Direction
+------------------+
| app_ControlApp |
+------------------+
β
+------------------+
| svc_ControlService|
+------------------+
β
+------------------+
| drv_RelayDriver |
+------------------+
Semua panah ke bawah.
Anti-Pattern β Upward Dependency
Contoh salah:
// drv_RelayDriver.cpp
#include "app_ControlApp.h"
Layer hancur.
Atau:
// svc_ControlService.cpp
#include "app_ControlApp.h"
Service tahu application. Boundary rusak.
Kenapa Dependency Direction Kritis?
Karena:
- Mengurangi coupling.
- Mencegah circular include.
- Mencegah blast radius perubahan.
- Meningkatkan testability.
Layering tanpa dependency discipline = kosmetik.
π Sensor + Relay Node dalam Model Ini
Sekarang kita lihat full flow.
Flow Eksekusi
loop()
β
app_ControlApp::run()
β
svc_ControlService::update()
β
svc_SensorService::read()
β
drv_RelayDriver::set()
Hardware hanya disentuh di driver.
Business rule hanya di service.
Orchestration hanya di app.
Contoh Implementasi Minimal (Ringkas & Konsisten)
// IndustrialNode.ino
#include "sys_Config.h"
#include "drv_RelayDriver.h"
#include "svc_SensorService.h"
#include "svc_ControlService.h"
#include "app_ControlApp.h"
RelayDriver relay(RELAY_PIN);
SensorService sensor(SENSOR_PIN);
ControlService control(sensor, relay);
ControlApp app(control);
void setup() {
app.init();
}
void loop() {
app.run();
}
Layer separation jelas.
Tidak ada cross-layer shortcut.
11οΈβ£ Kenapa Layering Penting Sebelum Production
Di Production nanti:
- Dependency akan di-freeze.
- Cross-layer call akan dianggap violation.
- Include direction akan diaudit.
- Prefix akan enforce boundary.
Jika Foundation tidak paham layering:
Production rule akan terasa βkerasβ.
Padahal:
Production hanya mengunci apa yang sudah dibentuk di Foundation.
Tanpa Layering
Tambahkan fitur:
- MQTT
- OTA
- WebServer
- Logging
- Error retry
Semua masuk ke loop.
Hasil:
- 1000+ line file.
- Dependency silang.
- Refactor berisiko.
- Bug sulit dilacak.
Dengan Layering
Tambahkan MQTT:
- Tambah svc_MqttService.
- App mengatur flow.
- Driver tetap tidak tahu MQTT.
- Sensor tetap tidak tahu network.
Blast radius kecil.
12οΈβ£ Mental Model yang Harus Dikunci
Setelah Article 6, engineer harus memahami:
1οΈβ£ Layer bukan soal folder
Ia soal dependency direction.
2οΈβ£ Driver hanya hardware boundary
Tidak tahu domain. Tidak tahu network.
3οΈβ£ Service adalah domain owner
Business rule hidup di sini.
4οΈβ£ Application adalah orchestrator
Flow control hidup di sini.
5οΈβ£ Dependency harus satu arah
Tidak ada upward call.
6οΈβ£ Prefix membantu disiplin dalam flat folder
Nama file = boundary visual.
7οΈβ£ Layering mengurangi blast radius perubahan
Fitur baru tidak merusak seluruh sistem.
Penutup
3-layer architecture yang kita gunakan bukan kompleks.
Ia minimal.
Tetapi cukup untuk:
- Mengendalikan dependency
- Mengisolasi hardware
- Mengontrol domain logic
- Menjaga determinism
- Mempersiapkan freeze di Production
Tanpa layering:
Firmware kecil akan runtuh ketika fitur bertambah.
Dengan layering:
Foundation β Production adalah evolusi natural.
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.