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

View File

@@ -0,0 +1,46 @@
# EggBot BLE Serial Protocol
## Scope
This transport exposes the same EggBot command protocol as serial, but over BLE GATT.
- Protocol commands and responses are unchanged.
- Framing is unchanged (`\r` command terminator, `\r\n` response lines).
## BLE GATT Profile
- Device name: `EggDuino`
- Service UUID: `6e400001-b5a3-f393-e0a9-e50e24dcca9e`
- RX characteristic (host -> EggDuino): `6e400002-b5a3-f393-e0a9-e50e24dcca9e`
- Properties: `Write`, `Write Without Response`
- TX characteristic (EggDuino -> host): `6e400003-b5a3-f393-e0a9-e50e24dcca9e`
- Properties: `Notify`, `Read`
## Data Model
- Host writes plain ASCII command bytes to RX.
- Firmware parses bytes with the same EggBot command parser used for USB serial.
- Firmware sends responses via TX notifications.
- Long responses are segmented into BLE-sized notification chunks; host must reassemble by bytes and parse lines by `\r\n`.
## Compatibility Rules
- A BLE client must send commands exactly as serial hosts do.
- Each command must end with `\r`.
- Data-returning commands (`QP`, `QB`, `QN`, `QL`) return a value line before final status (`OK\r\n`).
- Unknown/invalid command format returns `unknown CMD\r\n`.
## Example BLE Transactions
Version query:
- Write RX: `v\r`
- Notify TX: `EBBv13_and_above Protocol emulated by Eggduino-Firmware V1.6a\r\n`
Move command:
- Write RX: `SM,100,0,200\r`
- Notify TX: `OK\r\n`
Query node count:
- Write RX: `QN\r`
- Notify TX: `<number>\r\n`
- Notify TX: `OK\r\n`
## Operational Notes
- BLE receive bytes are queued and parsed in the main firmware loop.
- If the BLE RX queue overruns, excess bytes are dropped and a log entry is generated.
- BLE and USB serial can coexist; each command response is routed to the transport that received that command.

View File

@@ -0,0 +1,53 @@
# EggBot Serial Protocol (USB/UART)
## Scope
This firmware emulates the EggBot/EBB command protocol over serial transport.
- Transport: USB CDC/UART (`115200 8N1`)
- Command separator: `,`
- Command terminator: carriage return (`\r`, ASCII 0x0D)
- Response terminator: carriage return + newline (`\r\n`)
## Handshake and Generic Responses
- Version query: `v\r`
- Version response: `EBBv13_and_above Protocol emulated by Eggduino-Firmware V1.6a\r\n`
- Success response: `OK\r\n`
- Error response: `unknown CMD\r\n`
## Implemented Commands
All commands are case-sensitive and comma-delimited.
- `EM,<mode>[,<ignored>]`
- `SC,<id>,<value>`
- `SP,<penState>[,<delayMs>]`
- `SM,<durationMs>,<penSteps>,<rotSteps>`
- `SE` (ack-only placeholder)
- `TP[,<delayMs>]`
- `PO` (ack-only placeholder)
- `NI`
- `ND`
- `SN,<nodeCount>`
- `QN`
- `SL,<layer>`
- `QL`
- `QP`
- `QB`
## Command Behavior Notes
- `SM`: firmware blocks until previous move completes, sends `OK`, then runs the requested move.
- `SP` and `TP`: support optional delay argument in milliseconds.
- `QP`: returns pen state line (`1` for pen up, `0` for pen down), then `OK`.
- `QB`: returns button state (`0` or `1`), then `OK`; internal button latch is reset after query.
- `QN`, `QL`: return numeric line first, then `OK`.
- `SC` supports these IDs:
- `4`: set pen-down servo position (EBB value to servo-mapped value)
- `5`: set pen-up servo position
- `6`, `7`: accepted, ignored, and acknowledged
- `11`: set servo rate up
- `12`: set servo rate down
## Timing/Parsing Requirements for Hosts
- Always terminate each command with `\r`.
- Do not rely on `\n` as a command terminator.
- Read until `OK\r\n` (or `unknown CMD\r\n`) to complete command transactions.
- For commands that return data (`QP`, `QB`, `QN`, `QL`), read one data line plus the final status line.

View File

