mirror of
https://github.com/s00500/ESPUI.git
synced 2024-12-23 00:40:49 +00:00
Major restructuring of the code to get past an issue in the web socket where many changes are requested but the WS API cant handle the requests. Requests are now tracked per UI client.
Moved the client out of the main code into its own code and moved control data marshaling into the control class. NONE of these changes impact the users API. No code changes are needed by the users. WARNING: The LittleFS support for the ESP32 has been updated to be compatible with the latest ESP32 infrastructure. This includes using an improved WebServer.
This commit is contained in:
parent
fdffb9c041
commit
a2923e501f
491
src/ESPUIclient.cpp
Normal file
491
src/ESPUIclient.cpp
Normal file
@ -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;
|
||||
}
|
||||
}
|
||||
|
62
src/ESPUIclient.h
Normal file
62
src/ESPUIclient.h
Normal file
@ -0,0 +1,62 @@
|
||||
#pragma once
|
||||
|
||||
#include <Arduino.h>
|
||||
#include <ESPAsyncWebServer.h>
|
||||
#include <ArduinoJson.h>
|
||||
#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);
|
||||
};
|
107
src/ESPUIclientFsm.cpp
Normal file
107
src/ESPUIclientFsm.cpp
Normal file
@ -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();
|
||||
}
|
||||
}
|
79
src/ESPUIclientFsm.h
Normal file
79
src/ESPUIclientFsm.h
Normal file
@ -0,0 +1,79 @@
|
||||
#pragma once
|
||||
|
||||
#include <Arduino.h>
|
||||
#include <ArduinoJson.h>
|
||||
|
||||
// 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
|
||||
|
251
src/ESPUIcontrol.cpp
Normal file
251
src/ESPUIcontrol.cpp
Normal file
@ -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 <option>
|
||||
if (ControlType::Option == type)
|
||||
{
|
||||
Control* ParentControl = ESPUI.getControlNoLock(parentControl);
|
||||
if (nullptr == ParentControl)
|
||||
{
|
||||
item[F("selected")] = emptyString;
|
||||
}
|
||||
else if (ParentControl->value == value)
|
||||
{
|
||||
item[F("selected")] = F("selected");
|
||||
}
|
||||
else
|
||||
{
|
||||
item[F("selected")] = "";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void Control::MarshalErrorMessage(JsonObject & item)
|
||||
{
|
||||
item[F("id")] = id;
|
||||
item[F("type")] = uint32_t(ControlType::Label);
|
||||
item[F("label")] = ControlError.c_str();
|
||||
item[F("value")] = ControlError;
|
||||
item[F("visible")] = true;
|
||||
item[F("color")] = (int)ControlColor::Carrot;
|
||||
item[F("enabled")] = true;
|
||||
|
||||
if (parentControl != Control::noParent)
|
||||
{
|
||||
item[F("parentControl")] = String(parentControl);
|
||||
}
|
||||
}
|
||||
|
||||
void Control::onWsEvent(String & cmd, String& data)
|
||||
{
|
||||
do // once
|
||||
{
|
||||
if (!HasCallback())
|
||||
{
|
||||
#if defined(DEBUG_ESPUI)
|
||||
if (ESPUI.verbosity)
|
||||
{
|
||||
Serial.println(String(F("Control::onWsEvent:No callback found for ID ")) + String(id));
|
||||
}
|
||||
#endif
|
||||
break;
|
||||
}
|
||||
|
||||
// Serial.println("Control::onWsEvent:Generating callback");
|
||||
if (cmd.equals(F("bdown")))
|
||||
{
|
||||
SendCallback(B_DOWN);
|
||||
break;
|
||||
}
|
||||
|
||||
if (cmd.equals(F("bup")))
|
||||
{
|
||||
SendCallback(B_UP);
|
||||
break;
|
||||
}
|
||||
|
||||
if (cmd.equals(F("pfdown")))
|
||||
{
|
||||
SendCallback(P_FOR_DOWN);
|
||||
break;
|
||||
}
|
||||
|
||||
if (cmd.equals(F("pfup")))
|
||||
{
|
||||
SendCallback(P_FOR_UP);
|
||||
break;
|
||||
}
|
||||
|
||||
if (cmd.equals(F("pldown")))
|
||||
{
|
||||
SendCallback(P_LEFT_DOWN);
|
||||
break;
|
||||
}
|
||||
|
||||
else if (cmd.equals(F("plup")))
|
||||
{
|
||||
SendCallback(P_LEFT_UP);
|
||||
}
|
||||
else if (cmd.equals(F("prdown")))
|
||||
{
|
||||
SendCallback(P_RIGHT_DOWN);
|
||||
}
|
||||
else if (cmd.equals(F("prup")))
|
||||
{
|
||||
SendCallback(P_RIGHT_UP);
|
||||
}
|
||||
else if (cmd.equals(F("pbdown")))
|
||||
{
|
||||
SendCallback(P_BACK_DOWN);
|
||||
}
|
||||
else if (cmd.equals(F("pbup")))
|
||||
{
|
||||
SendCallback(P_BACK_UP);
|
||||
}
|
||||
else if (cmd.equals(F("pcdown")))
|
||||
{
|
||||
SendCallback(P_CENTER_DOWN);
|
||||
}
|
||||
else if (cmd.equals(F("pcup")))
|
||||
{
|
||||
SendCallback(P_CENTER_UP);
|
||||
}
|
||||
else if (cmd.equals(F("sactive")))
|
||||
{
|
||||
value = "1";
|
||||
SendCallback(S_ACTIVE);
|
||||
}
|
||||
else if (cmd.equals(F("sinactive")))
|
||||
{
|
||||
value = "0";
|
||||
// updateControl(c, client->id());
|
||||
SendCallback(S_INACTIVE);
|
||||
}
|
||||
else if (cmd.equals(F("slvalue")))
|
||||
{
|
||||
value = data;
|
||||
// updateControl(c, client->id());
|
||||
SendCallback(SL_VALUE);
|
||||
}
|
||||
else if (cmd.equals(F("nvalue")))
|
||||
{
|
||||
value = data;
|
||||
// updateControl(c, client->id());
|
||||
SendCallback(N_VALUE);
|
||||
}
|
||||
else if (cmd.equals(F("tvalue")))
|
||||
{
|
||||
value = data;
|
||||
// updateControl(c, client->id());
|
||||
SendCallback(T_VALUE);
|
||||
}
|
||||
else if (cmd.equals(F("tabvalue")))
|
||||
{
|
||||
SendCallback(0);
|
||||
}
|
||||
else if (cmd.equals(F("svalue")))
|
||||
{
|
||||
value = data;
|
||||
// updateControl(c, client->id());
|
||||
SendCallback(S_VALUE);
|
||||
}
|
||||
else if (cmd.equals(F("time")))
|
||||
{
|
||||
value = data;
|
||||
// updateControl(c, client->id());
|
||||
SendCallback(TM_VALUE);
|
||||
}
|
||||
else
|
||||
{
|
||||
#if defined(DEBUG_ESPUI)
|
||||
if (ESPUI.verbosity)
|
||||
{
|
||||
Serial.println(F("Control::onWsEvent:Malformed message from the websocket"));
|
||||
}
|
||||
#endif
|
||||
}
|
||||
} while (false);
|
||||
}
|
132
src/ESPUIcontrol.h
Normal file
132
src/ESPUIcontrol.h
Normal file
@ -0,0 +1,132 @@
|
||||
#pragma once
|
||||
|
||||
#include <Arduino.h>
|
||||
#include <ArduinoJson.h>
|
||||
|
||||
enum ControlType : uint8_t
|
||||
{
|
||||
// fixed Controls
|
||||
Title = 0,
|
||||
|
||||
// updatable Controls
|
||||
Pad,
|
||||
PadWithCenter,
|
||||
Button,
|
||||
Label,
|
||||
Switcher,
|
||||
Slider,
|
||||
Number,
|
||||
Text,
|
||||
Graph,
|
||||
GraphPoint,
|
||||
Tab,
|
||||
Select,
|
||||
Option,
|
||||
Min,
|
||||
Max,
|
||||
Step,
|
||||
Gauge,
|
||||
Accel,
|
||||
Separator,
|
||||
Time,
|
||||
|
||||
UpdateOffset = 100,
|
||||
};
|
||||
|
||||
enum ControlColor : uint8_t
|
||||
{
|
||||
Turquoise,
|
||||
Emerald,
|
||||
Peterriver,
|
||||
Wetasphalt,
|
||||
Sunflower,
|
||||
Carrot,
|
||||
Alizarin,
|
||||
Dark,
|
||||
None = 0xFF
|
||||
};
|
||||
|
||||
class Control
|
||||
{
|
||||
public:
|
||||
ControlType type;
|
||||
uint16_t id; // just mirroring the id here for practical reasons
|
||||
const char* label;
|
||||
void (*callback)(Control*, int);
|
||||
void (*extendedCallback)(Control*, int, void*);
|
||||
void* user;
|
||||
String value;
|
||||
ControlColor color;
|
||||
bool visible;
|
||||
bool wide;
|
||||
bool vertical;
|
||||
bool enabled;
|
||||
uint16_t parentControl;
|
||||
String panelStyle;
|
||||
String elementStyle;
|
||||
String inputType;
|
||||
Control* next;
|
||||
|
||||
static constexpr uint16_t noParent = 0xffff;
|
||||
|
||||
Control(ControlType type,
|
||||
const char* label,
|
||||
void (*callback)(Control*, int, void*),
|
||||
void* UserData,
|
||||
const String& value,
|
||||
ControlColor color,
|
||||
bool visible,
|
||||
uint16_t parentControl);
|
||||
|
||||
Control(const Control& Control);
|
||||
|
||||
void SendCallback(int type);
|
||||
bool HasCallback() { return ((nullptr != callback) || (nullptr != extendedCallback)); }
|
||||
void MarshalControl(ArduinoJson::JsonObject& item, bool refresh);
|
||||
void MarshalErrorMessage(ArduinoJson::JsonObject& item);
|
||||
bool ToBeDeleted() { return (ControlSyncState_t::deleted == ControlSyncState); }
|
||||
void DeleteControl();
|
||||
bool IsUpdated() { return ControlSyncState_t::synchronized != ControlSyncState; }
|
||||
void HasBeenUpdated() { ControlSyncState = ControlSyncState_t::updated; }
|
||||
void HasBeenSynchronized() {ControlSyncState = ControlSyncState_t::synchronized;}
|
||||
void onWsEvent(String& cmd, String& data);
|
||||
|
||||
private:
|
||||
enum ControlSyncState_t
|
||||
{
|
||||
synchronized = 0,
|
||||
updated,
|
||||
deleted,
|
||||
};
|
||||
ControlSyncState_t ControlSyncState = ControlSyncState_t::synchronized;
|
||||
};
|
||||
|
||||
#define UI_TITLE ControlType::Title
|
||||
#define UI_LABEL ControlType::Label
|
||||
#define UI_BUTTON ControlType::Button
|
||||
#define UI_SWITCHER ControlType::Switcher
|
||||
#define UI_PAD ControlType::Pad
|
||||
#define UI_CPAD ControlType::Cpad
|
||||
#define UI_SLIDER ControlType::Slider
|
||||
#define UI_NUMBER ControlType::Number
|
||||
#define UI_TEXT_INPUT ControlType::Text
|
||||
#define UI_GRAPH ControlType::Graph
|
||||
#define UI_ADD_GRAPH_POINT ControlType::GraphPoint
|
||||
|
||||
#define UPDATE_LABEL ControlType::UpdateLabel
|
||||
#define UPDATE_SWITCHER ControlType::UpdateSwitcher
|
||||
#define UPDATE_SLIDER ControlType::UpdateSlider
|
||||
#define UPDATE_NUMBER ControlType::UpdateNumber
|
||||
#define UPDATE_TEXT_INPUT ControlType::UpdateText
|
||||
#define CLEAR_GRAPH ControlType::ClearGraph
|
||||
|
||||
// Colors
|
||||
#define COLOR_TURQUOISE ControlColor::Turquoise
|
||||
#define COLOR_EMERALD ControlColor::Emerald
|
||||
#define COLOR_PETERRIVER ControlColor::Peterriver
|
||||
#define COLOR_WETASPHALT ControlColor::Wetasphalt
|
||||
#define COLOR_SUNFLOWER ControlColor::Sunflower
|
||||
#define COLOR_CARROT ControlColor::Carrot
|
||||
#define COLOR_ALIZARIN ControlColor::Alizarin
|
||||
#define COLOR_DARK ControlColor::Dark
|
||||
#define COLOR_NONE ControlColor::None
|
Loading…
Reference in New Issue
Block a user