Refactor SerialCommand to support Stream interface and improve command handling

- Added readSerial(Stream &stream) method to allow reading from any Stream-compatible transport.
- Introduced readChar(char inChar) method for processing individual characters.
- Updated command matching logic to enhance debugging output.
- Improved buffer handling and command execution flow.

Enhance platformio.ini for better compatibility and added libraries

- Added NimBLE-Arduino and WebSockets libraries for BLE and WiFi support.
- Updated upload and monitor ports for better compatibility with macOS.

Integrate WiFi and BLE protocol interfaces

- Implemented startWebInterface() to initialize WiFi protocol alongside existing web server.
- Added BLE support with a new EggBot BLE Serial Protocol for command handling over BLE.
- Created WebSocket server for WiFi communication, maintaining compatibility with existing command protocols.

Refactor command handling in Functions.cpp

- Replaced direct Serial.print calls with protocolWrite for consistent output handling.
- Updated command registration to use a lambda function for better readability and maintainability.

Add documentation for EggBot protocols

- Created separate markdown files for BLE, WiFi, and Serial protocols detailing command structures and usage.
- Provided examples of command transactions for better developer guidance.

Implement BLE and WiFi protocol handling in respective source files

- Developed BLE_Interface.cpp for managing BLE connections and data transmission.
- Created WiFi_Protocol.cpp for handling WebSocket communication and data reception.
This commit is contained in:
2026-02-24 22:00:26 +01:00
parent 3487d7c263
commit a1ffcb08ca
15 changed files with 1115 additions and 83 deletions

244
src/BLE_Interface.cpp Normal file
View File