@@ -0,0 +1,45 @@
# EggBot WiFi Serial Protocol
## Scope
This transport exposes the same EggBot command protocol as serial, but over WiFi/WebSocket.
- Protocol commands and responses are unchanged.
- Framing is unchanged (`\r` command terminator, `\r\n` response lines).
## Transport Profile
- Protocol endpoint: `ws://<eggs-esp32-ip>:1337/`
- WebSocket message type: text or binary frames accepted
- Payload: ASCII command bytes
## Data Model
- Host writes command bytes to the WebSocket.
- Firmware parses incoming bytes with the same EggBot parser used for USB serial and BLE.
- Firmware sends responses as WebSocket text messages.
- Hosts must treat inbound data as a stream and parse lines by `\r\n`.
## Compatibility Rules
- A WiFi client must send commands exactly as serial hosts do.
- Each command must end with `\r`.
- Data-returning commands (`QP`, `QB`, `QN`, `QL`) return a value line before final status (`OK\r\n`).
- Unknown/invalid command format returns `unknown CMD\r\n`.
## Example WiFi Transactions
Version query:
- Send: `v\r`
- Receive: `EBBv13_and_above Protocol emulated by Eggduino-Firmware V1.6a\r\n`
Move command:
- Send: `SM,100,0,200\r`
- Receive: `OK\r\n`
Query layer:
- Send: `QL\r`
- Receive: `<layer>\r\n`
- Receive: `OK\r\n`
## Operational Notes
- The WiFi protocol endpoint is started only when ESP32 station WiFi is connected.
- Incoming WiFi bytes are queued and parsed in the main loop.
- If the WiFi RX queue overruns, excess bytes are dropped and a log entry is generated.
- WiFi transport is single active client: first connected sender is accepted until disconnect.
- USB serial, BLE, and WiFi can coexist; responses are routed to the transport that received each command.

View File

@@ -0,0 +1,185 @@
const SERVICE_UUID = '6e400001-b5a3-f393-e0a9-e50e24dcca9e';
const RX_UUID = '6e400002-b5a3-f393-e0a9-e50e24dcca9e';
const TX_UUID = '6e400003-b5a3-f393-e0a9-e50e24dcca9e';
const encoder = new TextEncoder();
const decoder = new TextDecoder();
let device = null;
let server = null;
let rxCharacteristic = null;
let txCharacteristic = null;
let lineBuffer = '';
let commandQueue = Promise.resolve();
const lineListeners = new Set();
function emitLine(line) {
for (const listener of lineListeners) {
listener(line);
}
}
function handleNotification(event) {
const dataView = event.target.value;
const chunk = decoder.decode(dataView, { stream: true });
lineBuffer += chunk;
while (true) {
const delimiterIndex = lineBuffer.indexOf('\r\n');
if (delimiterIndex < 0) {
break;
}
const line = lineBuffer.slice(0, delimiterIndex);
lineBuffer = lineBuffer.slice(delimiterIndex + 2);
emitLine(line);
}
}
function handleDisconnected() {
server = null;
rxCharacteristic = null;
if (txCharacteristic) {
txCharacteristic.removeEventListener('characteristicvaluechanged', handleNotification);
}
txCharacteristic = null;
}
async function writeToRxCharacteristic(payloadBytes) {
if (!rxCharacteristic) {
throw new Error('EggDuino is not connected');
}
if (typeof rxCharacteristic.writeValueWithoutResponse === 'function') {
await rxCharacteristic.writeValueWithoutResponse(payloadBytes);
return;
}
if (typeof rxCharacteristic.writeValueWithResponse === 'function') {
await rxCharacteristic.writeValueWithResponse(payloadBytes);
return;
}
await rxCharacteristic.writeValue(payloadBytes);
}
function normalizeCommand(command) {
return command.endsWith('\r') ? command : `${command}\r`;
}
function enqueueCommand(fn) {
const run = commandQueue.then(fn, fn);
commandQueue = run.catch(() => {});
return run;
}
export async function connectEggDuino() {
if (server?.connected && rxCharacteristic && txCharacteristic) {
return;
}
try {
device = await navigator.bluetooth.requestDevice({
filters: [{ services: [SERVICE_UUID] }],
optionalServices: [SERVICE_UUID],
});
} catch (error) {
throw new Error(`BLE request failed: ${error.message}`);
}
device.addEventListener('gattserverdisconnected', handleDisconnected);
try {
server = await device.gatt.connect();
} catch (error) {
throw new Error(`BLE gatt connect failed: ${error.message}`);
}
let service;
try {
service = await server.getPrimaryService(SERVICE_UUID);
} catch (error) {
throw new Error(`BLE service lookup failed: ${error.message}`);
}
try {
rxCharacteristic = await service.getCharacteristic(RX_UUID);
txCharacteristic = await service.getCharacteristic(TX_UUID);
} catch (error) {
throw new Error(`BLE characteristic lookup failed: ${error.message}`);
}
try {
await txCharacteristic.startNotifications();
txCharacteristic.addEventListener('characteristicvaluechanged', handleNotification);
} catch (error) {
throw new Error(`BLE notifications setup failed: ${error.message}`);
}
}
export function disconnectEggDuino() {
if (!device) {
return;
}
if (txCharacteristic) {
txCharacteristic.removeEventListener('characteristicvaluechanged', handleNotification);
}
if (device.gatt?.connected) {
device.gatt.disconnect();
}
handleDisconnected();
}
export async function sendEggBotCommand(command) {
const normalized = normalizeCommand(command);
await writeToRxCharacteristic(encoder.encode(normalized));
}
export async function sendEggBotCommandExpectOk(command, timeoutMs = 2000) {
return enqueueCommand(() => new Promise(async (resolve, reject) => {
const collectedLines = [];
const timeout = setTimeout(() => {
lineListeners.delete(onResponseLine);
reject(new Error('EggBot response timeout'));
}, timeoutMs);
const onResponseLine = (line) => {
collectedLines.push(line);
if (line === 'OK') {
clearTimeout(timeout);
lineListeners.delete(onResponseLine);
resolve(collectedLines.slice(0, -1));
}
if (line === 'unknown CMD') {
clearTimeout(timeout);
lineListeners.delete(onResponseLine);
reject(new Error(`EggBot error: ${collectedLines.join(' | ')}`));
}
};
lineListeners.add(onResponseLine);
try {
await sendEggBotCommand(command);
} catch (error) {
clearTimeout(timeout);
lineListeners.delete(onResponseLine);
reject(error);
}
}));
}
export function onLine(callback) {
lineListeners.add(callback);
}
export function offLine(callback) {
lineListeners.delete(callback);
}

