Published on

OOP + FreeRTOS Tanpa Membunuh Determinism

Authors

📘 Artikel 5: OOP + FreeRTOS Tanpa Membunuh Determinism

Posisi: Artikel 5 dari 8 Domain Keputusan: Concurrency & Execution Boundary Status Lock: 🔒 Task Boundary & ISR Interaction Freeze Entry Context: IndustrialNode/IndustrialNode.ino



1. Problem Reality

ESP32 (Arduino core) berjalan di atas FreeRTOS.

Artinya:

  • Ada WiFi task internal.
  • Ada TCP/IP task.
  • Ada callback MQTT.
  • Ada timer interrupt.
  • Ada ISR eksternal.
  • Ada loop() task utama.

Firmware bukan single-thread.

Pada titik ini, firmware yang sudah dibangun pada Artikel 1–4 memiliki struktur:

app_ → svc_ → drv_

Struktur ini mengatur siapa boleh memanggil siapa, tetapi belum mengatur siapa menjalankan kode tersebut.

Masalah mulai muncul ketika struktur ini dijalankan dalam konteks FreeRTOS.

Beberapa eksekusi berjalan bersamaan:

  • callback MQTT berjalan di task network
  • ISR berjalan asynchronous
  • loop berjalan di task utama
  • task internal ESP32 berjalan di background

Semua ini bisa memanggil kode yang sama.

Jika tidak dikontrol, maka:

  • ISR bisa langsung mengubah state service
  • callback bisa langsung mengontrol actuator
  • task berbeda bisa mengakses resource yang sama

Artinya:

execution tidak lagi mengikuti boundary layer

Masalah umum yang muncul:

  • ISR langsung mengubah state service
  • MQTT callback memanggil actuator
  • Control loop memanggil publish blocking
  • Logging blocking di task utama
  • Queue tanpa batas menyebabkan memory spike

Gejala produksi:

  • Relay chatter
  • Watchdog reset
  • Latency tidak konsisten
  • Bug hanya muncul saat traffic tinggi

Masalahnya bukan FreeRTOS.

Masalahnya:

Layer sudah benar, tetapi eksekusi tidak mengikuti layer tersebut.

Atau lebih spesifik:

tidak ada boundary eksekusi yang menjaga agar:
Task → app_ → svc_ → drv_
tetap konsisten

Tidak ada boundary eksekusi yang dikunci.


2. Root Cause Analysis

Concurrency chaos muncul dari 4 sumber utama.


Sebelum masuk ke sumber masalah, perlu dipahami bagaimana concurrency muncul pada firmware ini.

Pada Artikel 4, struktur firmware sudah dikunci:

app_ → svc_ → drv_

Struktur ini mengatur alur pemanggilan fungsi, tetapi tidak mengatur konteks eksekusi.

Pada ESP32, kode tidak dijalankan dalam satu alur tunggal.

Beberapa konteks eksekusi berjalan bersamaan:

- ControlTask (menjalankan application)
- Task internal WiFi / TCP-IP
- Callback MQTT (dipanggil oleh network stack)
- ISR (interrupt hardware)
- loop() (task utama Arduino)

Semua konteks ini bisa memanggil:

app_
svc_

Tanpa kontrol, maka:

lebih dari satu eksekusi bisa masuk ke layer yang sama
pada waktu yang tidak terprediksi

Inilah yang disebut:

concurrency

Masalah muncul bukan karena ada banyak task, tetapi karena:

tidak ada aturan siapa yang boleh mengakses apa
dan dari konteks mana

Akibatnya:

  • boundary layer dilanggar oleh eksekusi
  • beberapa eksekusi mengubah state yang sama
  • urutan eksekusi tidak deterministik

Empat kasus berikut adalah bentuk paling umum dari pelanggaran tersebut.


2.1 ISR Melakukan Logic

void IRAM_ATTR buttonISR() {
    actuator.setRelay(true);   // ❌
}

Masalah:

  • ISR preempt task lain
  • Bisa race dengan ControlTask
  • Bisa blocking jika tidak hati-hati

ISR bukan tempat logic.


2.2 Callback Komunikasi Mengontrol Hardware

void onMqttMessage(...) {
    actuator.setRelay(true);   // ❌
}

Callback berjalan dalam konteks network loop.

Jika ia mengontrol hardware langsung:

  • Tidak ada arbitration
  • Tidak ada guard

2.3 Blocking Call di Control Loop

void ControlApp::run() {
    comm.publish(data);  // bisa blocking ❌
}

Jika TLS handshake terjadi:

  • Latency spike
  • Watchdog reset

Control loop harus deterministic.


2.4 Shared Mutable State Tanpa Guard

Dua task mengakses state sama tanpa mekanisme sinkronisasi.

Race condition muncul sporadis.


🔎 Visualisasi Concurrency Tanpa Boundary

