lacteApp
C++17 service for Lacte hardware
Loading...
Searching...
No Matches
BoardModel.h
Go to the documentation of this file.
1#pragma once
2
25
26#include <algorithm>
27#include <array>
28#include <atomic>
29#include <cstdint>
30#include <functional>
31#include <memory>
32#include <mutex>
33#include <optional>
34#include <string>
35#include <thread>
36#include <tuple>
37
38#include "AppRuntime.hpp"
39#include "ILogger.hpp"
40#include "protocols/lacte/LacteProtocol.hpp"
41
42using namespace std::chrono_literals;
43
44namespace detail {
45// Превращает RuntimeFields (std::tuple<Field...> из простых типов полей)
46// в std::tuple<std::optional<Field>...> - форму, в которой хранятся данные
47// платы за один цикл опроса. Написано вручную намеренно, вместо того чтобы
48// напрямую называть insitech::runtime::Runtime<RuntimeFields>::Snapshot:
49// такой алиас заставил бы BoardModel.h зависеть от самого существования
50// runtime.hpp/Runtime - именно от той связанности, которую, согласно
51// собственному doc-комментарию этого файла, BoardModel не должен иметь.
52// Оба типа фактически являются буквально одной и той же инстанциацией
53// (та же форма tuple-из-optional, построенная из того же пакета
54// RuntimeFields), поэтому значение BoardData всё равно передаётся прямо в
55// LacteRuntime::Set() без какого-либо преобразования - см. собственный
56// callback OnUpdate() в LacteApp.cpp.
57template <typename Tuple>
59template <typename... Fields>
60struct BoardReadingOf<std::tuple<Fields...>> {
61 using type = std::tuple<std::optional<Fields>...>;
62};
63} // namespace detail
64
65// Значения полей платы за один цикл опроса - непустые optional'ы это
66// ровно те поля, что этот цикл действительно прочитал; всё остальное -
67// std::nullopt. О том, как вызывающий код должен потреблять эти данные,
68// см. собственный doc-комментарий BoardModel::OnUpdate().
70
71// Собственная частота опроса Worker() - раньше была 20мс (50Гц),
72// подтверждено вживую через `sample`, что именно она доминирующая причина
73// холостого потолка CPU у lacteApp (каждый тик вызывает get_info() через
74// host-протокол, который в режиме --virtual идёт круговым обходом через
75// EchoInterface). Прошли путь 200мс (5Гц) -> 1000мс (1Гц) -> остановились
76// на ~333мс (3Гц): при 1Гц измеренное CPU было ~0.2-0.4% (накопленная
77// дельта CPU-времени за живое окно в 60с), но это давало запас всего в 3
78// пропущенных цикла, прежде чем собственное значение по умолчанию
79// m_online_timeout_ (3000мс, см. doc-комментарий SetOnlineTimeout())
80// объявит плату оффлайн; 3Гц отдаёт часть этого запаса по CPU обратно в
81// обмен на запас уже в ~9 пропущенных циклов, что всё ещё значительно
82// дешевле базовых 50Гц.
83inline constexpr auto K_WORKER_POLL_INTERVAL = 333ms;
84
85// Небольшая пауза перед обращением к общим буферам протокола платы
86// (m_host_proto) - даёт уже начавшемуся обмену данными потока worker'а
87// устояться, прежде чем уйдёт запрос этого вызова, вместо гонки с ним.
89 std::chrono::milliseconds(100);
90// Таймаут "плата считается оффлайн" по умолчанию, используемый до того,
91// как собственное значение AppConfig K_BOARD_ONLINE_TIMEOUT_MS
92// переопределит его при старте (см. LacteApp::run_impl()).
93inline constexpr std::chrono::milliseconds K_MODEL_DEFAULT_ONLINE_TIMEOUT{3000};
94// Цикл повторных попыток для чтения/записи параметра, которое столкнулось
95// с занятостью самой платы - короткая задержка, немного попыток:
96// действительно оффлайн/сломанная плата должна отказать быстро, а не
97// задерживать вызывающий код.
98inline constexpr auto K_MODEL_PARAM_RETRY_DELAY = std::chrono::milliseconds(10);
99inline constexpr int K_MODEL_PARAM_RETRY_COUNT = 5;
100
101// Не шаблон: на процесс всегда есть только ОДИН BoardModel (LacteApp
102// владеет ровно одним), поэтому его обращённые к хосту временные буферы
103// (см. m_host_proto ниже) живут здесь как собственные статические члены
104// класса, вместо того чтобы передаваться как аргументы шаблона uint8_t*
105// тем, кто строит BoardModel - то же упрощение, что и у собственных
106// rx_buffer_/tx_buffer_ у AppContext (см.
107// Libraries/app_control/AppContext.h). Обращённые к плате буферы
108// (собственные у VirtualBoard, собственные у LacteApp/tests) - это
109// совершенно отдельная забота - BoardModel никогда их не трогает - так
110// что они остаются у того, кто владеет VirtualBoard, а не переезжают
111// тоже сюда.
113 public:
114 static constexpr std::size_t K_HOST_BUFFER_SIZE = 300;
115 // NOLINTBEGIN(modernize-avoid-c-arrays): должны оставаться C-массивами,
116 // не std::array - `LacteHostProtocol<RX,TX>` ниже принимает их как
117 // нетиповые параметры шаблона, для чего нужен объект со связыванием
118 // (linkage); обёртывание в std::array изменило бы идентичность шаблона,
119 // а не только способ доступа к ним.
120 inline static uint8_t host_rx_buffer[K_HOST_BUFFER_SIZE]{};
121 inline static uint8_t host_tx_buffer[K_HOST_BUFFER_SIZE]{};
122 // NOLINTEND(modernize-avoid-c-arrays)
123
124 proto::lacte::LacteHostProtocol<host_rx_buffer, host_tx_buffer> m_host_proto;
125
126 // ОДИН конструктор для обоих вызывающих: и `LoggerBusClient` (строится
127 // через `ctx.logger_bus.MakeClient(name)` - см. LacteApp.cpp), и
128 // обычный, не связанный с шиной `insitech::Logger` (например,
129 // LacteApp/tests/BoardModelTest.cpp, у которого вообще нет AppContext/
130 // шины) оба ЯВЛЯЮТСЯ `ILogger` (см.
131 // `Libraries/logger/include/ILogger.hpp`/`Logger.hpp`) - log()/
132 // LogStream() ниже просто идут через интерфейс, без ветвления по тому,
133 // какой конкретно вид получил этот BoardModel. Вызывающий код,
134 // передающий `Logger&`, владением которым он не хочет делиться с
135 // BoardModel, сам оборачивает его в невладеющий `shared_ptr` (см.
136 // BoardModelTest.cpp) - BoardModel в любом случае всегда хранит
137 // `shared_ptr<ILogger>`.
138 explicit BoardModel(std::shared_ptr<ILogger> log_sink)
139 : m_log_sink_(std::move(log_sink)) {}
140
141 auto get_aeration() -> int {
142 std::unique_lock lock(m_worker_mtx_);
143 std::this_thread::sleep_for(K_MODEL_BOARD_PROTO_THROTTLE_DELAY);
144 if (const auto aeration_packet =
145 m_host_proto.get_param<proto::lacte::Params::AERATION>()) {
146 return aeration_packet.value().m_data;
147 }
148 return 0;
149 }
150
151 auto set_aeration(const uint8_t aeration) {
152 proto::lacte::AerationParam packet;
153 packet.m_data = aeration;
154 std::unique_lock lock(m_worker_mtx_);
155 std::this_thread::sleep_for(K_MODEL_BOARD_PROTO_THROTTLE_DELAY);
156 return m_host_proto.set_param<proto::lacte::Params::AERATION>(packet);
157 }
158
159 auto flash(const std::string& path) -> bool {
160 std::unique_lock lock(m_worker_mtx_);
161 std::this_thread::sleep_for(K_MODEL_BOARD_PROTO_THROTTLE_DELAY);
162 if (auto info = m_host_proto.get_info()) {
163 if (info.value().m_status == proto::lacte::BoardStatus::WORK ||
164 info.value().m_status == proto::lacte::BoardStatus::CALIBRATION) {
165 log(LogLevel::INFO,
166 "Device has status WORK or CALIBRATION. It's denied to update");
167 return false;
168 }
169 log(LogLevel::INFO, "Device status is ", info.value().m_status);
170 std::this_thread::sleep_for(K_MODEL_BOARD_PROTO_THROTTLE_DELAY);
171 log(LogLevel::INFO, "Trying to reset board");
172 if (const auto answer = m_host_proto.restart(); not answer) {
173 log(LogLevel::WARNING,
174 "Failed to execute RESTART command; the device may already "
175 "be in bootloader mode.");
176 } else if (answer.value().m_data == proto::lacte::BootAnswer::NO) {
177 log(LogLevel::INFO,
178 "Device returned denied on RESTART command; aborting "
179 "firmware update.");
180 return false;
181 }
182 }
183 log(LogLevel::INFO, "Starting firmware update: ", path);
184 return proto::lacte::LacteHostProtocol<
185 host_rx_buffer, host_tx_buffer>::flash(m_log_sink_->get_stream(),
186 path.c_str(),
187 *m_flash_interface_);
188 }
189
190 ~BoardModel() { stop(); } // NOLINT(bugprone-exception-escape)
191
192 auto set_interfaces(proto::interface::IInterface& rx_interface,
193 proto::interface::IInterface& tx_interface) -> bool {
194 m_host_proto.set_interfaces(rx_interface, tx_interface);
195 m_flash_interface_ = &tx_interface;
196 return true;
197 }
198
199 // Задаёт интерфейс, в который пишет прошивка firmware. SetInterfaces уже
200 // делает это для обычно подключённой платы; этот метод нужен для случая,
201 // когда интерфейсы были подключены другим способом и требуется задать
202 // только цель для прошивки (например, виртуальная плата, подключённая
203 // через собственный set_host — см. LacteApp).
204 void set_flash_interface(proto::interface::IInterface& tx_interface) {
205 m_flash_interface_ = &tx_interface;
206 }
207
208 void start() {
209 std::unique_lock lock(m_worker_mtx_);
210 if (m_running_) {
211 log(LogLevel::WARNING, "model lacte is running. Please call stop firs!");
212 return;
213 }
214
215 m_running_ = true;
216 m_thread_worker_ = std::thread(&BoardModel::worker, this);
217 }
218
219 void stop() {
220 if (bool expected = true;
221 !m_running_.compare_exchange_strong(expected, false)) {
222 return; // уже остановлен, избегаем повторного выполнения
223 }
224
225 if (m_thread_worker_.joinable()) {
226 log(LogLevel::INFO, "Waiting for lacte model thread to finish...");
227 m_thread_worker_.join();
228 }
229 }
230
231 // По умолчанию — тот же порог, на который уже полагается каждый
232 // существующий вызывающий код (включая тестовый набор) — BoardModel
233 // ничего не знает об AppConfig, вызывающий код, которому нужна
234 // настраиваемость (см. LacteApp::RunImpl()), вызывает этот метод.
235 void set_online_timeout(const std::chrono::milliseconds timeout) {
236 m_online_timeout_ = timeout;
237 }
238
239 auto is_online() const -> bool {
240 return std::chrono::system_clock::now() - m_last_update_time_ <
241 m_online_timeout_;
242 }
243
244 // Был ли вызван Start() и ещё не выполнялся Stop() - используется,
245 // например, для собственного health() у LacteApp (см. реализацию
246 // ModelBusHandler в LacteApp.hpp), который раньше был собственным
247 // health() у BoardModel, прежде чем ModelBusHandler переехал на уровень
248 // выше.
249 [[nodiscard]] auto is_running() const -> bool { return m_running_.load(); }
250
251 // Регистрирует единственный callback, который Worker() вызывает один
252 // раз за цикл опроса со свежим показанием BoardData: непустые optional'ы
253 // это ровно те поля, что этот цикл действительно прочитал (либо, при
254 // переходе в оффлайн, просто BoardOnlineStatus{false}); все остальные
255 // поля оставлены пустыми. Это и есть целиком половина потока "плата
256 // обновила своё состояние, вернула новое состояние" - что происходит
257 // дальше (запись в Runtime, генерация diff/события) целиком дело
258 // вызывающего кода, а не BoardModel (см. собственный файловый
259 // doc-комментарий этого файла). Единственный слот callback'а, как и у
260 // самого insitech::runtime::Runtime::OnDiff() - регистрация нового
261 // заменяет предыдущий.
262 void on_update(std::function<void(const BoardData&)> callback) {
263 m_on_update_ = std::move(callback);
264 }
265
266 private:
267 std::thread m_thread_worker_;
268 std::atomic<bool> m_running_{false};
269 std::mutex m_worker_mtx_;
270 proto::interface::IInterface* m_flash_interface_{};
271 std::chrono::time_point<std::chrono::system_clock> m_last_update_time_;
272 std::chrono::milliseconds m_online_timeout_{K_MODEL_DEFAULT_ONLINE_TIMEOUT};
273 // Заменяет старую защитную проверку "спросить у Runtime его последний
274 // известный BoardOnlineStatus" (раньше для этого Worker() вызывал
275 // m_runtime_.get<BoardOnlineStatus>()) - BoardModel не может прочитать
276 // Runtime, которым он больше не владеет, поэтому отслеживает локально,
277 // сообщал ли он в последний раз, что плата онлайн. Семантика та же, что
278 // и раньше: сообщать о переходе в оффлайн только если хоть раз до этого
279 // действительно произошло успешное чтение.
280 bool m_reported_online_{false};
281 std::function<void(const BoardData&)> m_on_update_;
282
283 // СОБСТВЕННЫЙ приёмник логов этого BoardModel (см. собственный
284 // doc-комментарий конструктора) - `LoggerBusClient`, помечающий каждую
285 // строку тегом "[<name>]" на шине, либо обычный, не связанный с шиной
286 // `insitech::Logger` в тестах. log()/LogStream() ниже просто идут через
287 // него - какой конкретно вид это - здесь не важно.
288 std::shared_ptr<ILogger> m_log_sink_;
289
290 template <typename... Args>
291 void log(LogLevel level, Args&&... args) const {
292 m_log_sink_->log(level, std::forward<Args>(args)...);
293 }
294
295 // Считывает с платы свежие значения полей за один тик и возвращает их
296 // как единое показание BoardData - непустые optional'ы это ровно те
297 // поля, что этот цикл действительно прочитал. Объединение чтения
298 // нескольких полей (например, весь блок RFID ниже) в одну атомарную
299 // единицу изменения теперь дело вызывающего кода (см. собственный
300 // callback OnUpdate() в LacteApp.cpp, который оборачивает свои вызовы
301 // Runtime::Set() в один ReadCycle) - у самого BoardModel больше вообще
302 // нет понятия "цикл чтения"/diff/Runtime.
303 auto update_params(const proto::lacte::InfoPacketType& info) -> BoardData {
304 using namespace proto::lacte;
305
306 BoardData reading{};
307 auto set_field = [&reading](auto value) -> void {
308 std::get<std::optional<decltype(value)>>(reading) = std::move(value);
309 };
310
311 auto try_to_get = [&](auto&& func) -> decltype(func()) {
312 std::this_thread::sleep_for(K_MODEL_PARAM_RETRY_DELAY);
313 auto result = func();
314 int cnt = 0;
315 while (not result && cnt++ < K_MODEL_PARAM_RETRY_COUNT) {
316 std::this_thread::sleep_for(K_MODEL_PARAM_RETRY_DELAY);
317 result = func();
318 }
319 return result;
320 };
321
322 // Каждый вызов m_host_proto.get_*() ниже - это ограниченный по времени,
323 // но медленный круговой обход к плате (собственный RECEIVE_TIMEOUT у
324 // ProtocolEndpoint равен 1с; try_to_get повторяет попытку до 6 раз, так
325 // что одно поле может занять ~6с, если плата вообще не отвечает).
326 // Здесь ничего не держит никакой блокировки, кроме самого
327 // m_worker_mtx_ (уже удерживаемого Worker() вокруг всего этого
328 // вызова) - вызывающий код, проталкивающий это показание в Runtime
329 // (см. собственный callback OnUpdate() в LacteApp.cpp), видит его
330 // только один раз, уже полностью собранным ниже, никогда не в
331 // процессе чтения.
332 const auto hard_version =
333 try_to_get([&]() -> auto { return m_host_proto.get_version(); });
334 const auto uid =
335 try_to_get([&]() -> auto { return m_host_proto.get_uid(); });
336 const auto aeration = try_to_get(
337 [&]() -> auto { return m_host_proto.get_param<Params::AERATION>(); });
338
339 /******* заполнение rfid data start *******/
340 std::optional<RFIDDataPacketType> rfid_data;
341 std::optional<RFIDNumberType> rfid_num_new;
342 if (not(info.m_errors.has(BoardError::RFID_NO_CARD) and
343 info.m_errors.has(BoardError::RFID_BAD_CARD)) and
344 info.m_status != BoardStatus::CALIBRATION) {
345 rfid_data =
346 try_to_get([&]() -> auto { return m_host_proto.get_rfid_data(); });
347 rfid_num_new =
348 try_to_get([&]() -> auto { return m_host_proto.get_rfid(); });
349 }
350
351 set_field(info);
352 set_field(BoardErrorDiff{info.m_errors});
353
354 set_field(BoardOnlineStatus{"online"});
355 set_field(SoftVersion{SOFTWARE_VERSION});
356
357 if (hard_version) {
358 set_field(*hard_version);
359 }
360 if (uid) {
361 set_field(*uid);
362 }
363 if (aeration) {
364 set_field(*aeration);
365 }
366
367 if (rfid_num_new and rfid_num_new.value().to_string() != "0" and
368 rfid_data and rfid_data->m_magicWord.m_data == MagicWord::DEFAULT_VAL) {
369 set_field(*rfid_num_new);
370 set_field(rfid_data->m_magicWord);
371 set_field(rfid_data->m_lacteId);
372 set_field(rfid_data->m_lacteSn);
373 set_field(rfid_data->m_productVolume);
374 set_field(rfid_data->m_prodDate);
375 set_field(rfid_data->m_shelfLife);
376 set_field(rfid_data->m_usageTime);
377 set_field(rfid_data->m_mcuUid);
378 set_field(rfid_data->m_machineSn);
379 set_field(rfid_data->m_activationTime);
380 set_field(rfid_data->m_milkCounter);
381 set_field(rfid_data->m_timeCounter);
382 } else if (rfid_num_new and rfid_num_new.value().to_string() == "0") {
383 // LACTE-BUG-001 (исправлено): метка реально отсутствует - сообщить
384 // об этом ЯВНО (RFIDNumberType{0}), а не просто пропустить это
385 // поле. Раньше, когда метку убирали, ЭТА ветка отсутствовала
386 // целиком - set_field(*rfid_num_new) не вызывался вообще ни для
387 // какого значения RFIDNumberType, значит Runtime::Set() (пишет
388 // только непустые optional'ы из BoardData) никогда не видел
389 // переход "метка была -> метки нет", и LacteId/ProductVolume/
390 // MilkCounter/... (объявленные в LacteApp::GetRfidData()/
391 // BuildSnapshot() как признак присутствия метки) оставались
392 // зависшими на данных предыдущей метки сколь угодно долго после
393 // её физического извлечения. LacteId/ProductVolume/... сами по
394 // себе всё ещё не сбрасываются здесь (RFIDDataPacketType не
395 // читается вовсе, когда метки нет) - именно поэтому
396 // GetRfidData()/BuildSnapshot() должны сами проверять ИМЕННО
397 // текущее значение RFIDNumberType (см. их собственные
398 // doc-комментарии), а не наличие/отсутствие какого-либо другого
399 // производного от RFID поля.
400 set_field(*rfid_num_new);
401 }
402 /******* заполнение rfid data end *******/
403
404 // Старая проверка "вернуть rfid_num к прежнему значению, если
405 // проскользнул свежепрочитанный 0" намеренно не воспроизведена:
406 // теперь свежий "0" явно доходит до BoardData через ветку выше -
407 // именно это и является исправлением LACTE-BUG-001, а не воссозданием
408 // старой защитной проверки.
409 return reading;
410 }
411
412 void worker() {
413 log(LogLevel::INFO, "lacte model started ");
414 while (m_running_) {
415 std::this_thread::sleep_for(K_WORKER_POLL_INTERVAL);
416 std::unique_lock lock(m_worker_mtx_);
417 if (auto info = m_host_proto.get_info()) {
418 m_last_update_time_ = std::chrono::system_clock::now();
419 m_reported_online_ = true;
420 if (const auto reading = update_params(*info); m_on_update_) {
421 m_on_update_(reading);
422 }
423 } else if (not is_online() and m_reported_online_) {
424 // Точно повторяет старую защитную проверку: действовать, только
425 // если плата *ранее была известна как онлайн* — плата, которая ни
426 // разу не была успешно прочитана, не должна вызывать ложное
427 // сообщение "отключена".
428 m_reported_online_ = false;
429 log(LogLevel::WARNING,
430 "lacte board was disconnected or there is some "
431 "communication issue!");
432 if (m_on_update_) {
433 BoardData reading{};
434 std::get<std::optional<BoardOnlineStatus>>(reading) =
435 BoardOnlineStatus{"false"};
436 m_on_update_(reading);
437 }
438 }
439 }
440 log(LogLevel::INFO, "lacte model stopped ");
441 }
442};
Собственные типы runtime-полей платы и кортеж RuntimeFields, называющий весь набор - и ничего больше.
constexpr auto K_MODEL_BOARD_PROTO_THROTTLE_DELAY
Definition BoardModel.h:88
constexpr int K_MODEL_PARAM_RETRY_COUNT
Definition BoardModel.h:99
typename detail::BoardReadingOf< RuntimeFields >::type BoardData
Definition BoardModel.h:69
constexpr auto K_WORKER_POLL_INTERVAL
Definition BoardModel.h:83
constexpr std::chrono::milliseconds K_MODEL_DEFAULT_ONLINE_TIMEOUT
Definition BoardModel.h:93
constexpr auto K_MODEL_PARAM_RETRY_DELAY
Definition BoardModel.h:98
auto is_online() const -> bool
Definition BoardModel.h:239
void stop()
Definition BoardModel.h:219
auto set_interfaces(proto::interface::IInterface &rx_interface, proto::interface::IInterface &tx_interface) -> bool
Definition BoardModel.h:192
void set_flash_interface(proto::interface::IInterface &tx_interface)
Definition BoardModel.h:204
auto flash(const std::string &path) -> bool
Definition BoardModel.h:159
static uint8_t host_rx_buffer[K_HOST_BUFFER_SIZE]
Definition BoardModel.h:120
auto get_aeration() -> int
Definition BoardModel.h:141
void on_update(std::function< void(const BoardData &)> callback)
Definition BoardModel.h:262
auto set_aeration(const uint8_t aeration)
Definition BoardModel.h:151
void start()
Definition BoardModel.h:208
void set_online_timeout(const std::chrono::milliseconds timeout)
Definition BoardModel.h:235
static constexpr std::size_t K_HOST_BUFFER_SIZE
Definition BoardModel.h:114
auto is_running() const -> bool
Definition BoardModel.h:249
BoardModel(std::shared_ptr< ILogger > log_sink)
Definition BoardModel.h:138
proto::lacte::LacteHostProtocol< host_rx_buffer, host_tx_buffer > m_host_proto
Definition BoardModel.h:124
static uint8_t host_tx_buffer[K_HOST_BUFFER_SIZE]
Definition BoardModel.h:121
std::tuple< std::optional< Fields >... > type
Definition BoardModel.h:61