View File

@@ -0,0 +1,183 @@
const decoder = new TextDecoder();
let socket = null;
let lineBuffer = '';
let commandQueue = Promise.resolve();
const lineListeners = new Set();
function emitLine(line) {
for (const listener of lineListeners) {
listener(line);
}
}
function parseIncomingChunk(chunk) {
lineBuffer += chunk;
while (true) {
const delimiterIndex = lineBuffer.indexOf('\r\n');
if (delimiterIndex < 0) {
break;
}
const line = lineBuffer.slice(0, delimiterIndex);
lineBuffer = lineBuffer.slice(delimiterIndex + 2);
emitLine(line);
}
}
function handleSocketMessage(event) {
if (typeof event.data === 'string') {
parseIncomingChunk(event.data);
return;
}
if (event.data instanceof ArrayBuffer) {
parseIncomingChunk(decoder.decode(event.data));
}
}
function normalizeCommand(command) {
return command.endsWith('\r') ? command : `${command}\r`;
}
function enqueueCommand(fn) {
const run = commandQueue.then(fn, fn);
commandQueue = run.catch(() => {});
return run;
}
function ensureSocketReady() {
if (!socket || socket.readyState !== WebSocket.OPEN) {
throw new Error('EggDuino WiFi socket is not connected');
}
}
function buildSocketUrl(options) {
if (options.url) {
return options.url;
}
const host = options.host;
if (!host) {
throw new Error('Missing EggDuino host');
}
const secure = options.secure === true;
const protocol = secure ? 'wss' : 'ws';
const port = Number(options.port ?? 1337);
const path = options.path ?? '/';
return `${protocol}://${host}:${port}${path}`;
}
export async function connectEggDuinoWifi(options = {}) {
if (socket && socket.readyState === WebSocket.OPEN) {
return;
}
if (socket && socket.readyState === WebSocket.CONNECTING) {
await new Promise((resolve, reject) => {
const onOpen = () => {
socket.removeEventListener('error', onError);
resolve();
};
const onError = () => {
socket.removeEventListener('open', onOpen);
reject(new Error('EggDuino WiFi socket failed while connecting'));
};
socket.addEventListener('open', onOpen, { once: true });
socket.addEventListener('error', onError, { once: true });
});
return;
}
const url = buildSocketUrl(options);
socket = new WebSocket(url);
socket.binaryType = 'arraybuffer';
socket.addEventListener('message', handleSocketMessage);
socket.addEventListener('close', () => {
socket = null;
});
await new Promise((resolve, reject) => {
const onOpen = () => {
socket.removeEventListener('error', onError);
resolve();
};
const onError = () => {
socket.removeEventListener('open', onOpen);
socket?.close();
socket = null;
reject(new Error(`EggDuino WiFi socket connection failed: ${url}`));
};
socket.addEventListener('open', onOpen, { once: true });
socket.addEventListener('error', onError, { once: true });
});
}
export function disconnectEggDuinoWifi() {
if (!socket) {
return;
}
socket.removeEventListener('message', handleSocketMessage);
if (socket.readyState === WebSocket.OPEN || socket.readyState === WebSocket.CONNECTING) {
socket.close();
}
socket = null;
}
export async function sendEggBotCommand(command) {
ensureSocketReady();
socket.send(normalizeCommand(command));
}
export async function sendEggBotCommandExpectOk(command, timeoutMs = 2000) {
return enqueueCommand(() => new Promise(async (resolve, reject) => {
const collectedLines = [];
const timeout = setTimeout(() => {
lineListeners.delete(onResponseLine);
reject(new Error('EggBot response timeout'));
}, timeoutMs);
const onResponseLine = (line) => {
collectedLines.push(line);
if (line === 'OK') {
clearTimeout(timeout);
lineListeners.delete(onResponseLine);
resolve(collectedLines.slice(0, -1));
}
if (line === 'unknown CMD') {
clearTimeout(timeout);
lineListeners.delete(onResponseLine);
reject(new Error(`EggBot error: ${collectedLines.join(' | ')}`));
}
};
lineListeners.add(onResponseLine);
try {
await sendEggBotCommand(command);
} catch (error) {
clearTimeout(timeout);
lineListeners.delete(onResponseLine);
reject(error);
}
}));
}
export function onLine(callback) {
lineListeners.add(callback);
}
export function offLine(callback) {
lineListeners.delete(callback);
}

