# lvgl-msg-bus
Thread-safe **message bus** and **reactive data store** for ESP-IDF + LVGL applications.
Designed for embedded systems where hardware tasks need to exchange data with a UI layer running in the LVGL task — safely, efficiently, and with minimal boilerplate.
## Features
| Feature | Description |
|---------|-------------|
| **Publish / Subscribe** | Topic-based message bus with `uint32_t` topic IDs for O(1) lookup. |
| **LVGL-thread dispatch** | `LvglAsync` delivery mode uses `lv_async_call()` — subscribers safely update widgets without manual locking. |
| **Reactive DataStore** | Thread-safe key-value store that auto-publishes change notifications. |
| **RAII Subscriptions** | `Subscription` and `SubscriptionGroup` automatically unsubscribe on destruction. |
| **Zero framework lock-in** | Pure FreeRTOS + LVGL — no dependency on a specific board or display driver. |
## Quick Start
### Installation
**As a local component** (recommended during development):
```
your_project/
├── components/
│ └── lvgl-msg-bus/ ← clone or copy here
├── main/
│ └── ...
└── CMakeLists.txt
```
**As a managed component** (after publishing):
```yaml
# main/idf_component.yml
dependencies:
txp666/lvgl-msg-bus: "^1.0.0"
```
### Initialise
Call once at startup, before creating any subscriptions:
```cpp
#include "lvgl_msg_bus/message_bus.h"
#include "lvgl_msg_bus/data_store.h"
// In app_main() or board init — after lv_init().
msgbus::MessageBus::GetInstance().Initialize();
msgbus::DataStore::GetInstance().Initialize();
```
### Define topics
```cpp
// msg_topics.h (application-specific)
namespace Topic {
constexpr uint32_t SensorData = 0x0001;
constexpr uint32_t BatteryStatus = 0x0002;
constexpr uint32_t WifiStatus = 0x0003;
}
```
### Publish from a hardware task
```cpp
#include "lvgl_msg_bus/message_bus.h"
#include "msg_topics.h"
struct SensorReading { float current_ma; uint8_t channel; };
void adc_task(void*) {
while (true) {
SensorReading r = { .current_ma = read_adc(), .channel = 0 };
msgbus::MessageBus::GetInstance().Publish(Topic::SensorData, r);
vTaskDelay(pdMS_TO_TICKS(200));
}
}
```
### Subscribe in a UI page
```cpp
#include "lvgl_msg_bus/message_bus.h"
#include "lvgl_msg_bus/subscription.h"
#include "msg_topics.h"
class HomePage : public PageBase {
msgbus::Subscription sensor_sub_;
void OnCreate(lv_obj_t* parent) override {
sensor_sub_ = msgbus::Subscription(
msgbus::MessageBus::GetInstance().Subscribe(
Topic::SensorData,
[this](const msgbus::Message& msg) {
// Runs in LVGL thread — safe to update widgets.
auto& r = msg.As<SensorReading>();
lv_label_set_text_fmt(label_, "%.2f mA", r.current_ma);
}));
}
// sensor_sub_ auto-unsubscribes when the page is destroyed.
};
```
### Use the DataStore for shared state
```cpp
#include "lvgl_msg_bus/data_store.h"
// Hardware side — any task:
msgbus::DataStore::GetInstance().Set(Topic::BatteryStatus, battery_pct);
// UI side — watch for changes (callback in LVGL thread):
battery_watch_ = msgbus::Subscription(
msgbus::DataStore::GetInstance().Watch(Topic::BatteryStatus,
[this](uint32_t key) {
int pct = 0;
msgbus::DataStore::GetInstance().Get(key, pct);
lv_label_set_text_fmt(bat_label_, "%d%%", pct);
}));
```
## API Reference
### MessageBus
| Method | Description |
|--------|-------------|
| `Initialize(config)` | One-time init. Optional `BusConfig` to tune capacity. |
| `Subscribe(topic, cb, mode)` | Register a callback. Returns `SubscriptionId`. |
| `Unsubscribe(id)` | Remove a subscription. |
| `Publish(topic, data, size)` | Send a message to all matching subscribers. |
| `Publish<T>(topic, value)` | Typed convenience wrapper. |
### DataStore
| Method | Description |
|--------|-------------|
| `Initialize(config, topic_base)` | One-time init. `topic_base` offsets change-notification topics. |
| `Set<T>(key, value)` | Store a value; publishes notification if changed. |
| `Get<T>(key, out)` | Read a value. Returns `false` if key not found. |
| `Watch(key, callback)` | Subscribe to changes (LVGL thread). Returns `SubscriptionId`. |
| `Unwatch(id)` | Cancel a watch. |
| `Contains(key)` | Check if a key exists. |
| `Remove(key)` | Delete a key. |
### Subscription / SubscriptionGroup
| Class | Description |
|-------|-------------|
| `Subscription` | RAII guard for a single subscription. Move-only. |
| `SubscriptionGroup` | Holds multiple `Subscription` objects; clears all on destruction. |
### DeliveryMode
| Value | Behaviour |
|-------|-----------|
| `Immediate` | Callback runs synchronously in the publisher's thread. |
| `LvglAsync` | Callback dispatched to the LVGL task via `lv_async_call()`. |
## Thread Safety
- `Subscribe()`, `Unsubscribe()`, `Publish()` — safe from any FreeRTOS task.
- `DataStore::Set()`, `Get()`, `Contains()`, `Remove()` — safe from any task.
- **Not ISR-safe** — do not call from interrupt handlers.
- `LvglAsync` callbacks execute in the LVGL task context, so widget operations are safe without additional locking.
## Requirements
- ESP-IDF >= 5.0
- LVGL >= 9.0
## License
MIT — see [LICENSE](LICENSE).
fec2934c5116f1bc5007ee88b2d5c761738fdab7
idf.py add-dependency "txp666/lvgl-msg-bus^1.0.0"