Published on

Firmware Architecture Berbasis Class (Layered & Deterministic)

Authors

πŸ“˜ Artikel 4: Firmware Architecture Berbasis Class (Layered & Deterministic)

Posisi: Artikel 4 dari 8 Domain Keputusan: Architecture & Layering Status Lock: πŸ”’ Layering & Dependency Direction Freeze Entry Context: IndustrialNode/IndustrialNode.ino



1. Problem Reality

Setelah Artikel 2 dan 3:

  • Global mutable sudah hilang.
  • Allocation sudah disiplin.
  • Constructor injection sudah eksplisit.

Namun firmware masih bisa runtuh jika arsitektur tidak dibekukan.

Masalah umum yang muncul:

  • svc_ mulai include app_
  • drv_ tahu detail logic bisnis
  • Application langsung akses hardware
  • Circular include muncul perlahan
  • File flat Arduino mempermudah include sembarangan

Karena Arduino tidak mengizinkan nested folder kompleks, semua file berada dalam satu root.

Tanpa aturan dependency direction, struktur akan kembali menjadi spaghetti β€” versi class.


2. Root Cause Analysis

Masalahnya bukan pada class.

Masalahnya adalah:

Tidak ada direction rule antar layer.

Contoh pelanggaran tipikal:

#include "app_ControlApp.h"   // di dalam svc_CommManager.cpp ❌

Atau:

#include "svc_ActuatorService.h"  // di dalam drv_RelayDriver.cpp ❌

Akibatnya:

  • Circular dependency
  • Tight coupling
  • Refactor sulit
  • Boundary kabur

Dependency graph menjadi cyclic.


πŸ”Ž Visualisasi Dependency Chaos Tanpa Layering

Image

Image

Tanpa aturan arah, semua modul bisa saling tarik.

Firmware kehilangan bentuk.


Baik. Berikut full version yang sudah diperbaiki (khususnya Rule 4 & 5), tanpa mengubah struktur lain, dan tanpa ambiguitas serta tanpa melanggar layering.


3. Design Principle (Rule yang Dikunci)

Artikel ini adalah titik di mana struktur firmware tidak lagi bebas berubah.

Sebelum titik ini:

  • kode masih bisa dipindah-pindah
  • tanggung jawab masih bisa dicampur
  • eksperimen masih diperbolehkan

Setelah titik ini:

  • struktur menjadi tetap
  • setiap file punya batas tanggung jawab
  • perubahan hanya boleh terjadi di dalam layer, bukan antar layer

Tujuannya bukan β€œrapi secara teori”, tetapi:

mencegah firmware menjadi sulit dipelihara setelah jumlah file bertambah

πŸ”’ Rule 1 β€” 3-Layer Model Final

Firmware dibagi menjadi 4 kelompok file berdasarkan prefix:

app_   β†’ logic utama sistem
svc_   β†’ pengelola domain
drv_   β†’ akses hardware
sys_   β†’ tipe dan konfigurasi dasar

Karena Arduino tidak mendukung struktur folder kompleks, maka:

prefix file = satu-satunya cara membedakan layer

Contoh nyata di project:

app_control.cpp
svc_sensor.cpp
drv_relay.cpp
sys_config.h

Jika prefix tidak konsisten, maka:

  • engineer tidak tahu file itu berada di layer mana
  • dependency mudah salah arah

πŸ”’ Rule 2 β€” Dependency Direction Strict (DAG)

Arah include ditentukan secara eksplisit:

app_ β†’ svc_ β†’ drv_ β†’ sys_

Ini berarti:

  • app_ boleh memakai svc_
  • svc_ boleh memakai drv_
  • drv_ hanya boleh memakai sys_

Alasan praktis:

mencegah dependency saling silang yang sulit ditelusuri

Contoh masalah jika dilanggar:

  • driver memanggil service β†’ driver tidak bisa dipakai ulang
  • service memanggil app β†’ logika jadi berputar
  • circular include β†’ compile error atau coupling tinggi

