ui_bus
The lacte bus protocol between the Lacte UI process/thread and the Lacte backend (the milk dispenser controller) lives in LacteApp/bus/ModelBusClient.hpp (the backend/Model side) and LacteApp/bus/UiBusClient.hpp (the UI side) - message types in LacteApp/bus/Envelopes.hpp. Both are built directly on top of protolib's protocols/bus/Bus.hpp (insitech::bus::LocalClient - knows nothing about lacte). ModelBusClient/UiBusClient are where the actual protocol lives: serving the backend's request topics, listening for backend pushes, and issuing the UI's blocking requests are all methods on them — there's no separate "wiring" layer of free functions next to a pass-through wrapper.
The protocol covers:
- enabling/disabling the UI,
- getting/setting backend parameters (e.g. aeration),
- fetching a device snapshot (board online state, firmware/app versions, milk system serial number, expiry date, remaining percent, error flags),
- toggling a virtual test board, used for console/desktop testing when there is no real dispenser board attached.
Architecture
Two distinct client types, not one shared class trying to serve both roles: ModelBusClient (built and owned by Model itself, see Model::ConnectBus()) and UiBusClient (built and owned by UiApp itself, see UiApp::ConnectBus()). Both IS-A insitech::bus::LocalClient (see protolib's protocols/bus/Bus.hpp) and know nothing about bus mechanics themselves (framing, dispatch, addressing) - just the wire format for each of their own methods. Every client Connect()s onto the SAME one shared insitech::bus::Bus<RX,TX> node LacteApp owns (see AppContext::bus) at its own distinct address - LacteApp only ever builds that ONE shared bus node itself; every participant (Model, the UI, the DB) builds and configures its OWN client onto it, implementing its own subscriptions/publications, rather than being handed an already-built one:
protolib::bus (insitech::bus::Bus)
UI process/thread Backend process/thread
+---------------------+ requests (5 topics) +------------------------+
| ConsoleUi/ | --------------------> | Model |
| FramebufferUi | <-------------------- | (bus.ServeModel(this) |
| (bus.SubscribeRun- | replies | Model is the |
| time() + Check/ | | ModelBusHandler) |
| Get/Set) | | |
| | <-------------------- | |
| | pushed commands + | |
| | snapshots (2 topics) | |
+---------------------+ +------------------------+
- bus.ServeModel(handler) — call it once on the backend's ModelBusClient, with a ModelBusHandler& implementation (Model implements it directly, see Model::ConnectBus()). It advertises and serves the five request/reply topics. To push state to the UI whenever it changes, the backend also calls notifier.EnableUi(handler)/ notifier.DisableUi()/notifier.PublishSnapshot(handler) on a SECOND client it holds — a UiBusClient, not a ModelBusClient (that's UI protocol knowledge, so it lives on the UI's own client class regardless of which side calls it — see UiBusClient.hpp's file doc comment).
- bus.SubscribeRuntime(runtime) — call it once on the UI's UiBusClient, with a shared_ptr<UiRuntime> (the UI's cached enabled/aeration/snapshot state, updated thread-safely - only a weak_ptr is captured internally, so runtime's and the client's teardown order relative to each other doesn't matter). It subscribes to the two topics the backend pushes on. The UI issues blocking requests via bus.CheckHealth()/bus.SetParam()/bus.GetParam()/bus.GetSnapshot()/ bus.SetTestBoardOnline() — there's no separate client facade class: UiApp (see LacteApp/ui/UiApp.hpp) builds and owns the UiBusClient/UiRuntime pair via its constructor/ConnectBus()/Bus()/ Runtime(), and concrete UIs (ConsoleUi, FramebufferUi) call these methods directly on Bus()/Runtime().
Both sides communicate only through the message types in Envelopes.hpp and never share any C++ state directly — everything crosses the insitech::bus::Bus (an in-process EchoInterface-backed pair in tests, MulticastUdpInterface in the real app — see LacteApp/App.cpp).
Bus topics
Request/reply topics (UI -> backend)
| Topic (kTopic) | Request type | Reply type |
| lacte.backend.health | HealthRequest (no payload) | StatusReply |
| lacte.backend.param.get | ParamGetRequest{param} | ParamGetReply{ok, error, param, value} |
| lacte.backend.param.set | ParamSetRequest{param, value} | StatusReply |
| lacte.backend.snapshot.get | SnapshotRequest (no payload) | SnapshotReply{ok, error, snapshot} |
| lacte.backend.test.board.online.set | TestBoardOnlineRequest{online} | StatusReply (error is "not-available" or "rejected" on failure) |
Each request type owns its own wire encoding (Build()/Parse(), see Envelopes.hpp) — callers never touch bytes or topic strings. Reply-only types (StatusReply, ParamGetReply, SnapshotReply) have no kTopic of their own: the topic they travel on is the paired request's, via ModelBusClient::Serve<Req, Reply>()/UiBusClient::Request<Req, Reply>() (both inherited directly from insitech::bus::LocalClient).
BoardSnapshot's fields: board_online (bool), aeration (int), app_version, fw_version, milk_system_sn, expiry_date (strings), and, only when present, remaining_percent and error_flags (ints; absence means the corresponding std::optional field is std::nullopt).
Backend-pushed topics (backend -> UI)
| Topic (kTopic) | Message type | Meaning |
| lacte.ui.command | UiCommand{enable} | Backend telling the UI to enable or disable itself (notifier.EnableUi()/notifier.DisableUi()) |
| lacte.ui.snapshot | BoardSnapshotEvent{snapshot} | Backend pushing a fresh device snapshot (notifier.PublishSnapshot()) |
bus.SubscribeRuntime() subscribes to both: on UiCommand it calls UiRuntime::SetEnabled(); on BoardSnapshotEvent it calls UiRuntime::ApplySnapshot() followed by UiRuntime::SetAeration().
Usage
Backend side (as in LacteApp/tests/UiBusTest.cpp)
insitech::bus::Bus<rx_buffer, tx_buffer> shared_bus(100, proto::bus::make_identity("lacteApp", "lacteApp", 1, "lacte"));
shared_bus.set_interfaces(iface, iface);
auto bus = lacte::bus::ModelBusClient::Create(shared_bus, lacte::bus::kModelBusAddress, "lacte-model");
auto notifier = lacte::bus::UiBusClient::Create(shared_bus, 4, "lacte-model-ui-notifier");
callbacks.
health = [] {
return true; };
callbacks.
get_param = [&](std::string param) -> std::optional<int> {
if (param == lacte::bus::kParamAeration) return aeration;
return std::nullopt;
};
callbacks.
set_param = [&](std::string param,
int value) {
if (param != lacte::bus::kParamAeration) return false;
aeration = value;
return true;
};
return snapshot;
};
bus->ServeModel(callbacks);
notifier->EnableUi(callbacks);
notifier->PublishSnapshot(callbacks);
ModelBusClient: собственный клиент Model на шине lacte - создаётся и принадлежит самому Model (см.
UiBusClient: собственный клиент UI на шине lacte - создаётся и принадлежит самому UiApp (см.
ModelBusHandler, реализованный через пользовательские колбэки.
auto health() -> bool override
Здоров ли бэкенд/диспенсер.
auto set_param(const std::string &name, const int value) -> bool override
Записывает именованный параметр; возвращает, удалась ли запись.
auto get_param(const std::string &name) -> std::optional< int > override
Читает именованный параметр; nullopt, если параметр неизвестен.
auto get_snapshot() -> BoardSnapshot override
Формирует текущий снапшот устройства.
Моментальный снимок состояния диспенсера/платы, которым обмениваются бэкенд и UI (как запрос/ответ сн...
bool board_online
Доступна/онлайн ли диспенсерная плата в данный момент.
std::string app_version
Строка версии прикладной прошивки, выполняющейся на плате, если известна.
int aeration
Текущий уровень аэрации (в процентах).
UI side (as in LacteApp/ui/console/ConsoleUi.cpp)
auto bus = lacte::bus::UiBusClient::Create(shared_bus, lacte::bus::kUiBusAddress, "lacte-ui");
auto runtime = std::make_shared<lacte::bus::UiRuntime>(
[&](bool enabled) { },
[&](int value) { win->set_level(value); });
bus->SubscribeRuntime(runtime);
if (!bus->CheckHealth(*runtime, 300ms)) {
}
if (auto snapshot = bus->GetSnapshot(300ms)) {
runtime->ApplySnapshot(*snapshot);
}
if (auto aeration = bus->GetParam(lacte::bus::kParamAeration, 300ms)) {
runtime->SetAeration(*aeration);
}
bus->SetParam(lacte::bus::kParamAeration, runtime->Aeration(), 500ms);
bus->SetTestBoardOnline(true, 500ms);
In the real app, UiApp's constructor builds this UI's own UiBusClient (see LacteApp/ui/UiApp.hpp); UiApp::ConnectBus() builds the runtime and calls Bus().SubscribeRuntime(); concrete UIs call these methods directly on Bus()/Runtime().