Tanpa boundary:

  • ISR
  • WiFi task
  • MQTT callback
  • ControlTask

Semua bisa menyentuh service.


3. Design Principle (Rule yang Dikunci)

Artikel ini mengunci model concurrency final.


🔒 Rule 1 — Task per Domain (Minimal)

Minimal harus ada:

  • ControlTask (Application)
  • CommTask (Communication)

Tidak semua logic dicampur di loop().


🔒 Rule 2 — ISR Only Notify

ISR hanya boleh:

  • Set flag volatile
  • Push event ke queue fixed-size
  • Notify task

Tidak boleh:

  • Allocate
  • Call service langsung
  • Publish

🔒 Rule 3 — No Blocking in ControlTask

ControlTask tidak boleh:

  • Publish network blocking
  • Logging blocking
  • Delay panjang

ControlTask harus ringan dan periodik.


🔒 Rule 4 — Queue Bounded & Fixed Size

Semua komunikasi antar task harus:

  • Fixed-size queue
  • Tidak growable
  • Tidak dynamic allocation

🔒 Rule 5 — No Shared Mutable Without Ownership

Jika state shared antar task:

  • Harus dimiliki satu task
  • Task lain hanya kirim request

Tidak boleh dua task modify state langsung.


Setelah Artikel 5:

  • ❌ ISR tidak boleh panggil service langsung
  • ❌ Callback tidak boleh ubah hardware langsung
  • ❌ ControlTask tidak boleh blocking
  • ❌ Tidak boleh queue tak terbatas

Concurrency boundary terkunci.


4. Implementation Pattern (ESP32 Arduino Context)

Bagian ini menunjukkan bagaimana rule pada Artikel 5 diterapkan dalam kode nyata.

Pada titik ini:

  • Struktur layer sudah dikunci (Artikel 4)
  • Boundary eksekusi sudah didefinisikan (Artikel 5)

Sehingga implementasi harus mengikuti:

Task → app_ → svc_ → drv_

Semua pattern di bawah bertujuan menjaga agar:

execution tidak melanggar boundary layer

4.1 Task Wrapper Class

class ControlTask {
public:
    void run();

private:
    ControlApp& app_;
};

ControlTask adalah representasi dari unit eksekusi (task) yang bertanggung jawab menjalankan application.

Hal penting yang harus diperhatikan:

Task tidak berisi logic sistem
Task hanya menjalankan application

Implementasi nyata biasanya:

void ControlTask::run()
{
    while (true)
    {
        app_.run();
        vTaskDelay(10);
    }
}

Di sini:

  • ControlTask → execution boundary
  • ControlApp → decision layer (app_)

Artinya:

semua keputusan tetap berada di app_
bukan di task

Jika logic dipindah ke dalam task:

layering dari Artikel 4 akan rusak

4.2 ISR → Queue / Notify Pattern

volatile bool buttonPressed = false;

void IRAM_ATTR buttonISR() {
    buttonPressed = true;
}

ISR hanya melakukan:

notify bahwa event terjadi

ISR tidak boleh:

mengakses service
mengontrol actuator
melakukan logic

Pengolahan dilakukan di ControlTask:

if (buttonPressed) {
    buttonPressed = false;
    actuator.setRelay(true);
}

Namun dalam konteks layering yang benar:

ControlTask tidak boleh langsung mengontrol driver

Seharusnya alur tetap:

ISR → notify → Task → app_ → svc_ → drv_

Artinya contoh di atas secara konsep adalah:

  • ISR hanya memberi sinyal
  • Task menerima sinyal
  • Application yang memutuskan aksi

Tujuan pattern ini:

memisahkan interrupt context dari execution context utama

4.3 Queue Communication Pattern

struct Command {
    bool relayOn;
};

QueueHandle_t commandQueue;

Queue digunakan untuk komunikasi antar task.

Contoh:

CommTask:

xQueueSend(commandQueue, &cmd, 0);

ControlTask:

xQueueReceive(commandQueue, &cmd, 0);

Di sini:

CommTask tidak mengontrol system langsung
CommTask hanya mengirim command

Dan:

ControlTask tetap menjadi satu-satunya yang menjalankan app_

Sehingga alur tetap konsisten:

CommTask → queue → ControlTask → app_ → svc_ → drv_

4.4 Kenapa Harus Queue (Bukan Shared State)

Tanpa queue:

dua task bisa mengubah state yang sama
→ race condition

Dengan queue:

satu arah komunikasi
tidak ada akses langsung ke state

4.5 Kenapa Harus Fixed Size & Bounded

Fixed size.
Bounded.
Deterministic.

Artinya:

  • ukuran queue tetap
  • tidak ada alokasi dinamis
  • tidak tumbuh saat load tinggi

Tanpa batas:

queue bisa terus bertambah saat network down
→ memory habis
→ crash

Dengan bounded queue:

data bisa di-drop
tetapi sistem tetap hidup

4.6 Hubungan dengan Layer

Semua pattern di atas menjaga satu hal:

tidak ada eksekusi yang langsung menyentuh svc_ atau drv_
tanpa melalui app_

4.7 Ringkasan Implementation Pattern

Task → menjalankan app_
ISR  → hanya notify
Comm → hanya kirim data (queue)

Sehingga:

semua perubahan state sistem terjadi di satu tempat:
Application layer

5. Constraint & Embedded Impact

Concurrency discipline tidak boleh mengorbankan resource terbatas.


5.1 RAM Impact

FreeRTOS queue:

  • Menggunakan buffer internal.
  • Ukuran = item_size × length.

Jika queue tidak dibatasi:

  • RAM cepat habis.
  • Backlog tak terkendali saat WiFi down.

Dengan fixed-size queue:

  • Penggunaan RAM predictable.
  • Backpressure eksplisit.

5.2 Stack Impact

Setiap task memiliki stack sendiri.

Jika satu task:

  • Parsing JSON
  • Logging berat
  • TLS handshake

Stack bisa overflow.

Dengan task separation:

  • ControlTask stack kecil & stabil.
  • CommTask stack lebih besar.
  • Risiko terisolasi.

5.3 Determinism Impact

Tanpa boundary:

  • Callback bisa preempt control.
  • Blocking call menghentikan loop.
  • Latency spike tak terduga.

Dengan rule:

  • ControlTask hanya mengontrol.
  • CommTask menangani network.
  • ISR hanya notify.

Execution menjadi terstruktur.


5.4 Watchdog Impact

Jika ControlTask blocking:

  • Watchdog reset.
  • Device restart misterius.

Dengan non-blocking rule:

  • Feed watchdog predictable.
  • Hang lebih mudah dideteksi.

6. Failure Scenario

Concurrency bug jarang langsung terlihat.

Ia muncul di kondisi ekstrem.


Scenario 1 — MQTT Callback Mengubah Relay

Tanpa boundary:

void onMqttMessage(...) {
    actuator.setRelay(true);
}

Jika ControlTask juga mengubah relay:

  • Race condition.
  • Relay chatter.
  • Contact wear.

Dengan queue:

  • Callback kirim command.
  • ControlTask memutuskan.
  • Tidak ada konflik langsung.

Scenario 2 — ControlTask Publish Blocking

app.run() {
    comm.publish(data);   // TLS blocking
}

Jika broker lambat:

  • Control loop delay.
  • Watchdog reset.

Dengan task separation:

  • ControlTask hanya push data ke queue.
  • CommTask publish asynchronous.

Scenario 3 — Queue Tak Terbatas Saat WAN Down

WiFi down 20 menit.

Jika queue growable:

  • RAM habis.
  • Crash.

Dengan bounded queue:

  • Data lama di-drop.
  • Sistem tetap hidup.

Scenario 4 — ISR Logic Berat

ISR melakukan:

  • digitalWrite
  • logging
  • publish

Akibat:

  • Latency tinggi.
  • Preemption panjang.
  • Instabilitas sistem.

Dengan rule:

ISR hanya set flag / notify.


7. Anti-Pattern

Daftar merah Artikel 5:

  • ❌ ISR memanggil Service
  • ❌ MQTT callback mengubah hardware langsung
  • ❌ Blocking publish di ControlTask
  • ❌ Shared mutable tanpa owner
  • ❌ Queue tanpa batas
  • ❌ Mutex digunakan tanpa desain ownership jelas
  • ❌ Task tidak memiliki domain jelas

Dampak:

  • Race condition
  • Deadlock
  • Watchdog reset
  • Latency tidak deterministik

8. Freeze Point

Setelah Artikel 5, keputusan berikut dianggap final:

  • Task minimal dipisah domain (Control & Communication).
  • ISR hanya notify.
  • Semua komunikasi antar task via queue fixed-size.
  • ControlTask tidak boleh blocking.
  • Tidak ada shared mutable tanpa owner.

Concurrency model tidak boleh berubah di artikel berikutnya.

Artikel 6–8 harus patuh pada boundary ini.


9. Engineering Checklist

Audit sebelum release:

  • Apakah ISR memanggil service langsung?
  • Apakah callback network mengubah hardware?
  • Apakah ControlTask melakukan publish blocking?
  • Apakah queue dibatasi ukuran?
  • Apakah ada shared state tanpa owner task?
  • Apakah stack tiap task sudah diaudit?

Jika satu saja “ya” → melanggar Artikel 5.


10. Summary (5 Bullet Maksimal)

  • FreeRTOS bukan masalah; boundary yang salah adalah masalah.
  • ISR hanya boleh notify.
  • ControlTask tidak boleh blocking.
  • Queue harus bounded dan fixed-size.
  • Concurrency discipline menentukan determinism 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.