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:
46
docs/eggbot-ble-serial-protocol.md
Normal file
46
docs/eggbot-ble-serial-protocol.md
Normal 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.
|
||||||
53
docs/eggbot-serial-protocol.md
Normal file
53
docs/eggbot-serial-protocol.md
Normal 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.
|
||||||
45
docs/eggbot-wifi-serial-protocol.md
Normal file
45
docs/eggbot-wifi-serial-protocol.md
Normal 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.
|
||||||
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);
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
|||||||
@@ -71,58 +71,65 @@ 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
|
||||||
|
Serial.print(inChar); // Echo back to serial stream
|
||||||
|
#endif
|
||||||
|
|
||||||
|
if (inChar == term) { // Check for the terminator (default '\r') meaning end of command
|
||||||
#ifdef SERIALCOMMAND_DEBUG
|
#ifdef SERIALCOMMAND_DEBUG
|
||||||
Serial.print(inChar); // Echo back to serial stream
|
Serial.print("Received: ");
|
||||||
|
Serial.println(buffer);
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
if (inChar == term) { // Check for the terminator (default '\r') meaning end of command
|
char *command = strtok_r(buffer, delim, &last); // Search for command at start of buffer
|
||||||
#ifdef SERIALCOMMAND_DEBUG
|
if (command != NULL) {
|
||||||
Serial.print("Received: ");
|
boolean matched = false;
|
||||||
Serial.println(buffer);
|
for (int i = 0; i < commandCount; i++) {
|
||||||
#endif
|
#ifdef SERIALCOMMAND_DEBUG
|
||||||
|
Serial.print("Comparing [");
|
||||||
|
Serial.print(command);
|
||||||
|
Serial.print("] to [");
|
||||||
|
Serial.print(commandList[i].command);
|
||||||
|
Serial.println("]");
|
||||||
|
#endif
|
||||||
|
|
||||||
char *command = strtok_r(buffer, delim, &last); // Search for command at start of buffer
|
// Compare the found command against the list of known commands for a match
|
||||||
if (command != NULL) {
|
if (strncmp(command, commandList[i].command, SERIALCOMMAND_MAXCOMMANDLENGTH) == 0) {
|
||||||
boolean matched = false;
|
|
||||||
for (int i = 0; i < commandCount; i++) {
|
|
||||||
#ifdef SERIALCOMMAND_DEBUG
|
#ifdef SERIALCOMMAND_DEBUG
|
||||||
Serial.print("Comparing [");
|
Serial.print("Matched Command: ");
|
||||||
Serial.print(command);
|
Serial.println(command);
|
||||||
Serial.print("] to [");
|
|
||||||
Serial.print(commandList[i].command);
|
|
||||||
Serial.println("]");
|
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
// Compare the found command against the list of known commands for a match
|
// Execute the stored handler function for the command
|
||||||
if (strncmp(command, commandList[i].command, SERIALCOMMAND_MAXCOMMANDLENGTH) == 0) {
|
(*commandList[i].function)();
|
||||||
#ifdef SERIALCOMMAND_DEBUG
|
matched = true;
|
||||||
Serial.print("Matched Command: ");
|
break;
|
||||||
Serial.println(command);
|
|
||||||
#endif
|
|
||||||
|
|
||||||
// Execute the stored handler function for the command
|
|
||||||
(*commandList[i].function)();
|
|
||||||
matched = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!matched && (defaultHandler != NULL)) {
|
|
||||||
(*defaultHandler)(command);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
clearBuffer();
|
if (!matched && (defaultHandler != NULL)) {
|
||||||
|
(*defaultHandler)(command);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
else if (isprint(inChar)) { // Only printable characters into the buffer
|
clearBuffer();
|
||||||
if (bufPos < SERIALCOMMAND_BUFFER) {
|
} else if (isprint(inChar)) { // Only printable characters into the buffer
|
||||||
buffer[bufPos++] = inChar; // Put character into buffer
|
if (bufPos < SERIALCOMMAND_BUFFER) {
|
||||||
buffer[bufPos] = '\0'; // Null terminate
|
buffer[bufPos++] = inChar; // Put character into buffer
|
||||||
} else {
|
buffer[bufPos] = '\0'; // Null terminate
|
||||||
#ifdef SERIALCOMMAND_DEBUG
|
} else {
|
||||||
Serial.println("Line buffer is full - increase SERIALCOMMAND_BUFFER");
|
#ifdef SERIALCOMMAND_DEBUG
|
||||||
#endif
|
Serial.println("Line buffer is full - increase SERIALCOMMAND_BUFFER");
|
||||||
}
|
#endif
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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).
|
||||||
|
|
||||||
|
|||||||
@@ -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
244
src/BLE_Interface.cpp
Normal 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
|
||||||
@@ -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
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
156
src/WiFi_Protocol.cpp
Normal 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
|
||||||
60
src/main.cpp
60
src/main.cpp
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user