From db84cdda5e6b47572dc48f360b810f93d70131e2 Mon Sep 17 00:00:00 2001 From: Maxim Prokhorov Date: Wed, 2 Apr 2025 23:03:30 +0300 Subject: [PATCH] webui(ws): try to contain wsSend to the queue callback - 'log' payload nesting reduced, just one element (for now) - move terminal and ws logger into ws helper classes - 'manual' callback mode to simply allow for ws send window by using slightly more complicated loop w/ queue checks, though --- code/espurna/terminal.cpp | 128 +++-------- code/espurna/terminal_parsing.h | 2 +- code/espurna/ws.cpp | 372 +++++++++++++++++++++++++------- code/espurna/ws.h | 21 +- code/espurna/ws_internal.h | 27 ++- code/espurna/ws_utils.h | 118 ++++++++++ code/html/src/debug.mjs | 9 +- 7 files changed, 474 insertions(+), 203 deletions(-) diff --git a/code/espurna/terminal.cpp b/code/espurna/terminal.cpp index a936373a..c19046a4 100644 --- a/code/espurna/terminal.cpp +++ b/code/espurna/terminal.cpp @@ -549,124 +549,44 @@ void setup() { #if WEB_SUPPORT namespace web { -struct Output { - static constexpr auto Timeout = espurna::duration::Seconds(2); - static constexpr auto Wait = espurna::duration::Milliseconds(100); - static constexpr int Limit { 8 }; - - Output() = delete; - Output(const Output&) = default; - Output(Output&&) = default; - - explicit Output(uint32_t id) : - _id(id) - {} - - ~Output() { - send(); - } - - void operator()(const char* line) { - if (wsConnected(_id)) { - if ((_count > Limit) && !send()) { - return; - } - - ++_count; - _output += line; - } - } - - void clear() { - _output = String(); - _count = 0; - } - - bool send() { - if (!_count || !_output.length()) { - clear(); - return false; - } - - if (!wsConnected(_id)) { - clear(); - return false; - } - - using Clock = time::CoreClock; - - auto start = Clock::now(); - bool ready { false }; - - while (Clock::now() - start < Timeout) { - auto info = wsClientInfo(_id); - if (!info.connected) { - clear(); - return false; - } - - if (!info.stalled) { - ready = true; - break; - } - - time::blockingDelay(Wait); - } - - if (ready) { - DynamicJsonBuffer buffer((2 * JSON_OBJECT_SIZE(1)) + JSON_ARRAY_SIZE(1)); - - JsonObject& root = buffer.createObject(); - JsonObject& log = root.createNestedObject("log"); - - JsonArray& msg = log.createNestedArray("msg"); - msg.add(_output.c_str()); - - wsSend(root); - clear(); - - return true; - } - - clear(); - return false; - } - -private: - String _output; - uint32_t _id { 0 }; - int _count { 0 }; -}; - -constexpr espurna::duration::Seconds Output::Timeout; -constexpr espurna::duration::Milliseconds Output::Wait; - STRING_VIEW_INLINE(Prefix, "cmd"); +using Output = PrintLine; + +struct Command { + String line; + uint32_t id; +}; + void onVisible(JsonObject& root) { wsPayloadModule(root, Prefix); } void onAction(uint32_t client_id, const char* action, JsonObject& data) { - PROGMEM_STRING(Cmd, "cmd"); - if (strncmp_P(action, &Cmd[0], __builtin_strlen(Cmd)) != 0) { + STRING_VIEW_INLINE(Cmd, "cmd"); + if (Cmd != action) { return; } - PROGMEM_STRING(Line, "line"); - if (!data.containsKey(FPSTR(Line)) || !data[FPSTR(Line)].is()) { + STRING_VIEW_INLINE(Line, "line"); + auto cmd = Command{ + .line = data[Line].as(), + .id = client_id, + }; + + if (!cmd.line.length()) { return; } - const auto cmd = std::make_shared( - data[FPSTR(Line)].as()); - if (!cmd->length()) { - return; - } + const auto shared = + std::make_shared(std::move(cmd)); - espurnaRegisterOnce([cmd, client_id]() { - PrintLine out(client_id); - api_find_and_call(*cmd, out); + espurnaRegisterOnce([shared]() { + wsPostManual(shared->id, + [shared](JsonObject& root) { + Output out(root, shared->id); + api_find_and_call(shared->line, out); + }); }); } diff --git a/code/espurna/terminal_parsing.h b/code/espurna/terminal_parsing.h index 7d765179..d799ea37 100644 --- a/code/espurna/terminal_parsing.h +++ b/code/espurna/terminal_parsing.h @@ -95,7 +95,7 @@ public: private: void send() { - _output(buffer()); + _output.write(buffer(), length()); } T _output; diff --git a/code/espurna/ws.cpp b/code/espurna/ws.cpp index d74110b4..60ad434b 100644 --- a/code/espurna/ws.cpp +++ b/code/espurna/ws.cpp @@ -154,6 +154,220 @@ void EnumerableTypes::operator()(int value, StringView text) { entry.add(text); } +PostponedPayload::PostponedPayload() = default; + +PostponedPayload::Flag::~Flag() { + if (_ref._pending) { + _ref._data = String(); + _ref._count = 0; + } + + _ref._pending = false; +} + +PostponedPayload::Flag::Flag(PostponedPayload& ref) : + _ref(ref) +{ + _ref._pending = true; +} + +bool PostponedPayload::connected() const { + return _id ? wsConnected(_id) : wsConnected(); +} + +std::shared_ptr PostponedPayload::make_flag() { + if (!_pending) { + return std::make_shared(*this); + } + + + return nullptr; +} + +bool PostponedPayload::post(bool connected) { + if (!connected) { + if (_data.length()) { + _data = String(); + } + + _count = 0; + _pending = false; + + return false; + } + + if (connected && !_pending && _count) { + auto flag = make_flag(); + + wsPost(_id, [flag](JsonObject& root) { + if (flag->pending()) { + auto& log = root.createNestedArray("log"); + log.add(flag->data()); + } + }); + + return true; + } + + return false; +} + +bool PostponedPayload::post() { + return post(connected()); +} + +void PostponedPayload::buffer(StringView data) { + if (!connected()) { + return; + } + + if (_count < CountMax) { + buffer_impl(data); + ++_count; + } + + post(); +} + +void PostponedPayload::buffer_impl(StringView data) { + _data.concat(data.data(), data.length()); +} + +void PostponedPayload::buffer_impl(const char* data, size_t length) { + _data.concat(data, length); +} + +void PostponedDebug::buffer(const DebugPrefix& prefix, StringView message) { + if (!connected()) { + return; + } + + if (_count < CountMax) { + const auto prefixLen = debugPrefixLength(prefix); + const auto bufferLen = _data.length() + + prefixLen + message.length(); + + _data.reserve(bufferLen); + + if (prefixLen) { + buffer_impl(prefix, prefixLen); + } + + buffer_impl(message); + ++_count; + } + + post(); +} + +constexpr duration::Milliseconds InplacePayload::DefaultWait; +constexpr duration::Seconds InplacePayload::DefaultTimeout; + +InplacePayload::InplacePayload(JsonObject& root, uint32_t id) : + _root(root), + _id(id) +{} + +void InplacePayload::reset() { + if (_data.length()) { + _data = String(); + } + + _count = 0; +} + +bool InplacePayload::connected() const { + return wsConnected(_id); +} + +void InplacePayload::write_impl(const char* data, size_t length) { + if (!connected()) { + return; + } + + if (_count > CountMax) { + return; + } + + _data.concat(data, length); + ++_count; +} + +void InplacePayload::write(const char* data, size_t length) { + write_impl(data, length); + send(); +} + +bool InplacePayload::can_send() const { + return connected() && _count && _data.length(); +} + +bool InplacePayload::poll_send() { + if (!can_send()) { + reset(); + return false; + } + + auto start = Clock::now(); + + while (Clock::now() - start < _timeout) { + auto info = wsClientInfo(_id); + if (!info.connected) { + reset(); + return false; + } + + if (!info.stalled) { + return true; + } + + time::blockingDelay(_wait); + } + + return false; +} + +bool InplacePayload::send() { + if (poll_send()) { + send_impl(); + return true; + } + + return false; +} + +void InplacePayload::send_impl() { + wsSend(_id, _root); + reset(); +} + +InplaceLog::InplaceLog(JsonObject& root, uint32_t id) : + InplacePayload(root, id) +{ + _root.createNestedArray("log"); +} + +void InplaceLog::write(const char* data, size_t length) { + write_impl(data, length); + send(); +} + +bool InplaceLog::send() { + if (poll_send()) { + JsonArray& log = _root["log"]; + if (log.size()) { + log[0] = _data.c_str(); + } else { + log.add(_data.c_str()); + } + + send_impl(); + return true; + } + + return false; +} + } // namespace ws } // namespace web } // namespace espurna @@ -275,6 +489,22 @@ void wsPost(const ws_on_send_callback_f& cb) { wsPost(0, cb); } +void wsPostManual(uint32_t client_id, ws_on_send_callback_f&& cb) { + _ws_queue.emplace(client_id, std::move(cb), WsPostponedCallbacks::Mode::ManualAll); +} + +void wsPostManual(ws_on_send_callback_f&& cb) { + wsPostManual(0, std::move(cb)); +} + +void wsPostManual(uint32_t client_id, const ws_on_send_callback_f& cb) { + _ws_queue.emplace(client_id, cb, WsPostponedCallbacks::Mode::ManualAll); +} + +void wsPostManual(const ws_on_send_callback_f& cb) { + wsPostManual(0, cb); +} + namespace { template @@ -316,6 +546,38 @@ void wsPostSequence(const ws_on_send_callback_list_t& cbs) { wsPostSequence(0, cbs); } +void wsPostManualAll(uint32_t client_id, ws_on_send_callback_list_t&& cbs) { + _wsPostCallbacks(client_id, std::move(cbs), WsPostponedCallbacks::Mode::ManualAll); +} + +void wsPostManualAll(ws_on_send_callback_list_t&& cbs) { + wsPostManualAll(0, std::move(cbs)); +} + +void wsPostManualAll(uint32_t client_id, const ws_on_send_callback_list_t& cbs) { + _wsPostCallbacks(client_id, cbs, WsPostponedCallbacks::Mode::ManualAll); +} + +void wsPostManualAll(const ws_on_send_callback_list_t& cbs) { + wsPostManualAll(0, cbs); +} + +void wsPostManualSequence(uint32_t client_id, ws_on_send_callback_list_t&& cbs) { + _wsPostCallbacks(client_id, std::move(cbs), WsPostponedCallbacks::Mode::ManualSequence); +} + +void wsPostManualSequence(ws_on_send_callback_list_t&& cbs) { + wsPostManualSequence(0, std::move(cbs)); +} + +void wsPostManualSequence(uint32_t client_id, const ws_on_send_callback_list_t& cbs) { + _wsPostCallbacks(client_id, cbs, WsPostponedCallbacks::Mode::ManualSequence); +} + +void wsPostManualSequence(const ws_on_send_callback_list_t& cbs) { + wsPostManualSequence(0, cbs); +} + // ----------------------------------------------------------------------------- ws_callbacks_t& ws_callbacks_t::onVisible(ws_callbacks_t::on_send_f cb, ws_callbacks_t::Prepend) { @@ -443,78 +705,13 @@ bool _wsAuth(AsyncWebSocketClient* client) { namespace { -struct WsDebug { - static constexpr int Limit { 8 }; - - WsDebug() = default; - WsDebug(const WsDebug&) = delete; - WsDebug(WsDebug&&) = delete; - - void clear() { - _buffer = String(); - _count = 0; - } - - void operator()(const DebugPrefix& prefix, espurna::StringView message) { - if (wsConnected()) { - if ((_count > Limit) && !send()) { - return; - } - - const auto prefixLen = debugPrefixLength(prefix); - _buffer.reserve(_buffer.length() - + prefixLen + message.length()); - - if (prefixLen) { - _buffer.concat(prefix, prefixLen); - } - _buffer.concat(message.data(), message.length()); - - ++_count; - } - } - - bool send(bool connected) { - if (!connected && (_count || _buffer.length())) { - clear(); - return false; - } - - // ref: http://arduinojson.org/v5/assistant/ for pre-allocation math - if (_count && connected) { - DynamicJsonBuffer buffer((2 * JSON_OBJECT_SIZE(1)) + JSON_ARRAY_SIZE(1)); - - JsonObject& root = buffer.createObject(); - JsonObject& log = root.createNestedObject("log"); - - JsonArray& msg = log.createNestedArray("msg"); - msg.add(_buffer.c_str()); - - wsSend(root); - clear(); - - return true; - } - - return false; - } - - bool send() { - return send(wsConnected()); - } - -private: - String _buffer; - int _count { 0 }; -}; - -WsDebug _ws_debug; +espurna::web::ws::PostponedDebug _ws_debug; } // namespace bool wsDebugSend(const DebugPrefix& prefix, espurna::StringView message) { if ((wifiConnected() || wifiApStations()) && wsConnected()) { - _ws_debug(prefix, message); + _ws_debug.buffer(prefix, message); return true; } @@ -797,7 +994,10 @@ void _wsHandlePostponedCallbacks(bool connected) { return; } - if (_ws_queue.empty()) return; + if (_ws_queue.empty()) { + return; + } + auto& callbacks = _ws_queue.front(); // avoid stalling forever when can't send anything @@ -834,13 +1034,25 @@ void _wsHandlePostponedCallbacks(bool connected) { DynamicJsonBuffer jsonBuffer(WsQueueJsonBufferSize); JsonObject& root = jsonBuffer.createObject(); + using Mode = decltype(callbacks.mode()); + callbacks.send(root); - if (callbacks.id()) { - wsSend(callbacks.id(), root); - } else { - wsSend(root); + + switch (callbacks.mode()) { + case Mode::ManualAll: + case Mode::ManualSequence: + break; + + case Mode::All: + case Mode::Sequence: + if (callbacks.id()) { + wsSend(callbacks.id(), root); + } else { + wsSend(root); + } + yield(); + break; } - yield(); if (callbacks.done()) { _ws_queue.pop(); @@ -848,12 +1060,9 @@ void _wsHandlePostponedCallbacks(bool connected) { } void _wsLoop() { - const bool connected = wsConnected(); + const auto connected = wsConnected(); _wsDoUpdate(connected); _wsHandlePostponedCallbacks(connected); - #if DEBUG_WEB_SUPPORT - _ws_debug.send(connected); - #endif } } // namespace @@ -865,11 +1074,10 @@ void _wsLoop() { WsClientInfo wsClientInfo(uint32_t client_id) { auto* client = _ws.client(client_id); - WsClientInfo out; - out.connected = (client != nullptr); - out.stalled = out.connected && client->queueIsFull(); - - return out; + return WsClientInfo{ + .connected = (client != nullptr), + .stalled = (client != nullptr) && client->queueIsFull(), + }; } bool wsConnected() { diff --git a/code/espurna/ws.h b/code/espurna/ws.h index 80c2e782..12287fe9 100644 --- a/code/espurna/ws.h +++ b/code/espurna/ws.h @@ -99,7 +99,11 @@ bool wsDebugSend(const DebugPrefix&, espurna::StringView); // There are two policies set on how to send the data: // - All will use the same JsonObject for each callback // - Sequence will use a different JsonObject for each callback -// Default is All +// - Manual works similarly to All & Sequence +// However, it is up to the user to call `wsSend()` +// +// `wsPost()` & `wsPostManual()` callback uses *All mode by default +// (however, it is not currently possible to amend the created list object) // // WARNING: callback lists are taken by reference! make sure that list is ether: // - std::move(...)'ed to give control of the callback list to us @@ -121,6 +125,21 @@ void wsPostSequence(ws_on_send_callback_list_t&& cbs); void wsPostSequence(uint32_t client_id, const ws_on_send_callback_list_t& cbs); void wsPostSequence(const ws_on_send_callback_list_t& cbs); +void wsPostManual(uint32_t client_id, ws_on_send_callback_f&& cb); +void wsPostManual(ws_on_send_callback_f&& cb); +void wsPostManual(uint32_t client_id, const ws_on_send_callback_f& cb); +void wsPostManual(ws_on_send_callback_f& cb); + +void wsPostManualAll(uint32_t client_id, ws_on_send_callback_list_t&& cbs); +void wsPostManualAll(ws_on_send_callback_list_t&& cbs); +void wsPostManualAll(uint32_t client_id, const ws_on_send_callback_list_t& cbs); +void wsPostManualAll(ws_on_send_callback_list_t& cbs); + +void wsPostManualSequence(uint32_t client_id, ws_on_send_callback_list_t&& cs); +void wsPostManualSequence(ws_on_send_callback_list_t&& cs); +void wsPostManualSequence(uint32_t client_id, const ws_on_send_callback_list_t& cs); +void wsPostManualSequence(ws_on_send_callback_list_t& cbs); + // Immmediatly try to serialize and send JsonObject& // May silently fail when network is busy sending previous requests, or there's not enough RAM diff --git a/code/espurna/ws_internal.h b/code/espurna/ws_internal.h index 22e7fa2f..88066490 100644 --- a/code/espurna/ws_internal.h +++ b/code/espurna/ws_internal.h @@ -42,30 +42,35 @@ public: enum class Mode { Sequence, - All + ManualSequence, + All, + ManualAll, }; - WsPostponedCallbacks(uint32_t client_id, ws_on_send_callback_f&& cb) : + WsPostponedCallbacks(uint32_t client_id, ws_on_send_callback_f&& cb, Mode mode = Mode::All) : _client_id(client_id), _timestamp(TimeSource::now()), - _mode(Mode::All), + _mode(mode), _storage(new ws_on_send_callback_list_t {std::move(cb)}), _callbacks(*_storage.get()), _current(_callbacks.begin()) {} - WsPostponedCallbacks(uint32_t client_id, const ws_on_send_callback_f& cb) : + explicit WsPostponedCallbacks(ws_on_send_callback_f&& cb) : + WsPostponedCallbacks(0, std::move(cb)) + {} + + WsPostponedCallbacks(uint32_t client_id, const ws_on_send_callback_f& cb, Mode mode = Mode::All) : _client_id(client_id), _timestamp(TimeSource::now()), - _mode(Mode::All), + _mode(mode), _storage(new ws_on_send_callback_list_t {cb}), _callbacks(*_storage.get()), _current(_callbacks.begin()) {} - template - explicit WsPostponedCallbacks(T&& cb) : - WsPostponedCallbacks(0, std::forward(cb)) + explicit WsPostponedCallbacks(const ws_on_send_callback_f& cb) : + WsPostponedCallbacks(0, cb) {} WsPostponedCallbacks(uint32_t client_id, const ws_on_send_callback_list_t& cbs, Mode mode = Mode::Sequence) : @@ -105,9 +110,11 @@ public: void send(JsonObject& root) { switch (_mode) { case Mode::Sequence: + case Mode::ManualSequence: sendCurrent(root); break; case Mode::All: + case Mode::ManualAll: sendAll(root); break; } @@ -121,6 +128,10 @@ public: return _timestamp; } + Mode mode() const { + return _mode; + } + private: uint32_t _client_id; TimeSource::time_point _timestamp; diff --git a/code/espurna/ws_utils.h b/code/espurna/ws_utils.h index f22a5952..d537361e 100644 --- a/code/espurna/ws_utils.h +++ b/code/espurna/ws_utils.h @@ -40,6 +40,7 @@ Copyright (C) 2019-2021 by Maxim Prokhorov +#include "system_time.h" #include "settings.h" namespace espurna { @@ -144,6 +145,123 @@ private: JsonArray& _root; }; +struct PostponedDebug; + +struct PostponedPayload { + static constexpr size_t CountMax { 8 }; + + struct Flag { + explicit Flag(PostponedPayload&); + ~Flag(); + + Flag(const Flag&) = delete; + Flag& operator=(const Flag&) = delete; + + Flag(Flag&&) = delete; + Flag& operator=(Flag&&) = delete; + + const char* data() const { + return _ref._data.c_str(); + } + + bool pending() const { + return _ref.pending(); + } + + private: + PostponedPayload& _ref; + }; + + PostponedPayload(); + explicit PostponedPayload(uint32_t id) : + _id(id) + {} + + bool pending() const { + return _pending; + } + + bool post(bool connected); + bool post(); + + void buffer(StringView); + bool connected() const; + + std::shared_ptr make_flag(); + +private: + friend Flag; + friend PostponedDebug; + + void buffer_impl(StringView); + void buffer_impl(const char*, size_t); + + size_t _count{}; + + String _data; + bool _pending { false }; + + uint32_t _id{}; +}; + +struct PostponedDebug : public PostponedPayload { + void buffer(const DebugPrefix&, espurna::StringView); +}; + +struct InplaceLog; + +struct InplacePayload { + using Clock = espurna::time::CoreClock; + using Send = std::function; + + static constexpr size_t CountMax { 8 }; + + static constexpr auto DefaultTimeout = duration::Seconds{ 2 }; + static constexpr auto DefaultWait = duration::Milliseconds{ 100 }; + + InplacePayload() = delete; + InplacePayload(JsonObject& root, uint32_t id); + + void reset(); + + bool connected() const; + bool can_send() const; + + void write(const char*, size_t); + bool poll_send(); + bool send(); + + void timeout(Clock::duration duration) { + _timeout = duration; + } + + void wait_time(Clock::duration duration) { + _wait = duration; + } + +private: + void write_impl(const char*, size_t); + void send_impl(); + + friend InplaceLog; + + Clock::duration _timeout { DefaultTimeout }; + Clock::duration _wait { DefaultWait }; + + String _data; + size_t _count{}; + + JsonObject& _root; + uint32_t _id; +}; + +struct InplaceLog : public InplacePayload { + InplaceLog(JsonObject&, uint32_t); + + void write(const char* , size_t); + bool send(); +}; + } // namespace ws } // namespace web } // namespace espurna diff --git a/code/html/src/debug.mjs b/code/html/src/debug.mjs index 52d7acc2..83359503 100644 --- a/code/html/src/debug.mjs +++ b/code/html/src/debug.mjs @@ -84,13 +84,8 @@ function listeners() { "log": (_, value) => { send("{}"); - const messages = value["msg"]; - if (messages === undefined) { - return; - } - - for (let msg of messages) { - CmdOutput.push(msg); + for (const message of value) { + CmdOutput.push(message); } CmdOutput.follow();