View File

@@ -67,6 +67,10 @@ extern FastAccelStepper *g_pStepperPen;
extern Servo penServo; extern Servo penServo;
extern SerialCommand SCmd; extern SerialCommand SCmd;
#ifdef ESP32
extern SerialCommand g_BLECmd;
extern SerialCommand g_WifiCmd;
#endif
extern int g_iPenUpPos; extern int g_iPenUpPos;
extern int g_iPenDownPos; extern int g_iPenDownPos;
@@ -82,10 +86,20 @@ extern float fROT_STEP_CORRECTION;
extern float fPEN_STEP_CORRECTION; extern float fPEN_STEP_CORRECTION;
extern boolean g_bMotorsEnabled; extern boolean g_bMotorsEnabled;
enum ProtocolTransport {
PROTOCOL_TRANSPORT_SERIAL = 0,
PROTOCOL_TRANSPORT_BLE = 1,
PROTOCOL_TRANSPORT_WIFI = 2,
};
extern ConfigParameter configParameters[]; extern ConfigParameter configParameters[];
extern const size_t configParameterCount; extern const size_t configParameterCount;
void makeComInterface(); void makeComInterface();
void setActiveProtocolContext(SerialCommand *parser, ProtocolTransport transport);
char *nextCommandArg();
void protocolWrite(const char *message);
void protocolWrite(const String &message);
void initHardware(); void initHardware();
void moveOneStep(); void moveOneStep();
void moveToDestination(); void moveToDestination();
@@ -108,6 +122,29 @@ String buildConfigJson();
bool applyConfigJson(const String &payload, String &errorMessage); bool applyConfigJson(const String &payload, String &errorMessage);
void startWebInterface(); void startWebInterface();
void handleWebInterface(); void handleWebInterface();
#ifdef ESP32
void startBleInterface();
void handleBleInterface();
bool bleProtocolWrite(const char *message);
void startWifiProtocolInterface();
void handleWifiProtocolInterface();
bool wifiProtocolWrite(const char *message);
#else
inline void startBleInterface() {}
inline void handleBleInterface() {}
inline bool bleProtocolWrite(const char *message)
{
(void)message;
return false;
}
inline void startWifiProtocolInterface() {}
inline void handleWifiProtocolInterface() {}
inline bool wifiProtocolWrite(const char *message)
{
(void)message;
return false;
}
#endif
void Log(const String &message); void Log(const String &message);
void Log(const char *message); void Log(const char *message);
String buildLogsJson(uint32_t sinceSeq); String buildLogsJson(uint32_t sinceSeq);

