ESP3D/esp3d/src/modules/websocket/websocket_server.cpp
Luc fe23f0cb1e
Grbl grblHAL better support for realtime commands (#1064)
* Add a realtime command detector 
* Move `\r` as printable char and not replaced as `\n`
* Refactorize Serial service code to avoid redondant code between esp32 and esp8266
* Implement isrealtimeCommand check for serial and centralize function in string helper
* Add new type message : realtimecmd
* Update simulator to handle commands and realtime commands
* Add simple serial test tool
*  Generate error if use HAS_DISPLAY with grbl/grblHAL
* Implement isRealTimeCommand for BT client
* Simplify BT push2buffer code
* Implement support for realtimecommand in telnet
* Implement isRealTimeCommand on websocket RX
* Simplify push2RXbuffer for websocket
* Implement isRealTimeCommand for USB serial
* Bump version
2024-12-08 17:26:19 +08:00

382 lines
11 KiB
C++

/*
websocket_server.cpp - websocket functions class
Copyright (c) 2014 Luc Lebosse. All rights reserved.
This code is free software; you can redistribute it and/or
modify it under the terms of the GNU Lesser General Public
License as published by the Free Software Foundation; either
version 2.1 of the License, or (at your option) any later version.
This code is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public
License along with This code; if not, write to the Free Software
Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
*/
#include "../../include/esp3d_config.h"
#if defined(HTTP_FEATURE) || defined(WS_DATA_FEATURE) || \
(defined(ESP_LOG_FEATURE) && ESP_LOG_FEATURE == LOG_OUTPUT_WEBSOCKET)
#include <WebSocketsServer.h>
#include "../../core/esp3d_commands.h"
#include "../../core/esp3d_message.h"
#include "../../core/esp3d_settings.h"
#include "../../core/esp3d_string.h"
#include "../authentication/authentication_service.h"
#include "websocket_server.h"
WebSocket_Server websocket_terminal_server("webui-v3",
ESP3DClientType::webui_websocket);
#if defined(WS_DATA_FEATURE)
WebSocket_Server websocket_data_server("arduino", ESP3DClientType::websocket);
#endif // WS_DATA_FEATURE
bool WebSocket_Server::pushMSG(const char *data) {
if (_websocket_server) {
esp3d_log("[%u]Broadcast %s", _current_id, data);
return _websocket_server->broadcastTXT(data);
}
return false;
}
void WebSocket_Server::enableOnly(uint num) {
// some sanity check
if (_websocket_server) {
for (uint8_t i = 0; i < WEBSOCKETS_SERVER_CLIENT_MAX; i++)
if (i != num && _websocket_server->clientIsConnected(i)) {
_websocket_server->disconnect(i);
}
}
}
bool WebSocket_Server::dispatch(ESP3DMessage *message) {
if (!message || !_started) {
return false;
}
if (message->size > 0 && message->data) {
size_t sentcnt = writeBytes(message->data, message->size);
if (sentcnt != message->size) {
return false;
}
esp3d_message_manager.deleteMsg(message);
return true;
}
return false;
}
bool WebSocket_Server::pushMSG(uint num, const char *data) {
if (_websocket_server) {
esp3d_log("[%u]Send %s", num, data);
return _websocket_server->sendTXT(num, data);
}
return false;
}
bool WebSocket_Server::isConnected() {
if (_websocket_server) {
return _websocket_server->connectedClients() > 0;
}
return false;
}
void WebSocket_Server::closeClients() {
if (_websocket_server) {
_websocket_server->disconnect();
}
}
#if defined(WS_DATA_FEATURE)
// Events for Websocket bridge
void handle_Websocket_Server_Event(uint8_t num, uint8_t type, uint8_t *payload,
size_t length) {
(void)num;
switch (type) {
case WStype_DISCONNECTED:
esp3d_log("[%u] Disconnected! port %d", num,
websocket_data_server.port());
break;
case WStype_CONNECTED: {
websocket_data_server.initAuthentication();
esp3d_log("[%u] Connected! port %d, %s", num,
websocket_data_server.port(), payload);
} break;
case WStype_TEXT:
esp3d_log("[%u] get Text: %s port %d", num, payload,
websocket_data_server.port());
websocket_data_server.push2RXbuffer(payload, length);
break;
case WStype_BIN:
esp3d_log("[%u] get binary length: %u port %d", num, length,
websocket_data_server.port());
websocket_data_server.push2RXbuffer(payload, length);
break;
default:
break;
}
}
#endif // WS_DATA_FEATURE
#if defined(HTTP_FEATURE)
// Events for Websocket used in WebUI for events and serial bridge
void handle_Websocket_Terminal_Event(uint8_t num, uint8_t type,
uint8_t *payload, size_t length) {
(void)payload;
(void)length;
String msg;
switch (type) {
case WStype_DISCONNECTED:
esp3d_log("[%u] Socket Disconnected port %d!", num,
websocket_terminal_server.port());
break;
case WStype_CONNECTED: {
esp3d_log("[%u] Connected! port %d, %s", num,
websocket_terminal_server.port(), (const char *)payload);
msg = "currentID:" + String(num);
// send message to client
websocket_terminal_server.set_currentID(num);
websocket_terminal_server.pushMSG(num, msg.c_str());
msg = "activeID:" + String(num);
websocket_terminal_server.pushMSG(msg.c_str());
websocket_terminal_server.enableOnly(num);
esp3d_log("[%u] Socket connected port %d", num,
websocket_terminal_server.port());
} break;
case WStype_TEXT:
#if defined(AUTHENTICATION_FEATURE)
// we do not expect any input but ping to get session timeout if any
if (AuthenticationService::getSessionTimeout() != 0) {
msg = (const char *)payload;
if (msg.startsWith("PING:")) {
String session = msg.substring(5);
String response =
"PING:" + String(AuthenticationService::getSessionRemaining(
session.c_str()));
response += ":" + String(AuthenticationService::getSessionTimeout());
websocket_terminal_server.pushMSG(num, response.c_str());
}
}
#endif // AUTHENTICATION_FEATURE
// esp3d_log("[IGNORED][%u] get Text: %s port %d", num, payload,
// websocket_terminal_server.port());
break;
case WStype_BIN:
// we do not expect any input
// esp3d_log("[IGNORED][%u] get binary length: %u port %d", num,
// length, websocket_terminal_server.port());
break;
default:
break;
}
}
#endif // HTTP_FEATURE
int WebSocket_Server::available() { return _RXbufferSize; }
int WebSocket_Server::availableForWrite() {
return TXBUFFERSIZE - _TXbufferSize;
}
WebSocket_Server::WebSocket_Server(const char *protocol, ESP3DClientType type) {
_websocket_server = nullptr;
_started = false;
_port = 0;
_current_id = 0;
_RXbuffer = nullptr;
_RXbufferSize = 0;
_protocol = protocol;
_type = type;
initAuthentication();
}
WebSocket_Server::~WebSocket_Server() { end(); }
bool WebSocket_Server::begin(uint16_t port) {
end();
if (port == 0) {
_port = ESP3DSettings::readUint32(ESP_HTTP_PORT) + 1;
} else {
_port = port;
if (ESP3DSettings::readByte(ESP_WEBSOCKET_ON) == 0) {
return true;
}
}
_websocket_server = new WebSocketsServer(_port, "", _protocol.c_str());
if (_websocket_server) {
_websocket_server->begin();
#if defined(HTTP_FEATURE) // terminal websocket for HTTP
if (port == 0) {
_websocket_server->onEvent(handle_Websocket_Terminal_Event);
}
#endif // HTTP_FEATURE
#if defined(WS_DATA_FEATURE) // terminal websocket for HTTP
if ((port != 0) && _protocol != "log") {
_websocket_server->onEvent(handle_Websocket_Server_Event);
_RXbuffer = (uint8_t *)malloc(RXBUFFERSIZE + 1);
if (!_RXbuffer) {
return false;
}
}
#endif // WS_DATA_FEATURE
_started = true;
} else {
end();
}
return _started;
}
void WebSocket_Server::end() {
_current_id = 0;
_TXbufferSize = 0;
if (_RXbuffer) {
free(_RXbuffer);
_RXbuffer = nullptr;
}
_RXbufferSize = 0;
if (_websocket_server) {
_websocket_server->close();
delete _websocket_server;
_websocket_server = nullptr;
_port = 0;
}
_started = false;
initAuthentication();
}
WebSocket_Server::operator bool() const { return _started; }
void WebSocket_Server::set_currentID(uint8_t current_id) {
_current_id = current_id;
}
uint8_t WebSocket_Server::get_currentID() { return _current_id; }
// size_t WebSocket_Server::write(uint8_t c) { return write(&c, 1); }
size_t WebSocket_Server::writeBytes(const uint8_t *buffer, size_t size) {
if (_started) {
if ((buffer == nullptr) || (!_websocket_server) || (size == 0)) {
return 0;
}
if (_TXbufferSize == 0) {
_lastTXflush = millis();
}
// send full line
if (_TXbufferSize + size > TXBUFFERSIZE) {
flushTXbuffer();
}
if (_websocket_server->connectedClients() == 0) {
return 0;
}
// need periodic check to force to flush in case of no end
for (uint i = 0; i < size; i++) {
// add a sanity check to avoid buffer overflow
if (_TXbufferSize >= TXBUFFERSIZE) {
flushTXbuffer();
}
_TXbuffer[_TXbufferSize] = buffer[i];
_TXbufferSize++;
}
return size;
}
return 0;
}
void WebSocket_Server::push2RXbuffer(uint8_t *sbuf, size_t len) {
if (!_RXbuffer || !_started || !sbuf) {
return;
}
for (size_t i = 0; i < len; i++) {
_lastRXflush = millis();
if (esp3d_string::isRealTimeCommand(sbuf[i])) {
flushRXChar(sbuf[i]);
} else {
_RXbuffer[_RXbufferSize] = sbuf[i];
_RXbufferSize++;
if (_RXbufferSize > RXBUFFERSIZE ||
_RXbuffer[_RXbufferSize - 1] == '\n') {
flushRXbuffer();
}
}
}
}
void WebSocket_Server::initAuthentication() {
#if defined(AUTHENTICATION_FEATURE)
_auth = ESP3DAuthenticationLevel::guest;
#else
_auth = ESP3DAuthenticationLevel::admin;
#endif // AUTHENTICATION_FEATURE
}
ESP3DAuthenticationLevel WebSocket_Server::getAuthentication() { return _auth; }
void WebSocket_Server::flushRXChar(char c) {
flushRXData((uint8_t *)&c, 1, ESP3DMessageType::realtimecmd);
}
void WebSocket_Server::flushRXbuffer() {
_RXbuffer[_RXbufferSize] = 0x0;
flushRXData((uint8_t *)_RXbuffer, _RXbufferSize, ESP3DMessageType::unique);
_RXbufferSize = 0;
}
void WebSocket_Server::flushRXData(const uint8_t *data, size_t size,
ESP3DMessageType type) {
if (!data || !_started) {
return;
}
ESP3DMessage *message = esp3d_message_manager.newMsg(
_type, esp3d_commands.getOutputClient(),
data, size, _auth);
if (message) {
message->type = type;
esp3d_log("Process Message");
esp3d_commands.process(message);
} else {
esp3d_log_e("Cannot create message");
}
_lastRXflush = millis();
}
void WebSocket_Server::handle() {
ESP3DHal::wait(0);
if (_started) {
if (_TXbufferSize > 0) {
if ((_TXbufferSize >= TXBUFFERSIZE) ||
((millis() - _lastTXflush) > FLUSHTIMEOUT)) {
flushTXbuffer();
}
}
if (_RXbufferSize > 0) {
if ((_RXbufferSize >= RXBUFFERSIZE) ||
((millis() - _lastRXflush) > FLUSHTIMEOUT)) {
flushRXbuffer();
}
}
if (_websocket_server) {
_websocket_server->loop();
}
}
}
void WebSocket_Server::flush(void) {
flushTXbuffer();
flushRXbuffer();
}
void WebSocket_Server::flushTXbuffer(void) {
if (_started) {
if ((_TXbufferSize > 0) && (_websocket_server->connectedClients() > 0)) {
if (_websocket_server) {
_websocket_server->broadcastBIN(_TXbuffer, _TXbufferSize);
esp3d_log("WS Broadcast bin port %d: %d bytes", port(), _TXbufferSize);
}
// refresh timout
_lastTXflush = millis();
}
}
// reset buffer
_TXbufferSize = 0;
}
#endif // HTTP_FEATURE || WS_DATA_FEATURE