DAG memastikan:

tidak ada siklus dependency

πŸ”’ Rule 3 β€” Driver is Pure Hardware Boundary

Driver hanya berisi kode seperti:

  • digitalWrite
  • analogRead
  • Wire.read
  • SPI.transfer

Driver tidak boleh mengandung:

if mode == AUTO
if mqtt_connected
if temperature > threshold

Alasan:

driver harus tetap bisa dipakai ulang tanpa tahu sistem di atasnya

Contoh:

drv_relay.cpp hanya tahu:

set HIGH
set LOW

Tidak tahu:

  • kenapa relay ON
  • kapan relay ON
  • siapa yang memutuskan

πŸ”’ Rule 4 β€” Service is Domain Boundary (Final)

Service berada di tengah:

  • menerima data dari driver
  • menyediakan fungsi ke application

Contoh:

svc_sensor
svc_actuator
svc_comm

Service boleh:

  • mengolah data sensor
  • melakukan filtering sederhana
  • melakukan logic lokal sederhana (tidak bergantung kondisi global sistem)
  • menyediakan API seperti getTemperature()

Contoh logic yang boleh di service:

if (temp > threshold)
    actuatorService.on();
else
    actuatorService.off();

Selama:

tidak ada mode (AUTO/MANUAL)
tidak ada state machine
tidak ada kondisi global sistem

Service tidak boleh:

mengandung mode sistem (AUTO/MANUAL)
mengatur state machine utama
menggunakan kondisi global (system_error, network_state, dll)
mengakses ISR langsung

Alasan:

service hanya menangani perilaku lokal domain, bukan keputusan sistem

πŸ”’ Rule 5 β€” Application is System Decision Layer (Final)

Application adalah tempat keputusan tingkat sistem dibuat.

Contoh:

  • mode AUTO / MANUAL
  • fail-safe
  • override
  • kombinasi beberapa service

Application:

  • menggunakan service
  • menggabungkan beberapa input
  • menentukan perilaku sistem secara keseluruhan

Contoh yang benar (fail-safe, tidak melanggar layer):

if (system_error)
{
    actuatorService.forceOff();
}

Penjelasan:

Application membuat keputusan
Service mengeksekusi aksi
Driver tetap tersembunyi

Application tidak boleh:

memanggil relay langsung
memanggil digitalWrite langsung
mengakses Wire/SPI langsung

Alasan:

application menangani keputusan sistem, bukan implementasi hardware

πŸ”’ Rule 6 β€” Composition Root Only in IndustrialNode.ino

Semua object dibuat hanya di:

IndustrialNode.ino

Contoh:

RelayDriver relay(5);
TempSensor sensor;
ActuatorService actuator(relay);
ControlApp app(sensor, actuator);

Tidak boleh:

  • membuat object di banyak file
  • membuat object tersembunyi di dalam class lain

Alasan:

agar semua dependency terlihat di satu tempat

Setelah Artikel 4

Mulai titik ini, aturan tidak boleh dilanggar:

❌ tidak boleh ubah layer
❌ tidak boleh include melanggar arah
❌ tidak boleh shortcut antar layer

Jika dilanggar, efeknya biasanya:

  • dependency sulit dilacak
  • perubahan kecil merusak banyak file
  • firmware sulit dikembangkan

Inti yang Harus Dipahami Engineer Baru

Layering ini bukan untuk β€œrapi”, tetapi untuk:

membatasi dampak perubahan

Contoh:

  • ubah driver β†’ tidak merusak application
  • ubah logic β†’ tidak menyentuh hardware
  • ganti sensor β†’ hanya ubah driver/service

Jika boundary ini dijaga, firmware tetap bisa berkembang tanpa menjadi kacau.


πŸ”’ Rule 4 β€” Service is Domain Boundary

Service berada di tengah:

  • menerima data dari driver
  • menyediakan fungsi ke application

Contoh:

svc_sensor
svc_actuator
svc_comm

