diff --git a/src/ESPUIclient.cpp b/src/ESPUIclient.cpp new file mode 100644 index 0000000..adc0e5d --- /dev/null +++ b/src/ESPUIclient.cpp @@ -0,0 +1,491 @@ +#include "ESPUI.h" +#include "ESPUIclient.h" +#include "ESPUIcontrol.h" + +ESPUIclient::ESPUIclient(AsyncWebSocketClient * _client): + client(_client) +{ + fsm_EspuiClient_state_Idle_imp.SetParent(this); + fsm_EspuiClient_state_SendingUpdate_imp.SetParent(this); + fsm_EspuiClient_state_Rebuilding_imp.SetParent(this); + fsm_EspuiClient_state_Reloading_imp.SetParent(this); + + fsm_EspuiClient_state_Idle_imp.Init(); +} + +ESPUIclient::ESPUIclient(const ESPUIclient& source): + client(source.client) +{ + fsm_EspuiClient_state_Idle_imp.SetParent(this); + fsm_EspuiClient_state_SendingUpdate_imp.SetParent(this); + fsm_EspuiClient_state_Rebuilding_imp.SetParent(this); + fsm_EspuiClient_state_Reloading_imp.SetParent(this); + + fsm_EspuiClient_state_Idle_imp.Init(); +} + +ESPUIclient::~ESPUIclient() +{ +} + +bool ESPUIclient::CanSend() +{ + bool Response = false; + if (nullptr != client) + { + Response = client->canSend(); + } + return Response; +} + +void ESPUIclient::FillInHeader(DynamicJsonDocument& document) +{ + document[F("type")] = UI_EXTEND_GUI; + document[F("sliderContinuous")] = ESPUI.sliderContinuous; + document[F("startindex")] = 0; + document[F("totalcontrols")] = ESPUI.controlCount; + JsonArray items = document.createNestedArray(F("controls")); + JsonObject titleItem = items.createNestedObject(); + titleItem[F("type")] = (int)UI_TITLE; + titleItem[F("label")] = ESPUI.ui_title; +} + +bool ESPUIclient::IsSyncronized() +{ + return ((ClientUpdateType_t::Synchronized == ClientUpdateType) && + (&fsm_EspuiClient_state_Idle_imp == pCurrentFsmState)); +} + +bool ESPUIclient::SendClientNotification(ClientUpdateType_t value) +{ + bool Response = false; + + do // once + { + if(!CanSend()) + { + // Serial.println(F("ESPUIclient::NotifyClient")); + break; + } + + DynamicJsonDocument document(ESPUI.jsonUpdateDocumentSize); + FillInHeader(document); + if(ClientUpdateType_t::ReloadNeeded == value) + { + // Serial.println(F("ESPUIclient::SendClientNotification:set type to reload")); + document["type"] = int(UI_RELOAD); + } + // dont send any controls + + Response = SendJsonDocToWebSocket(document); + // Serial.println(String("ESPUIclient::SendClientNotification:NotificationSent:Response: ") + String(Response)); + + } while (false); + return Response; +} + +void ESPUIclient::NotifyClient(ClientUpdateType_t newState) +{ + SetState(newState); + pCurrentFsmState->NotifyClient(); + +#ifdef OldWay + do // once + { + // Serial.println(String("ESPUIclient::NotifyClient: State: ") + String(int(newState))); + SetState(newState); + + if (HasBeenNotified) + { + // do not need to do anything + break; + } + + if(TransferIsInprogress) + { + // record that a notification was needed while we were transfering data to the client + DelayedNotification = true; + break; + } + + DelayedNotification = false; + + if (SendJsonDocToWebSocket(document)) + { + HasBeenNotified = true; + } + + } while (false); + + return HasBeenNotified; +#endif // def OldWay +} + +// Handle Websockets Communication +void ESPUIclient::onWsEvent(AwsEventType type, void* arg, uint8_t* data, size_t len) +{ + // Serial.println(String("ESPUIclient::OnWsEvent: type: ") + String(type)); + + switch (type) + { + case WS_EVT_PONG: + { + #if defined(DEBUG_ESPUI) + if (ESPUI.verbosity) + { + Serial.println(F("ESPUIclient::OnWsEvent:WS_EVT_PONG")); + } + #endif + break; + } + + case WS_EVT_ERROR: + { + #if defined(DEBUG_ESPUI) + if (ESPUI.verbosity) + { + Serial.println(F("ESPUIclient::OnWsEvent:WS_EVT_ERROR")); + } + #endif + break; + } + + case WS_EVT_CONNECT: + { + #if defined(DEBUG_ESPUI) + if (ESPUI.verbosity) + { + Serial.println(F("ESPUIclient::OnWsEvent:WS_EVT_CONNECT")); + Serial.println(client->id()); + } + #endif + + // Serial.println("ESPUIclient:onWsEvent:WS_EVT_CONNECT: Call NotifyClient: RebuildNeeded"); + NotifyClient(ClientUpdateType_t::RebuildNeeded); + break; + } + + case WS_EVT_DATA: + { + // Serial.println(F("ESPUIclient::OnWsEvent:WS_EVT_DATA")); + String msg = ""; + msg.reserve(len + 1); + + for (size_t i = 0; i < len; i++) + { + msg += (char)data[i]; + } + + String cmd = msg.substring(0, msg.indexOf(":")); + String value = msg.substring(cmd.length() + 1, msg.lastIndexOf(':')); + uint16_t id = msg.substring(msg.lastIndexOf(':') + 1).toInt(); + + #if defined(DEBUG_ESPUI) + if (ESPUI.verbosity >= Verbosity::VerboseJSON) + { + Serial.println(String(F(" WS msg: ")) + msg); + Serial.println(String(F(" WS cmd: ")) + cmd); + Serial.println(String(F(" WS id: ")) + String(id)); + Serial.println(String(F("WS value: ")) + String(value)); + } + #endif + + if (cmd.equals(F("uiok"))) + { + // Serial.println(F("ESPUIclient::OnWsEvent:WS_EVT_DATA:uiok:ProcessAck")); + pCurrentFsmState->ProcessAck(id); + break; + } + + if (cmd.equals(F("uiuok"))) + { + // Serial.println(F("WS_EVT_DATA: uiuok. Unlock new async notifications")); + break; + } + + Control* control = ESPUI.getControl(id); + if (nullptr == control) + { + #if defined(DEBUG_ESPUI) + if (ESPUI.verbosity) + { + Serial.println(String(F("No control found for ID ")) + String(id)); + } + #endif + break; + } + control->onWsEvent(cmd, value); + break; + } + + default: + { + // Serial.println(F("ESPUIclient::OnWsEvent:default")); + break; + } + } // end switch +} + +/* +Prepare a chunk of elements as a single JSON string. If the allowed number of elements is greater than the total +number this will represent the entire UI. More likely, it will represent a small section of the UI to be sent. The +client will acknowledge receipt by requesting the next chunk. + */ +uint32_t ESPUIclient::prepareJSONChunk(uint16_t startindex, + DynamicJsonDocument & rootDoc, + bool InUpdateMode) +{ +#ifdef ESP32 + xSemaphoreTake(ESPUI.ControlsSemaphore, portMAX_DELAY); +#endif // def ESP32 + + // Serial.println(String("prepareJSONChunk: Start. InUpdateMode: ") + String(InUpdateMode)); + int elementcount = 0; + + do // once + { + // Follow the list until control points to the startindex'th node + Control* control = ESPUI.controls; + uint32_t currentIndex = 0; + JsonArray items = rootDoc[F("controls")]; + + while ((startindex > currentIndex) && (nullptr != control)) + { + // only count active controls + if (!control->ToBeDeleted()) + { + if(InUpdateMode) + { + // In update mode we only count the controls that have been updated. + if(control->IsUpdated()) + { + ++currentIndex; + } + } + else + { + // not in update mode. Count all active controls + ++currentIndex; + } + } + control = control->next; + } + + // any controls left to be processed? + if(nullptr == control) + { + // Serial.println("prepareJSONChunk: No controls to process"); + break; + } + + // keep track of the number of elements we have serialised into this + // message. Overflow is detected and handled later in this loop + // and needs an index to the last item added. + while (nullptr != control) + { + // skip deleted controls or controls that have not been updated + if (control->ToBeDeleted()) + { + // Serial.println(String("prepareJSONChunk: Ignoring Deleted control: ") + String(control->id)); + control = control->next; + continue; + } + + if(InUpdateMode) + { + if(control->IsUpdated()) + { + // dont skip this control + } + else + { + // control has not been updated. Skip it + control = control->next; + continue; + } + } + + JsonObject item = items.createNestedObject(); + elementcount++; + control->MarshalControl(item, InUpdateMode); + + if (rootDoc.overflowed()) + { + // String("prepareJSONChunk: too much data in the message. Remove the last entry"); + if (1 == elementcount) + { + Serial.println(String(F("ERROR: prepareJSONChunk: Control ")) + String(control->id) + F(" is too large to be sent to the browser.")); + rootDoc.clear(); + item = items.createNestedObject(); + control->MarshalErrorMessage(item); + elementcount = 0; + } + else + { + // Serial.println(String("prepareJSONChunk: Defering control: ") + String(control->id)); + // Serial.println(String("prepareJSONChunk: elementcount: ") + String(elementcount)); + + items.remove(elementcount); + --elementcount; + } + // exit the loop + control = nullptr; + + } + else + { + control = control->next; + } + } // end while (control != nullptr) + + } while (false); + +#ifdef ESP32 + xSemaphoreGive(ESPUI.ControlsSemaphore); +#endif // def ESP32 + + // Serial.println(String("prepareJSONChunk: elementcount: ") + String(elementcount)); + return elementcount; +} + +/* +Convert & Transfer Arduino elements to JSON elements. This function sends a chunk of +JSON describing the controls of the UI, starting from the control at index startidx. +If startidx is 0 then a UI_INITIAL_GUI message will be sent, else a UI_EXTEND_GUI. +Both message types contain a list of serialised UI elements. Only a portion of the UI +will be sent in order to avoid websocket buffer overflows. The client will acknowledge +receipt of a partial message by requesting the next chunk of UI. + +The protocol is: +SERVER: SendControlsToClient(0): + "UI_INITIAL_GUI: n serialised UI elements" +CLIENT: controls.js:handleEvent() + "uiok:n" +SERVER: SendControlsToClient(n): + "UI_EXTEND_GUI: n serialised UI elements" +CLIENT: controls.js:handleEvent() + "uiok:2*n" +etc. + Returns true if all controls have been sent (aka: Done) +*/ +bool ESPUIclient::SendControlsToClient(uint16_t startidx, + ClientUpdateType_t TransferMode) +{ + bool Response = false; + // Serial.println(String("ESPUIclient:SendControlsToClient:startidx: ") + String(startidx)); + do // once + { + if(!CanSend()) + { + // Serial.println("ESPUIclient:SendControlsToClient: Cannot Send to clients."); + break; + } + + if (startidx >= ESPUI.controlCount) + { + // Serial.println("ESPUIclient:SendControlsToClient: No more controls to send."); + Response = true; + break; + } + + DynamicJsonDocument document(ESPUI.jsonInitialDocumentSize); + FillInHeader(document); + document[F("startindex")] = startidx; + document[F("totalcontrols")] = 65534; // ESPUI.controlCount; + + if(0 == startidx) + { + // Serial.println("ESPUIclient:SendControlsToClient: Tell client we are starting a transfer of controls."); + document["type"] = (ClientUpdateType_t::RebuildNeeded == TransferMode) ? UI_INITIAL_GUI : UI_EXTEND_GUI; + } + // Serial.println(String("ESPUIclient:SendControlsToClient:type: ") + String((uint32_t)document["type"])); + + // Serial.println("ESPUIclient:SendControlsToClient: Build Controls."); + if(prepareJSONChunk(startidx, document, ClientUpdateType_t::UpdateNeeded == TransferMode)) + { + #if defined(DEBUG_ESPUI) + if (ESPUI.verbosity >= Verbosity::VerboseJSON) + { + Serial.println(F("ESPUIclient:SendControlsToClient: Sending elements --------->")); + String json; + serializeJson(document, json); + Serial.println(json); + } + #endif + + // Serial.println("ESPUIclient:SendControlsToClient: Send message."); + if(true == SendJsonDocToWebSocket(document)) + { + // Serial.println("ESPUIclient:SendControlsToClient: Sent."); + } + else + { + // Serial.println("ESPUIclient:SendControlsToClient: Send failed."); + } + } + else + { + // Serial.println("ESPUIclient:SendControlsToClient: No elements to send."); + Response = true; + } + + } while(false); + + // Serial.println(String("ESPUIclient:SendControlsToClient:Response: ") + String(Response)); + return Response; +} + +bool ESPUIclient::SendJsonDocToWebSocket(DynamicJsonDocument& document) +{ + bool Response = true; + + do // once + { + if (!CanSend()) + { + #if defined(DEBUG_ESPUI) + if (ESPUI.verbosity >= Verbosity::VerboseJSON) + { + Serial.println("SendJsonDocToWebSocket: Cannot Send to client. Not sending websocket message"); + } + #endif + // Serial.println("SendJsonDocToWebSocket: Cannot Send to client. Not sending websocket message"); + Response = false; + break; + } + + String json; + json.reserve(document.size() / 2); + json.clear(); + serializeJson(document, json); + + #if defined(DEBUG_ESPUI) + if (ESPUI.verbosity >= Verbosity::VerboseJSON) + { + Serial.println(String("SendJsonDocToWebSocket: json: '") + json + "'"); + } + #endif + + #if defined(DEBUG_ESPUI) + if (ESPUI.verbosity >= Verbosity::VerboseJSON) + { + Serial.println(F("SendJsonDocToWebSocket: client.text")); + } + #endif + // Serial.println(F("SendJsonDocToWebSocket: client.text")); + client->text(json); + + } while (false); + + return Response; +} + +void ESPUIclient::SetState(ClientUpdateType_t value) +{ + // only a higher priority state request can replace the current state request + if(uint32_t(ClientUpdateType) < uint32_t(value)) + { + ClientUpdateType = value; + } +} + diff --git a/src/ESPUIclient.h b/src/ESPUIclient.h new file mode 100644 index 0000000..5a34699 --- /dev/null +++ b/src/ESPUIclient.h @@ -0,0 +1,62 @@ +#pragma once + +#include +#include +#include +#include "ESPUIclientFsm.h" +#include "ESPUIcontrol.h" + +class ESPUIclient +{ +public: + enum ClientUpdateType_t + { // this is an orderd list. highest number is highest priority + Synchronized = 0, + UpdateNeeded = 1, + RebuildNeeded = 2, + ReloadNeeded = 3, + }; + +protected: + // bool HasBeenNotified = false; // Set when a notification has been sent and we are waiting for a reply + // bool DelayedNotification = false; // set if a delayed notification is needed + + ClientUpdateType_t ClientUpdateType = ClientUpdateType_t::RebuildNeeded; + + AsyncWebSocketClient * client = nullptr; + + friend class fsm_EspuiClient_state_Idle; + friend class fsm_EspuiClient_state_SendingUpdate; + friend class fsm_EspuiClient_state_Rebuilding; + friend class fsm_EspuiClient_state_WaitForAck; + friend class fsm_EspuiClient_state_Reloading; + friend class fsm_EspuiClient_state; + + fsm_EspuiClient_state_Idle fsm_EspuiClient_state_Idle_imp; + fsm_EspuiClient_state_SendingUpdate fsm_EspuiClient_state_SendingUpdate_imp; + fsm_EspuiClient_state_Rebuilding fsm_EspuiClient_state_Rebuilding_imp; + fsm_EspuiClient_state_Reloading fsm_EspuiClient_state_Reloading_imp; + fsm_EspuiClient_state* pCurrentFsmState = &fsm_EspuiClient_state_Idle_imp; + + time_t EspuiClientEndTime = 0; + + // bool NeedsNotification() { return pCurrentFsmState != &fsm_EspuiClient_state_Idle_imp; } + + bool CanSend(); + void FillInHeader(ArduinoJson::DynamicJsonDocument& document); + uint32_t prepareJSONChunk(uint16_t startindex, DynamicJsonDocument& rootDoc, bool InUpdateMode); + bool SendControlsToClient(uint16_t startidx, ClientUpdateType_t TransferMode); + + bool SendClientNotification(ClientUpdateType_t value); + +public: + ESPUIclient(AsyncWebSocketClient * _client); + ESPUIclient(const ESPUIclient & source); + virtual ~ESPUIclient(); + void NotifyClient(ClientUpdateType_t value); + void onWsEvent(AwsEventType type, void* arg, uint8_t* data, size_t len); + bool IsSyncronized(); + uint32_t id() { return client->id(); } + void SetState(ClientUpdateType_t value); + bool SendJsonDocToWebSocket(ArduinoJson::DynamicJsonDocument& document); +}; diff --git a/src/ESPUIclientFsm.cpp b/src/ESPUIclientFsm.cpp new file mode 100644 index 0000000..1bd628b --- /dev/null +++ b/src/ESPUIclientFsm.cpp @@ -0,0 +1,107 @@ +#include "ESPUI.h" +#include "ESPUIclient.h" + +//---------------------------------------------- +// FSM definitions +//---------------------------------------------- +void fsm_EspuiClient_state::Init() +{ + // Serial.println(String("fsm_EspuiClient_state:Init: ") + GetStateName()); + Parent->pCurrentFsmState = this; +} + +//---------------------------------------------- +//---------------------------------------------- +//---------------------------------------------- +bool fsm_EspuiClient_state_Idle::NotifyClient() +{ + bool Response = false; + + // Serial.println(F("fsm_EspuiClient_state_Idle: NotifyClient")); + ClientUpdateType_t TypeToProcess = Parent->ClientUpdateType; + // Clear the type so that we capture any changes in type that happen + // while we are processing the current request. + Parent->ClientUpdateType = ClientUpdateType_t::Synchronized; + + // Start processing the current request. + switch (TypeToProcess) + { + case ClientUpdateType_t::Synchronized: + { + // Serial.println(F("fsm_EspuiClient_state_Idle: NotifyClient:State:Synchronized")); + // Parent->fsm_EspuiClient_state_Idle_imp.Init(); + Response = true; // Parent->SendClientNotification(ClientUpdateType_t::UpdateNeeded); + break; + } + case ClientUpdateType_t::UpdateNeeded: + { + // Serial.println(F("fsm_EspuiClient_state_Idle: NotifyClient:State:UpdateNeeded")); + Parent->fsm_EspuiClient_state_SendingUpdate_imp.Init(); + Response = Parent->SendClientNotification(ClientUpdateType_t::UpdateNeeded); + break; + } + case ClientUpdateType_t::RebuildNeeded: + { + // Serial.println(F("fsm_EspuiClient_state_Idle: NotifyClient:State:RebuildNeeded")); + Parent->fsm_EspuiClient_state_Rebuilding_imp.Init(); + Response = Parent->SendClientNotification(ClientUpdateType_t::RebuildNeeded); + break; + } + case ClientUpdateType_t::ReloadNeeded: + { + // Serial.println(F("fsm_EspuiClient_state_Idle: NotifyClient:State:ReloadNeeded")); + Parent->fsm_EspuiClient_state_Reloading_imp.Init(); + Response = Parent->SendClientNotification(ClientUpdateType_t::ReloadNeeded); + break; + } + } + return Response; +} + +void fsm_EspuiClient_state_Idle::ProcessAck(uint16_t) +{ + // This is an unexpected request for control data from the browser + // treat it as if it was a rebuild operation + // Serial.println(F("fsm_EspuiClient_state_Idle: ProcessAck")); + Parent->NotifyClient(ClientUpdateType_t::RebuildNeeded); +} + +//---------------------------------------------- +//---------------------------------------------- +//---------------------------------------------- +bool fsm_EspuiClient_state_SendingUpdate::NotifyClient() +{ + // Serial.println(F("fsm_EspuiClient_state_SendingUpdate:NotifyClient")); + return true; /* Ignore request */ +} + +void fsm_EspuiClient_state_SendingUpdate::ProcessAck(uint16_t ControlIndex) +{ + // Serial.println(F("fsm_EspuiClient_state_SendingUpdate: ProcessAck")); + if(Parent->SendControlsToClient(ControlIndex, ClientUpdateType_t::UpdateNeeded)) + { + // No more data to send. Go back to idle or start next request + Parent->fsm_EspuiClient_state_Idle_imp.Init(); + Parent->fsm_EspuiClient_state_Idle_imp.NotifyClient(); + } +} + +//---------------------------------------------- +//---------------------------------------------- +//---------------------------------------------- +bool fsm_EspuiClient_state_Rebuilding::NotifyClient() +{ + // Serial.println(F("fsm_EspuiClient_state_Rebuilding: NotifyClient")); + return true; /* Ignore request */ +} + +void fsm_EspuiClient_state_Rebuilding::ProcessAck(uint16_t ControlIndex) +{ + // Serial.println(F("fsm_EspuiClient_state_Rebuilding: ProcessAck")); + if(Parent->SendControlsToClient(ControlIndex, ClientUpdateType_t::RebuildNeeded)) + { + // No more data to send. Go back to idle or start next request + Parent->fsm_EspuiClient_state_Idle_imp.Init(); + Parent->fsm_EspuiClient_state_Idle_imp.NotifyClient(); + } +} diff --git a/src/ESPUIclientFsm.h b/src/ESPUIclientFsm.h new file mode 100644 index 0000000..b04bb54 --- /dev/null +++ b/src/ESPUIclientFsm.h @@ -0,0 +1,79 @@ +#pragma once + +#include +#include + +// forward declaration +class ESPUIclient; + +/*****************************************************************************/ +/* +* Generic fsm base class. +*/ +/*****************************************************************************/ +/*****************************************************************************/ +class fsm_EspuiClient_state +{ +public: + fsm_EspuiClient_state() {}; + virtual ~fsm_EspuiClient_state() {} + + void Init(); + virtual bool NotifyClient() = 0; + virtual void ProcessAck(uint16_t id) = 0; + virtual String GetStateName () = 0; + void SetParent(ESPUIclient * value) { Parent = value; } + +protected: + ESPUIclient * Parent = nullptr; + +}; // fsm_EspuiClient_state + +class fsm_EspuiClient_state_Idle : public fsm_EspuiClient_state +{ +public: + fsm_EspuiClient_state_Idle() {} + virtual ~fsm_EspuiClient_state_Idle() {} + + virtual bool NotifyClient(); + virtual void ProcessAck(uint16_t id); + String GetStateName() { return String(F("Idle")); } + +}; // fsm_EspuiClient_state_Idle + +class fsm_EspuiClient_state_SendingUpdate : public fsm_EspuiClient_state +{ +public: + fsm_EspuiClient_state_SendingUpdate() {} + virtual ~fsm_EspuiClient_state_SendingUpdate() {} + + virtual bool NotifyClient(); + virtual void ProcessAck(uint16_t id); + String GetStateName() { return String(F("Sending Update")); } + +}; // fsm_EspuiClient_state_SendingUpdate + +class fsm_EspuiClient_state_Rebuilding : public fsm_EspuiClient_state +{ +public: + fsm_EspuiClient_state_Rebuilding() {} + virtual ~fsm_EspuiClient_state_Rebuilding() {} + + virtual bool NotifyClient(); + virtual void ProcessAck(uint16_t id); + String GetStateName() { return String(F("Sending Rebuild")); } + +}; // fsm_EspuiClient_state_Rebuilding + +class fsm_EspuiClient_state_Reloading : public fsm_EspuiClient_state +{ +public: + fsm_EspuiClient_state_Reloading() {} + virtual ~fsm_EspuiClient_state_Reloading() {} + + virtual bool NotifyClient() { return false; } + virtual void ProcessAck(uint16_t) {} + String GetStateName() { return String(F("Reloading")); } + +}; // fsm_EspuiClient_state_Reloading + diff --git a/src/ESPUIcontrol.cpp b/src/ESPUIcontrol.cpp new file mode 100644 index 0000000..e9c973c --- /dev/null +++ b/src/ESPUIcontrol.cpp @@ -0,0 +1,251 @@ +#include "ESPUI.h" + +static uint16_t idCounter = 0; +static const String ControlError = "*** ESPUI ERROR: Could not transfer control ***"; + +Control::Control(ControlType type, const char* label, void (*callback)(Control*, int, void*), void* UserData, + const String& value, ControlColor color, bool visible, uint16_t parentControl) + : type(type), + label(label), + callback(nullptr), + extendedCallback(callback), + user(UserData), + value(value), + color(color), + visible(visible), + wide(false), + vertical(false), + enabled(true), + parentControl(parentControl), + next(nullptr) +{ + id = ++idCounter; +} + +Control::Control(const Control& Control) + : type(Control.type), + id(Control.id), + label(Control.label), + callback(Control.callback), + extendedCallback(Control.extendedCallback), + user(Control.user), + value(Control.value), + color(Control.color), + visible(Control.visible), + parentControl(Control.parentControl), + next(Control.next) +{ } + +void Control::SendCallback(int type) +{ + if(callback) + { + callback(this, type); + } + + if (extendedCallback) + { + extendedCallback(this, type, user); + } +} + +void Control::DeleteControl() +{ + ControlSyncState = ControlSyncState_t::deleted; + extendedCallback = nullptr; + callback = nullptr; +} + +void Control::MarshalControl(JsonObject & item, bool refresh) +{ + item[F("id")] = id; + if(refresh) + { + item[F("type")] = uint32_t(type) + uint32_t(ControlType::UpdateOffset); + } + else + { + item[F("type")] = uint32_t(type); + } + item[F("label")] = label; + item[F("value")] = value; + item[F("visible")] = visible; + item[F("color")] = (int)color; + item[F("enabled")] = enabled; + + if (!panelStyle.isEmpty()) {item[F("panelStyle")] = panelStyle;} + if (!elementStyle.isEmpty()) {item[F("elementStyle")] = elementStyle;} + if (!inputType.isEmpty()) {item[F("inputType")] = inputType;} + if (wide == true) {item[F("wide")] = true;} + if (vertical == true) {item[F("vertical")] = true;} + if (parentControl != Control::noParent) + { + item[F("parentControl")] = String(parentControl); + } + + // special case for selects: to preselect an option, you have to add + // "selected" to