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,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);
}