View File

@@ -71,8 +71,17 @@ void SerialCommand::setDefaultHandler(void (*function)(const char *)) {
* buffer for a prefix command, and calls handlers setup by addCommand() member * buffer for a prefix command, and calls handlers setup by addCommand() member
*/ */
void SerialCommand::readSerial() { void SerialCommand::readSerial() {
while (Serial.available() > 0) { readSerial(Serial);
char inChar = Serial.read(); // Read single available character, there may be more waiting }
void SerialCommand::readSerial(Stream &stream) {
while (stream.available() > 0) {
char inChar = stream.read(); // Read single available character, there may be more waiting
readChar(inChar);
}
}
void SerialCommand::readChar(char inChar) {
#ifdef SERIALCOMMAND_DEBUG #ifdef SERIALCOMMAND_DEBUG
Serial.print(inChar); // Echo back to serial stream Serial.print(inChar); // Echo back to serial stream
#endif #endif
@@ -113,8 +122,7 @@ void SerialCommand::readSerial() {
} }
} }
clearBuffer(); clearBuffer();
} } else if (isprint(inChar)) { // Only printable characters into the buffer
else if (isprint(inChar)) { // Only printable characters into the buffer
if (bufPos < SERIALCOMMAND_BUFFER) { if (bufPos < SERIALCOMMAND_BUFFER) {
buffer[bufPos++] = inChar; // Put character into buffer buffer[bufPos++] = inChar; // Put character into buffer
buffer[bufPos] = '\0'; // Null terminate buffer[bufPos] = '\0'; // Null terminate
@@ -124,7 +132,6 @@ void SerialCommand::readSerial() {
#endif #endif
} }
} }
}
} }
/* /*

View File

@@ -49,6 +49,8 @@ class SerialCommand {
void setDefaultHandler(void (*function)(const char *)); // A handler to call when no valid command received. void setDefaultHandler(void (*function)(const char *)); // A handler to call when no valid command received.
void readSerial(); // Main entry point. void readSerial(); // Main entry point.
void readSerial(Stream &stream); // Reads commands from any Stream-compatible transport.
void readChar(char inChar); // Feeds one incoming character into the parser.
void clearBuffer(); // Clears the input buffer. void clearBuffer(); // Clears the input buffer.
char *next(); // Returns pointer to next token found in command buffer (for getting arguments to commands). char *next(); // Returns pointer to next token found in command buffer (for getting arguments to commands).

View File

@@ -19,15 +19,20 @@ lib_deps =
madhephaestus/ESP32Servo@^3.0.6 madhephaestus/ESP32Servo@^3.0.6
bblanchon/ArduinoJson@^6.21.5 bblanchon/ArduinoJson@^6.21.5
gin66/FastAccelStepper@^0.33.13 gin66/FastAccelStepper@^0.33.13
h2zero/NimBLE-Arduino@^2.3.6
links2004/WebSockets@^2.6.1
[env:uno_macos] [env:uno_macos]
platform = platformio/espressif32 platform = platformio/espressif32
board = esp32dev board = esp32dev
framework = arduino framework = arduino
monitor_speed = 115200 monitor_speed = 115200
monitor_port = /dev/cu.usb*
upload_speed = 115200 upload_speed = 115200
upload_port = /dev/tty.usbserial-110 upload_port = /dev/cu.usb*
lib_deps = lib_deps =
madhephaestus/ESP32Servo@^3.0.6 madhephaestus/ESP32Servo@^3.0.6
bblanchon/ArduinoJson@^6.21.5 bblanchon/ArduinoJson@^6.21.5
gin66/FastAccelStepper@^0.33.13 gin66/FastAccelStepper@^0.33.13
h2zero/NimBLE-Arduino@^2.3.6
links2004/WebSockets@^2.6.1

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.on("/api/logs", HTTP_GET, handleGetLogs);
server.onNotFound(handleNotFound); server.onNotFound(handleNotFound);
server.begin(); server.begin();
startWifiProtocolInterface();
} }
else else
{ {

View File

@@ -8,13 +8,13 @@ void queryPen()
state = '1'; state = '1';
else else
state = '0'; state = '0';
Serial.print(String(state) + "\r\n"); protocolWrite(String(state) + "\r\n");
sendAck(); sendAck();
} }
void queryButton() void queryButton()
{ {
Serial.print(String(g_bPrgButtonState) + "\r\n"); protocolWrite(String(g_bPrgButtonState) + "\r\n");
sendAck(); sendAck();
g_bPrgButtonState = 0; g_bPrgButtonState = 0;
} }
@@ -22,7 +22,7 @@ void queryButton()
void queryLayer() void queryLayer()
{ {
Log(__FUNCTION__); Log(__FUNCTION__);
Serial.print(String(g_uiLayer) + "\r\n"); protocolWrite(String(g_uiLayer) + "\r\n");
sendAck(); sendAck();
} }
@@ -31,7 +31,7 @@ void setLayer()
Log(__FUNCTION__); Log(__FUNCTION__);
uint32_t value = 0; uint32_t value = 0;
char *arg1; char *arg1;
arg1 = SCmd.next(); arg1 = nextCommandArg();
if (arg1 != NULL) if (arg1 != NULL)
{ {
value = atoi(arg1); value = atoi(arg1);
@@ -44,7 +44,7 @@ void setLayer()
void queryNodeCount() void queryNodeCount()
{ {
Serial.print(String(g_uiNodeCount) + "\r\n"); protocolWrite(String(g_uiNodeCount) + "\r\n");
sendAck(); sendAck();
} }
@@ -53,7 +53,7 @@ void setNodeCount()
Log(__FUNCTION__); Log(__FUNCTION__);
uint32_t value = 0; uint32_t value = 0;
char *arg1; char *arg1;
arg1 = SCmd.next(); arg1 = nextCommandArg();
if (arg1 != NULL) if (arg1 != NULL)
{ {
value = atoi(arg1); value = atoi(arg1);
@@ -112,7 +112,7 @@ void setPen()
moveToDestination(); moveToDestination();
arg = SCmd.next(); arg = nextCommandArg();
if (arg != NULL) if (arg != NULL)
{ {
cmd = atoi(arg); cmd = atoi(arg);
@@ -131,7 +131,7 @@ void setPen()
} }
} }
char *val; char *val;
val = SCmd.next(); val = nextCommandArg();
if (val != NULL) if (val != NULL)
{ {
value = atoi(val); value = atoi(val);
@@ -156,7 +156,7 @@ void togglePen()
moveToDestination(); moveToDestination();
arg = SCmd.next(); arg = nextCommandArg();
if (arg != NULL) if (arg != NULL)
value = atoi(arg); value = atoi(arg);
else else
@@ -189,10 +189,10 @@ void enableMotors()
int value; int value;
char *arg; char *arg;
char *val; char *val;
arg = SCmd.next(); arg = nextCommandArg();
if (arg != NULL) if (arg != NULL)
cmd = atoi(arg); cmd = atoi(arg);
val = SCmd.next(); val = nextCommandArg();
if (val != NULL) if (val != NULL)
value = atoi(val); value = atoi(val);
// values parsed // values parsed
@@ -237,11 +237,11 @@ void stepperModeConfigure()
int cmd; int cmd;
int value; int value;
char *arg; char *arg;
arg = SCmd.next(); arg = nextCommandArg();
if (arg != NULL) if (arg != NULL)
cmd = atoi(arg); cmd = atoi(arg);
char *val; char *val;
val = SCmd.next(); val = nextCommandArg();
if (val != NULL) if (val != NULL)
value = atoi(val); value = atoi(val);
if ((arg != NULL) && (val != NULL)) if ((arg != NULL) && (val != NULL))
@@ -281,8 +281,8 @@ void stepperModeConfigure()
void sendVersion() void sendVersion()
{ {
Log(__FUNCTION__); Log(__FUNCTION__);
Serial.print(initSting); protocolWrite(initSting);
Serial.print("\r\n"); protocolWrite("\r\n");
} }
void unrecognized(const char *command) void unrecognized(const char *command)
@@ -299,21 +299,29 @@ void ignore()
void makeComInterface() void makeComInterface()
{ {
SCmd.addCommand("v", sendVersion); auto registerCommands = [](SerialCommand &cmd) {
SCmd.addCommand("EM", enableMotors); cmd.addCommand("v", sendVersion);
SCmd.addCommand("SC", stepperModeConfigure); cmd.addCommand("EM", enableMotors);
SCmd.addCommand("SP", setPen); cmd.addCommand("SC", stepperModeConfigure);
SCmd.addCommand("SM", stepperMove); cmd.addCommand("SP", setPen);
SCmd.addCommand("SE", ignore); cmd.addCommand("SM", stepperMove);
SCmd.addCommand("TP", togglePen); cmd.addCommand("SE", ignore);
SCmd.addCommand("PO", ignore); // Engraver command, not implemented, gives fake answer cmd.addCommand("TP", togglePen);
SCmd.addCommand("NI", nodeCountIncrement); cmd.addCommand("PO", ignore); // Engraver command, not implemented, gives fake answer
SCmd.addCommand("ND", nodeCountDecrement); cmd.addCommand("NI", nodeCountIncrement);
SCmd.addCommand("SN", setNodeCount); cmd.addCommand("ND", nodeCountDecrement);
SCmd.addCommand("QN", queryNodeCount); cmd.addCommand("SN", setNodeCount);
SCmd.addCommand("SL", setLayer); cmd.addCommand("QN", queryNodeCount);
SCmd.addCommand("QL", queryLayer); cmd.addCommand("SL", setLayer);
SCmd.addCommand("QP", queryPen); cmd.addCommand("QL", queryLayer);
SCmd.addCommand("QB", queryButton); //"PRG" Button, cmd.addCommand("QP", queryPen);
SCmd.setDefaultHandler(unrecognized); // Handler for command that isn't matched (says "What?") 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() void sendAck()
{ {
Log(__FUNCTION__); Log(__FUNCTION__);
Serial.print("OK\r\n"); protocolWrite("OK\r\n");
} }
void sendError() void sendError()
{ {
Log(__FUNCTION__); Log(__FUNCTION__);
Serial.print("unknown CMD\r\n"); protocolWrite("unknown CMD\r\n");
} }
void motorsOff() void motorsOff()
@@ -92,19 +92,19 @@ void toggleMotors()
bool parseSMArgs(uint16_t *duration, int *penStepsEBB, int *rotStepsEBB) bool parseSMArgs(uint16_t *duration, int *penStepsEBB, int *rotStepsEBB)
{ {
char *arg1; char *arg1 = NULL;
char *arg2; char *arg2 = NULL;
char *arg3; char *arg3 = NULL;
arg1 = SCmd.next(); arg1 = nextCommandArg();
if (arg1 != NULL) if (arg1 != NULL)
{ {
*duration = atoi(arg1); *duration = atoi(arg1);
arg2 = SCmd.next(); arg2 = nextCommandArg();
} }
if (arg2 != NULL) if (arg2 != NULL)
{ {
*penStepsEBB = atoi(arg2); *penStepsEBB = atoi(arg2);
arg3 = SCmd.next(); arg3 = nextCommandArg();
} }
if (arg3 != NULL) 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 // make Objects
Servo penServo; Servo penServo;
SerialCommand SCmd; SerialCommand SCmd;
#ifdef ESP32
SerialCommand g_BLECmd;
SerialCommand g_WifiCmd;
#endif
namespace
{
SerialCommand *g_pActiveParser = &SCmd;
ProtocolTransport g_activeTransport = PROTOCOL_TRANSPORT_SERIAL;
}
// create Buttons // create Buttons
#ifdef prgButton #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 float fPEN_STEP_CORRECTION = 16.0 / penMicrostep; // devide EBB-Coordinates by this factor to get EGGduino-Steps
boolean g_bMotorsEnabled = 0; 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 // Stepper Test
#ifdef TEST #ifdef TEST
// #define dirPinStepper 16 // #define dirPinStepper 16
@@ -77,6 +133,7 @@ void setup()
Log("Starting..."); Log("Starting...");
makeComInterface(); makeComInterface();
initHardware(); initHardware();
startBleInterface();
startWebInterface(); startWebInterface();
} }
@@ -134,8 +191,11 @@ void loop()
} }
#else #else
// moveOneStep(); // moveOneStep();
setActiveProtocolContext(&SCmd, PROTOCOL_TRANSPORT_SERIAL);
SCmd.readSerial(); SCmd.readSerial();
handleBleInterface();
handleWebInterface(); handleWebInterface();
handleWifiProtocolInterface();
#endif #endif
#ifdef penToggleButton #ifdef penToggleButton