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