- 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
- 📘 Artikel 5: OOP + FreeRTOS Tanpa Membunuh Determinism
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 boundaryControlApp→ 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.