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



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

Image

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

  1. Tidak boleh include app_.
  2. Tidak boleh include svc_.
  3. Tidak boleh include MQTT/WiFi.
  4. 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.