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:
185
docs/examples/eggbot-ble-client.mjs
Normal file
185
docs/examples/eggbot-ble-client.mjs
Normal 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);
|
||||
}
|
||||
183
docs/examples/eggbot-wifi-client.mjs
Normal file
183
docs/examples/eggbot-wifi-client.mjs
Normal 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);
|
||||
}
|
||||
Reference in New Issue
Block a user