Service boleh:

  • mengolah data sensor
  • melakukan filtering sederhana
  • menyediakan API seperti getTemperature()

Service tidak boleh:

memutuskan mode sistem
mengatur state machine utama
mengakses ISR langsung

Alasan:

service harus tetap netral terhadap keputusan sistem

πŸ”’ Rule 5 β€” Application is Decision Layer

Application adalah tempat semua keputusan dibuat.

Contoh:

  • jika suhu > threshold β†’ nyalakan relay
  • jika error β†’ masuk fail-safe

Application:

  • menggunakan service
  • tidak langsung menyentuh hardware

Tidak boleh:

digitalWrite langsung
Wire.read langsung

Alasan:

memisahkan keputusan dari implementasi hardware

πŸ”’ Rule 6 β€” Composition Root Only in IndustrialNode.ino

Semua object dibuat hanya di:

IndustrialNode.ino

Contoh:

RelayDriver relay(5);
TempSensor sensor;
ControlApp app(sensor, relay);

Tidak boleh:

  • membuat object di banyak file
  • membuat object tersembunyi di dalam class lain

Alasan:

agar semua dependency terlihat di satu tempat

Setelah Artikel 4

Mulai titik ini, aturan tidak boleh dilanggar:

❌ tidak boleh ubah layer
❌ tidak boleh include melanggar arah
❌ tidak boleh shortcut antar layer

Jika dilanggar, efeknya biasanya:

  • dependency sulit dilacak
  • perubahan kecil merusak banyak file
  • firmware sulit dikembangkan

Inti yang Harus Dipahami Engineer Baru

Layering ini bukan untuk β€œrapi”, tetapi untuk:

membatasi dampak perubahan

Contoh:

  • ubah driver β†’ tidak merusak application
  • ubah logic β†’ tidak menyentuh hardware
  • ganti sensor β†’ hanya ubah driver/service

Jika boundary ini dijaga, firmware tetap bisa berkembang tanpa menjadi kacau.


4. Implementation Pattern (ESP32 Arduino Context)

4.1 Struktur File Final (Flat Arduino)

IndustrialNode/
β”‚
β”œβ”€β”€ IndustrialNode.ino
β”‚
β”œβ”€β”€ app_ControlApp.h
β”œβ”€β”€ app_ControlApp.cpp
β”‚
β”œβ”€β”€ svc_SensorService.h
β”œβ”€β”€ svc_SensorService.cpp
β”œβ”€β”€ svc_ActuatorService.h
β”œβ”€β”€ svc_ActuatorService.cpp
β”‚
β”œβ”€β”€ drv_RelayDriver.h
β”œβ”€β”€ drv_RelayDriver.cpp
β”‚
β”œβ”€β”€ sys_Config.h
β”œβ”€β”€ sys_Status.h

Tidak ada nested folder.

Prefix adalah boundary.


4.2 Dependency Direction Diagram

Image

Image

Image

Panah hanya satu arah:

Application ↓ Service ↓ Driver

Tidak ada panah naik.


4.3 IndustrialNode.ino (Composition Root)

#include "drv_RelayDriver.h"
#include "svc_ActuatorService.h"
#include "app_ControlApp.h"

RelayDriver relay(5);
ActuatorService actuator(relay);
ControlApp app(actuator);

void setup() {
    app.init();
}

void loop() {
    app.run();
}

Semua dependency terlihat eksplisit.


5. Constraint & Embedded Impact

Layering bukan hanya estetika arsitektur. Ia berdampak langsung pada reliability dan maintainability.


5.1 RAM Impact

Layering yang jelas:

  • Mengurangi duplikasi state.
  • Menghindari object tersebar tanpa owner.
  • Mengurangi kebutuhan global buffer.

Driver kecil β†’ Service agregasi β†’ Application orkestrasi.

State terkonsolidasi.


5.2 Flash Impact