@@ -0,0 +1,244 @@
#include "EggDuino.h"
#ifdef ESP32
#include <NimBLEDevice.h>
#include <string.h>
namespace
{
constexpr char kBleDeviceNamePrefix[] = "EggDuino_";
constexpr char kBleServiceUuid[] = "6e400001-b5a3-f393-e0a9-e50e24dcca9e";
constexpr char kBleRxCharUuid[] = "6e400002-b5a3-f393-e0a9-e50e24dcca9e";
constexpr char kBleTxCharUuid[] = "6e400003-b5a3-f393-e0a9-e50e24dcca9e";
constexpr size_t kBleRxQueueSize = 512;
constexpr size_t kBleNotifyChunkSize = 20;
NimBLEServer *g_pBleServer = NULL;
NimBLECharacteristic *g_pBleTxCharacteristic = NULL;
uint8_t g_bleRxQueue[kBleRxQueueSize];
size_t g_bleRxHead = 0;
size_t g_bleRxTail = 0;
bool g_bleRxQueueOverflow = false;
bool g_bleClientConnected = false;
portMUX_TYPE g_bleQueueMux = portMUX_INITIALIZER_UNLOCKED;
void logBleDiag(const String &message)
{
Log(message);
Serial.println(message);
}
void buildBleDeviceName(char *nameBuffer, size_t bufferSize)
{
if ((nameBuffer == NULL) || (bufferSize == 0))
{
return;
}
// Tasmota-style chip ID uses the lower 24 bits of the ESP32 efuse MAC.
const uint32_t chipId = static_cast<uint32_t>(ESP.getEfuseMac() & 0xFFFFFFULL);
snprintf(nameBuffer, bufferSize, "%s%06X", kBleDeviceNamePrefix, chipId);
}
bool queueBleByte(uint8_t value)
{
bool queued = false;
portENTER_CRITICAL(&g_bleQueueMux);
const size_t nextHead = (g_bleRxHead + 1) % kBleRxQueueSize;
if (nextHead == g_bleRxTail)
{
g_bleRxQueueOverflow = true;
}
else
{
g_bleRxQueue[g_bleRxHead] = value;
g_bleRxHead = nextHead;
queued = true;
}
portEXIT_CRITICAL(&g_bleQueueMux);
return queued;
}
bool dequeueBleByte(uint8_t *value)
{
bool hasData = false;
portENTER_CRITICAL(&g_bleQueueMux);
if (g_bleRxHead != g_bleRxTail)
{
*value = g_bleRxQueue[g_bleRxTail];
g_bleRxTail = (g_bleRxTail + 1) % kBleRxQueueSize;
hasData = true;
}
portEXIT_CRITICAL(&g_bleQueueMux);
return hasData;
}
class EggDuinoBleServerCallbacks : public NimBLEServerCallbacks
{
void onConnect(NimBLEServer *pServer, NimBLEConnInfo &connInfo)
{
(void)pServer;
(void)connInfo;
g_bleClientConnected = true;
Log("BLE client connected");
}
void onDisconnect(NimBLEServer *pServer, NimBLEConnInfo &connInfo, int reason)
{
(void)connInfo;
(void)reason;
g_bleClientConnected = false;
const bool restartedAdvertising = pServer->startAdvertising();
logBleDiag(String("BLE client disconnected; advertising restart: ") + (restartedAdvertising ? "ok" : "failed"));
}
};
class EggDuinoBleRxCallbacks : public NimBLECharacteristicCallbacks
{
void onWrite(NimBLECharacteristic *pCharacteristic, NimBLEConnInfo &connInfo)
{
(void)connInfo;
const std::string value = pCharacteristic->getValue();
for (size_t i = 0; i < value.size(); ++i)
{
queueBleByte(static_cast<uint8_t>(value[i]));
}
}
};
} // namespace
void startBleInterface()
{
char bleDeviceName[32] = {0};
buildBleDeviceName(bleDeviceName, sizeof(bleDeviceName));
if (bleDeviceName[0] == '\0')
{
snprintf(bleDeviceName, sizeof(bleDeviceName), "%sUNKNOWN", kBleDeviceNamePrefix);
}
logBleDiag("BLE init begin");
logBleDiag(String("BLE device name: ") + bleDeviceName);
logBleDiag(String("BLE service UUID: ") + kBleServiceUuid);
logBleDiag(String("BLE RX UUID: ") + kBleRxCharUuid);
logBleDiag(String("BLE TX UUID: ") + kBleTxCharUuid);
NimBLEDevice::init(bleDeviceName);
const bool blePowerSet = NimBLEDevice::setPower(ESP_PWR_LVL_P6);
const std::string bleAddress = NimBLEDevice::getAddress().toString();
logBleDiag(String("BLE radio address: ") + bleAddress.c_str());
logBleDiag(String("BLE TX power set: ") + (blePowerSet ? "ok" : "failed"));
g_pBleServer = NimBLEDevice::createServer();
if (g_pBleServer == NULL)
{
logBleDiag("BLE init failed: createServer returned null");
return;
}
g_pBleServer->setCallbacks(new EggDuinoBleServerCallbacks());
logBleDiag("BLE server created");
NimBLEService *pService = g_pBleServer->createService(kBleServiceUuid);
if (pService == NULL)
{
logBleDiag("BLE init failed: createService returned null");
return;
}
g_pBleTxCharacteristic = pService->createCharacteristic(
kBleTxCharUuid,
NIMBLE_PROPERTY::NOTIFY | NIMBLE_PROPERTY::READ);
if (g_pBleTxCharacteristic == NULL)
{
logBleDiag("BLE init failed: TX characteristic creation failed");
return;
}
NimBLECharacteristic *pRxCharacteristic = pService->createCharacteristic(
kBleRxCharUuid,
NIMBLE_PROPERTY::WRITE | NIMBLE_PROPERTY::WRITE_NR);
if (pRxCharacteristic == NULL)
{
logBleDiag("BLE init failed: RX characteristic creation failed");
return;
}
pRxCharacteristic->setCallbacks(new EggDuinoBleRxCallbacks());
logBleDiag("BLE characteristics created");
const bool serviceStarted = pService->start();
logBleDiag(String("BLE service start: ") + (serviceStarted ? "ok" : "failed"));
if (!serviceStarted)
{
return;
}
NimBLEAdvertising *pAdvertising = NimBLEDevice::getAdvertising();
if (pAdvertising == NULL)
{
logBleDiag("BLE init failed: getAdvertising returned null");
return;
}
pAdvertising->enableScanResponse(true);
const bool localNameSet = pAdvertising->setName(bleDeviceName);
const bool serviceUuidAdded = pAdvertising->addServiceUUID(kBleServiceUuid);
const bool advertisingStarted = pAdvertising->start();
logBleDiag(String("BLE advertising set local name: ") + (localNameSet ? "ok" : "failed"));
logBleDiag(String("BLE advertising add service UUID: ") + (serviceUuidAdded ? "ok" : "failed"));
logBleDiag(String("BLE advertising start: ") + (advertisingStarted ? "ok" : "failed"));
if (serviceUuidAdded && advertisingStarted)
{
logBleDiag("BLE service started");
}
}
void handleBleInterface()
{
if (g_bleRxQueueOverflow)
{
g_bleRxQueueOverflow = false;
Log("BLE RX queue overflow");
}
uint8_t value = 0;
while (dequeueBleByte(&value))
{
setActiveProtocolContext(&g_BLECmd, PROTOCOL_TRANSPORT_BLE);
g_BLECmd.readChar(static_cast<char>(value));
}
}
bool bleProtocolWrite(const char *message)
{
if ((message == NULL) || !g_bleClientConnected || (g_pBleTxCharacteristic == NULL))
{
return false;
}
const uint8_t *payload = reinterpret_cast<const uint8_t *>(message);
size_t remaining = strlen(message);
if (remaining == 0)
{
return true;
}
while (remaining > 0)
{
size_t chunkLen = remaining;
if (chunkLen > kBleNotifyChunkSize)
{
chunkLen = kBleNotifyChunkSize;
}
g_pBleTxCharacteristic->setValue(payload, chunkLen);
g_pBleTxCharacteristic->notify();
payload += chunkLen;
remaining -= chunkLen;
delay(3);
}
return true;
}
#endif