Struktur file terpisah memang menambah boilerplate, namun:

  • Mengurangi include liar.
  • Mengurangi template instantiation berulang.
  • Menghindari code bloat akibat circular include workaround.

Binary lebih stabil.


5.3 Stack Impact

Tanpa layering:

  • Application bisa langsung panggil driver.
  • Driver bisa callback ke application.
  • Stack chain menjadi tidak terkontrol.

Dengan layering DAG:

Call stack hanya turun.

Application β†’ Service β†’ Driver

Tidak pernah naik kembali.

Stack depth lebih mudah diprediksi.


5.4 Determinism Impact

Layering memaksa:

  • ISR hanya notify (Artikel 5).
  • Driver tidak tahu policy.
  • Application tidak tahu register.

Akibatnya:

  • Tidak ada side effect tersembunyi.
  • Tidak ada dependency balik.

Determinism meningkat karena alur eksekusi menjadi eksplisit.


6. Failure Scenario

Layering bukan teori. Tanpa freeze ini, sistem akan kembali rusak.


Scenario 1 β€” Driver Tahu Logic Bisnis

Misalnya:

void RelayDriver::set(bool on) {
    if (systemMode == AUTO) {   // ❌
        digitalWrite(pin_, on);
    }
}

Driver tahu systemMode.

Jika nanti mode bertambah:

  • Driver harus diubah.
  • Refactor menyebar.

Dengan layering freeze:

Driver hanya hardware. Mode ditentukan di Application.


Scenario 2 β€” Service Include Application

#include "app_ControlApp.h"   // ❌

Circular dependency muncul.

Dampak:

  • Include guard rumit.
  • Refactor hampir mustahil.
  • Compile error misterius.

Dengan direction rule:

Service tidak boleh tahu Application.


Scenario 3 β€” Application Bypass Service

relay.set(true);   // dipanggil langsung dari app

Batas domain dilanggar.

Akibatnya:

  • Guard logic di Service dilewati.
  • Logging tidak konsisten.
  • Fail-safe bisa terabaikan.

Dengan rule:

Application hanya berbicara ke Service.


Scenario 4 β€” Circular Include Diam-Diam

File A include B. B include C. C include A.

Build masih jalan (karena forward declare), tapi dependency graph cyclic.

Refactor berikutnya meledak.

Dengan DAG rule:

Tidak mungkin terjadi.


7. Anti-Pattern

Daftar merah Artikel 4:

  • ❌ Driver include Service
  • ❌ Service include Application
  • ❌ Application akses hardware langsung
  • ❌ Circular include
  • ❌ File tanpa prefix layer
  • ❌ Multiple composition root
  • ❌ Instansiasi object tersebar

Dampak:

  • Coupling meningkat
  • Debug sulit
  • Testability rendah
  • Boundary kabur

8. Freeze Point

Setelah Artikel 4, keputusan berikut dianggap final:

  • 3-layer model wajib (app*, svc*, drv* + sys*).
  • Dependency direction hanya turun (DAG).
  • Driver hanya hardware.
  • Service hanya domain.
  • Application hanya orchestration.
  • Composition root hanya di IndustrialNode.ino.
  • Tidak ada circular include.

Layering tidak boleh berubah di artikel berikutnya.

Artikel 5–8 harus patuh pada struktur ini.


9. Engineering Checklist

Audit sebelum menambah fitur:

  • Apakah ada file tanpa prefix layer?
  • Apakah ada include yang melanggar direction?
  • Apakah Application akses driver langsung?
  • Apakah Service tahu logic UI/policy?
  • Apakah ada instansiasi object di luar IndustrialNode.ino?

Jika satu saja β€œya” β†’ melanggar Artikel 4.


10. Summary (5 Bullet Maksimal)

  • OOP tanpa layering tetap bisa menjadi spaghetti.
  • Dependency direction harus satu arah.
  • Prefix file adalah boundary enforcement di Arduino flat structure.
  • Composition root hanya satu.
  • Layering freeze menentukan bentuk firmware jangka panjang.

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.