View File

@@ -397,6 +397,7 @@ void startWebInterface()
server.on("/api/logs", HTTP_GET, handleGetLogs);
server.onNotFound(handleNotFound);
server.begin();
startWifiProtocolInterface();
}
else
{

View File

@@ -8,13 +8,13 @@ void queryPen()
state = '1';
else
state = '0';
Serial.print(String(state) + "\r\n");
protocolWrite(String(state) + "\r\n");
sendAck();
}
void queryButton()
{
Serial.print(String(g_bPrgButtonState) + "\r\n");
protocolWrite(String(g_bPrgButtonState) + "\r\n");
sendAck();
g_bPrgButtonState = 0;
}
@@ -22,7 +22,7 @@ void queryButton()
void queryLayer()
{
Log(__FUNCTION__);
Serial.print(String(g_uiLayer) + "\r\n");
protocolWrite(String(g_uiLayer) + "\r\n");
sendAck();
}
@@ -31,7 +31,7 @@ void setLayer()
Log(__FUNCTION__);
uint32_t value = 0;
char *arg1;
arg1 = SCmd.next();
arg1 = nextCommandArg();
if (arg1 != NULL)
{
value = atoi(arg1);
@@ -44,7 +44,7 @@ void setLayer()
void queryNodeCount()
{
Serial.print(String(g_uiNodeCount) + "\r\n");
protocolWrite(String(g_uiNodeCount) + "\r\n");
sendAck();
}
@@ -53,7 +53,7 @@ void setNodeCount()
Log(__FUNCTION__);
uint32_t value = 0;
char *arg1;
arg1 = SCmd.next();
arg1 = nextCommandArg();
if (arg1 != NULL)
{
value = atoi(arg1);
@@ -112,7 +112,7 @@ void setPen()
moveToDestination();
arg = SCmd.next();
arg = nextCommandArg();
if (arg != NULL)
{
cmd = atoi(arg);
@@ -131,7 +131,7 @@ void setPen()
}
}
char *val;
val = SCmd.next();
val = nextCommandArg();
if (val != NULL)
{
value = atoi(val);
@@ -156,7 +156,7 @@ void togglePen()
moveToDestination();
arg = SCmd.next();
arg = nextCommandArg();
if (arg != NULL)
value = atoi(arg);
else
@@ -189,10 +189,10 @@ void enableMotors()
int value;
char *arg;
char *val;
arg = SCmd.next();
arg = nextCommandArg();
if (arg != NULL)
cmd = atoi(arg);
val = SCmd.next();
val = nextCommandArg();
if (val != NULL)
value = atoi(val);
// values parsed
@@ -237,11 +237,11 @@ void stepperModeConfigure()
int cmd;
int value;
char *arg;
arg = SCmd.next();
arg = nextCommandArg();
if (arg != NULL)
cmd = atoi(arg);
char *val;
val = SCmd.next();
val = nextCommandArg();
if (val != NULL)
value = atoi(val);
if ((arg != NULL) && (val != NULL))
@@ -281,8 +281,8 @@ void stepperModeConfigure()
void sendVersion()
{
Log(__FUNCTION__);
Serial.print(initSting);
Serial.print("\r\n");
protocolWrite(initSting);
protocolWrite("\r\n");
}
void unrecognized(const char *command)
@@ -299,21 +299,29 @@ void ignore()
void makeComInterface()
{
SCmd.addCommand("v", sendVersion);
SCmd.addCommand("EM", enableMotors);
SCmd.addCommand("SC", stepperModeConfigure);
SCmd.addCommand("SP", setPen);
SCmd.addCommand("SM", stepperMove);
SCmd.addCommand("SE", ignore);
SCmd.addCommand("TP", togglePen);
SCmd.addCommand("PO", ignore); // Engraver command, not implemented, gives fake answer
SCmd.addCommand("NI", nodeCountIncrement);
SCmd.addCommand("ND", nodeCountDecrement);
SCmd.addCommand("SN", setNodeCount);
SCmd.addCommand("QN", queryNodeCount);
SCmd.addCommand("SL", setLayer);
SCmd.addCommand("QL", queryLayer);
SCmd.addCommand("QP", queryPen);
SCmd.addCommand("QB", queryButton); //"PRG" Button,
SCmd.setDefaultHandler(unrecognized); // Handler for command that isn't matched (says "What?")
auto registerCommands = [](SerialCommand &cmd) {
cmd.addCommand("v", sendVersion);
cmd.addCommand("EM", enableMotors);
cmd.addCommand("SC", stepperModeConfigure);
cmd.addCommand("SP", setPen);
cmd.addCommand("SM", stepperMove);
cmd.addCommand("SE", ignore);
cmd.addCommand("TP", togglePen);
cmd.addCommand("PO", ignore); // Engraver command, not implemented, gives fake answer
cmd.addCommand("NI", nodeCountIncrement);
cmd.addCommand("ND", nodeCountDecrement);
cmd.addCommand("SN", setNodeCount);
cmd.addCommand("QN", queryNodeCount);
cmd.addCommand("SL", setLayer);
cmd.addCommand("QL", queryLayer);
cmd.addCommand("QP", queryPen);
cmd.addCommand("QB", queryButton); //"PRG" Button,
cmd.setDefaultHandler(unrecognized); // Handler for command that isn't matched (says "What?")
};
registerCommands(SCmd);
#ifdef ESP32
registerCommands(g_BLECmd);
registerCommands(g_WifiCmd);
#endif
}

View File

@@ -52,13 +52,13 @@ void storePenDownPosInEE()
void sendAck()
{
Log(__FUNCTION__);
Serial.print("OK\r\n");
protocolWrite("OK\r\n");
}
void sendError()
{
Log(__FUNCTION__);
Serial.print("unknown CMD\r\n");
protocolWrite("unknown CMD\r\n");
}
void motorsOff()
@@ -92,19 +92,19 @@ void toggleMotors()
bool parseSMArgs(uint16_t *duration, int *penStepsEBB, int *rotStepsEBB)
{
char *arg1;
char *arg2;
char *arg3;
arg1 = SCmd.next();
char *arg1 = NULL;
char *arg2 = NULL;
char *arg3 = NULL;
arg1 = nextCommandArg();
if (arg1 != NULL)
{
*duration = atoi(arg1);
arg2 = SCmd.next();
arg2 = nextCommandArg();
}
if (arg2 != NULL)
{
*penStepsEBB = atoi(arg2);
arg3 = SCmd.next();
arg3 = nextCommandArg();
}
if (arg3 != NULL)
{

156
src/WiFi_Protocol.cpp Normal file
View File

@@ -0,0 +1,156 @@
#include "EggDuino.h"
#ifdef ESP32
#include <WebSocketsServer.h>
namespace
{
constexpr uint16_t kWifiProtocolPort = 1337;
constexpr uint8_t kInvalidWifiClientId = 0xFF;
constexpr size_t kWifiRxQueueSize = 1024;
struct WifiRxByte
{
uint8_t value;
};
WebSocketsServer g_wifiProtocolSocket(kWifiProtocolPort);
WifiRxByte g_wifiRxQueue[kWifiRxQueueSize];
size_t g_wifiRxHead = 0;
size_t g_wifiRxTail = 0;
bool g_wifiRxOverflow = false;
bool g_wifiProtocolStarted = false;
uint8_t g_wifiProtocolClientId = kInvalidWifiClientId;
bool queueWifiByte(uint8_t value)
{
const size_t nextHead = (g_wifiRxHead + 1) % kWifiRxQueueSize;
if (nextHead == g_wifiRxTail)
{
g_wifiRxOverflow = true;
return false;
}
g_wifiRxQueue[g_wifiRxHead].value = value;
g_wifiRxHead = nextHead;
return true;
}
bool dequeueWifiByte(WifiRxByte *byte)
{
if ((byte == NULL) || (g_wifiRxHead == g_wifiRxTail))
{
return false;
}
*byte = g_wifiRxQueue[g_wifiRxTail];
g_wifiRxTail = (g_wifiRxTail + 1) % kWifiRxQueueSize;
return true;
}
void handleWifiSocketEvent(uint8_t clientId, WStype_t type, uint8_t *payload, size_t length)
{
(void)payload;
switch (type)
{
case WStype_CONNECTED:
if (g_wifiProtocolClientId == kInvalidWifiClientId)
{
g_wifiProtocolClientId = clientId;
}
Log(String("WiFi protocol client connected: ") + clientId);
break;
case WStype_DISCONNECTED:
if (g_wifiProtocolClientId == clientId)
{
g_wifiProtocolClientId = kInvalidWifiClientId;
}
Log(String("WiFi protocol client disconnected: ") + clientId);
break;
case WStype_TEXT:
case WStype_BIN:
if (g_wifiProtocolClientId == kInvalidWifiClientId)
{
g_wifiProtocolClientId = clientId;
}
if (clientId != g_wifiProtocolClientId)
{
break;
}
for (size_t i = 0; i < length; ++i)
{
queueWifiByte(payload[i]);
}
break;
default:
break;
}
}
} // namespace
void startWifiProtocolInterface()
{
if (g_wifiProtocolStarted)
{
return;
}
if (WiFi.status() != WL_CONNECTED)
{
return;
}
g_wifiProtocolSocket.begin();
g_wifiProtocolSocket.onEvent(handleWifiSocketEvent);
g_wifiProtocolStarted = true;
Log(String("WiFi EggBot protocol ws://") + WiFi.localIP().toString() + ":" + kWifiProtocolPort);
}
void handleWifiProtocolInterface()
{
if (!g_wifiProtocolStarted)
{
if (WiFi.status() == WL_CONNECTED)
{
startWifiProtocolInterface();
}
return;
}
g_wifiProtocolSocket.loop();
if (g_wifiRxOverflow)
{
g_wifiRxOverflow = false;
Log("WiFi protocol RX queue overflow");
}
WifiRxByte byte = {0};
while (dequeueWifiByte(&byte))
{
setActiveProtocolContext(&g_WifiCmd, PROTOCOL_TRANSPORT_WIFI);
g_WifiCmd.readChar(static_cast<char>(byte.value));
}
}
bool wifiProtocolWrite(const char *message)
{
if (!g_wifiProtocolStarted || (message == NULL))
{
return false;
}
if ((g_wifiProtocolClientId == kInvalidWifiClientId) || !g_wifiProtocolSocket.clientIsConnected(g_wifiProtocolClientId))
{
return false;
}
return g_wifiProtocolSocket.sendTXT(g_wifiProtocolClientId, message);
}
#endif

View File

@@ -36,6 +36,16 @@ FastAccelStepper *g_pStepperPen = NULL;
// make Objects
Servo penServo;
SerialCommand SCmd;
#ifdef ESP32
SerialCommand g_BLECmd;
SerialCommand g_WifiCmd;
#endif
namespace
{
SerialCommand *g_pActiveParser = &SCmd;
ProtocolTransport g_activeTransport = PROTOCOL_TRANSPORT_SERIAL;
}
// create Buttons
#ifdef prgButton
@@ -63,6 +73,52 @@ float fROT_STEP_CORRECTION = 16.0 / rotMicrostep; // devide EBB-Coordinates by t
float fPEN_STEP_CORRECTION = 16.0 / penMicrostep; // devide EBB-Coordinates by this factor to get EGGduino-Steps
boolean g_bMotorsEnabled = 0;
void setActiveProtocolContext(SerialCommand *parser, ProtocolTransport transport)
{
if (parser != NULL)
{
g_pActiveParser = parser;
}
g_activeTransport = transport;
}
char *nextCommandArg()
{
return g_pActiveParser->next();
}
void protocolWrite(const char *message)
{
if (message == NULL)
{
return;
}
#ifdef ESP32
if (g_activeTransport == PROTOCOL_TRANSPORT_BLE)
{
if (bleProtocolWrite(message))
{
return;
}
}
if (g_activeTransport == PROTOCOL_TRANSPORT_WIFI)
{
if (wifiProtocolWrite(message))
{
return;
}
}
#endif
Serial.print(message);
}
void protocolWrite(const String &message)
{
protocolWrite(message.c_str());
}
// Stepper Test
#ifdef TEST
// #define dirPinStepper 16
@@ -77,6 +133,7 @@ void setup()
Log("Starting...");
makeComInterface();
initHardware();
startBleInterface();
startWebInterface();
}
@@ -134,8 +191,11 @@ void loop()
}
#else
// moveOneStep();
setActiveProtocolContext(&SCmd, PROTOCOL_TRANSPORT_SERIAL);
SCmd.readSerial();
handleBleInterface();
handleWebInterface();
handleWifiProtocolInterface();
#endif
#ifdef penToggleButton