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 SerialCommand SCmd;
|
||||
#ifdef ESP32
|
||||
extern SerialCommand g_BLECmd;
|
||||
extern SerialCommand g_WifiCmd;
|
||||
#endif
|
||||
|
||||
extern int g_iPenUpPos;
|
||||
extern int g_iPenDownPos;
|
||||
@@ -82,10 +86,20 @@ extern float fROT_STEP_CORRECTION;
|
||||
extern float fPEN_STEP_CORRECTION;
|
||||
extern boolean g_bMotorsEnabled;
|
||||
|
||||
enum ProtocolTransport {
|
||||
PROTOCOL_TRANSPORT_SERIAL = 0,
|
||||
PROTOCOL_TRANSPORT_BLE = 1,
|
||||
PROTOCOL_TRANSPORT_WIFI = 2,
|
||||
};
|
||||
|
||||
extern ConfigParameter configParameters[];
|
||||
extern const size_t configParameterCount;
|
||||
|
||||
void makeComInterface();
|
||||
void setActiveProtocolContext(SerialCommand *parser, ProtocolTransport transport);
|
||||
char *nextCommandArg();
|
||||
void protocolWrite(const char *message);
|
||||
void protocolWrite(const String &message);
|
||||
void initHardware();
|
||||
void moveOneStep();
|
||||
void moveToDestination();
|
||||
@@ -108,6 +122,29 @@ String buildConfigJson();
|
||||
bool applyConfigJson(const String &payload, String &errorMessage);
|
||||
void startWebInterface();
|
||||
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 char *message);
|
||||
String buildLogsJson(uint32_t sinceSeq);
|
||||
|
||||
@@ -71,8 +71,17 @@ void SerialCommand::setDefaultHandler(void (*function)(const char *)) {
|
||||
* buffer for a prefix command, and calls handlers setup by addCommand() member
|
||||
*/
|
||||
void SerialCommand::readSerial() {
|
||||
while (Serial.available() > 0) {
|
||||
char inChar = Serial.read(); // Read single available character, there may be more waiting
|
||||
readSerial(Serial);
|
||||
}
|
||||
|
||||
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
|
||||
@@ -113,8 +122,7 @@ void SerialCommand::readSerial() {
|
||||
}
|
||||
}
|
||||
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) {
|
||||
buffer[bufPos++] = inChar; // Put character into buffer
|
||||
buffer[bufPos] = '\0'; // Null terminate
|
||||
@@ -125,7 +133,6 @@ void SerialCommand::readSerial() {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Clear the input buffer.
|
||||
|
||||
@@ -49,6 +49,8 @@ class SerialCommand {
|
||||
void setDefaultHandler(void (*function)(const char *)); // A handler to call when no valid command received.
|
||||
|
||||
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.
|
||||
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
|
||||
bblanchon/ArduinoJson@^6.21.5
|
||||
gin66/FastAccelStepper@^0.33.13
|
||||
h2zero/NimBLE-Arduino@^2.3.6
|
||||
links2004/WebSockets@^2.6.1
|
||||
|
||||
[env:uno_macos]
|
||||
platform = platformio/espressif32
|
||||
board = esp32dev
|
||||
framework = arduino
|
||||
monitor_speed = 115200
|
||||
monitor_port = /dev/cu.usb*
|
||||
upload_speed = 115200
|
||||
upload_port = /dev/tty.usbserial-110
|
||||
upload_port = /dev/cu.usb*
|
||||
lib_deps =
|
||||
madhephaestus/ESP32Servo@^3.0.6
|
||||
bblanchon/ArduinoJson@^6.21.5
|
||||
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.onNotFound(handleNotFound);
|
||||
server.begin();
|
||||
startWifiProtocolInterface();
|
||||
}
|
||||
else
|
||||
{
|
||||
|
||||
@@ -8,13 +8,13 @@ void queryPen()
|
||||
state = '1';
|
||||
else
|
||||
state = '0';
|
||||
Serial.print(String(state) + "\r\n");
|
||||
protocolWrite(String(state) + "\r\n");
|
||||
sendAck();
|
||||
}
|
||||
|
||||
void queryButton()
|
||||
{
|
||||
Serial.print(String(g_bPrgButtonState) + "\r\n");
|
||||
protocolWrite(String(g_bPrgButtonState) + "\r\n");
|
||||
sendAck();
|
||||
g_bPrgButtonState = 0;
|
||||
}
|
||||
@@ -22,7 +22,7 @@ void queryButton()
|
||||
void queryLayer()
|
||||
{
|
||||
Log(__FUNCTION__);
|
||||
Serial.print(String(g_uiLayer) + "\r\n");
|
||||
protocolWrite(String(g_uiLayer) + "\r\n");
|
||||
sendAck();
|
||||
}
|
||||
|
||||
@@ -31,7 +31,7 @@ void setLayer()
|
||||
Log(__FUNCTION__);
|
||||
uint32_t value = 0;
|
||||
char *arg1;
|
||||
arg1 = SCmd.next();
|
||||
arg1 = nextCommandArg();
|
||||
if (arg1 != NULL)
|
||||
{
|
||||
value = atoi(arg1);
|
||||
@@ -44,7 +44,7 @@ void setLayer()
|
||||
|
||||
void queryNodeCount()
|
||||
{
|
||||
Serial.print(String(g_uiNodeCount) + "\r\n");
|
||||
protocolWrite(String(g_uiNodeCount) + "\r\n");
|
||||
sendAck();
|
||||
}
|
||||
|
||||
@@ -53,7 +53,7 @@ void setNodeCount()
|
||||
Log(__FUNCTION__);
|
||||
uint32_t value = 0;
|
||||
char *arg1;
|
||||
arg1 = SCmd.next();
|
||||
arg1 = nextCommandArg();
|
||||
if (arg1 != NULL)
|
||||
{
|
||||
value = atoi(arg1);
|
||||
@@ -112,7 +112,7 @@ void setPen()
|
||||
|
||||
moveToDestination();
|
||||
|
||||
arg = SCmd.next();
|
||||
arg = nextCommandArg();
|
||||
if (arg != NULL)
|
||||
{
|
||||
cmd = atoi(arg);
|
||||
@@ -131,7 +131,7 @@ void setPen()
|
||||
}
|
||||
}
|
||||
char *val;
|
||||
val = SCmd.next();
|
||||
val = nextCommandArg();
|
||||
if (val != NULL)
|
||||
{
|
||||
value = atoi(val);
|
||||
@@ -156,7 +156,7 @@ void togglePen()
|
||||
|
||||
moveToDestination();
|
||||
|
||||
arg = SCmd.next();
|
||||
arg = nextCommandArg();
|
||||
if (arg != NULL)
|
||||
value = atoi(arg);
|
||||
else
|
||||
@@ -189,10 +189,10 @@ void enableMotors()
|
||||
int value;
|
||||
char *arg;
|
||||
char *val;
|
||||
arg = SCmd.next();
|
||||
arg = nextCommandArg();
|
||||
if (arg != NULL)
|
||||
cmd = atoi(arg);
|
||||
val = SCmd.next();
|
||||
val = nextCommandArg();
|
||||
if (val != NULL)
|
||||
value = atoi(val);
|
||||
// values parsed
|
||||
@@ -237,11 +237,11 @@ void stepperModeConfigure()
|
||||
int cmd;
|
||||
int value;
|
||||
char *arg;
|
||||
arg = SCmd.next();
|
||||
arg = nextCommandArg();
|
||||
if (arg != NULL)
|
||||
cmd = atoi(arg);
|
||||
char *val;
|
||||
val = SCmd.next();
|
||||
val = nextCommandArg();
|
||||
if (val != NULL)
|
||||
value = atoi(val);
|
||||
if ((arg != NULL) && (val != NULL))
|
||||
@@ -281,8 +281,8 @@ void stepperModeConfigure()
|
||||
void sendVersion()
|
||||
{
|
||||
Log(__FUNCTION__);
|
||||
Serial.print(initSting);
|
||||
Serial.print("\r\n");
|
||||
protocolWrite(initSting);
|
||||
protocolWrite("\r\n");
|
||||
}
|
||||
|
||||
void unrecognized(const char *command)
|
||||
@@ -299,21 +299,29 @@ void ignore()
|
||||
|
||||
void makeComInterface()
|
||||
{
|
||||
SCmd.addCommand("v", sendVersion);
|
||||
SCmd.addCommand("EM", enableMotors);
|
||||
SCmd.addCommand("SC", stepperModeConfigure);
|
||||
SCmd.addCommand("SP", setPen);
|
||||
SCmd.addCommand("SM", stepperMove);
|
||||
SCmd.addCommand("SE", ignore);
|
||||
SCmd.addCommand("TP", togglePen);
|
||||
SCmd.addCommand("PO", ignore); // Engraver command, not implemented, gives fake answer
|
||||
SCmd.addCommand("NI", nodeCountIncrement);
|
||||
SCmd.addCommand("ND", nodeCountDecrement);
|
||||
SCmd.addCommand("SN", setNodeCount);
|
||||
SCmd.addCommand("QN", queryNodeCount);
|
||||
SCmd.addCommand("SL", setLayer);
|
||||
SCmd.addCommand("QL", queryLayer);
|
||||
SCmd.addCommand("QP", queryPen);
|
||||
SCmd.addCommand("QB", queryButton); //"PRG" Button,
|
||||
SCmd.setDefaultHandler(unrecognized); // Handler for command that isn't matched (says "What?")
|
||||
auto registerCommands = [](SerialCommand &cmd) {
|
||||
cmd.addCommand("v", sendVersion);
|
||||
cmd.addCommand("EM", enableMotors);
|
||||
cmd.addCommand("SC", stepperModeConfigure);
|
||||
cmd.addCommand("SP", setPen);
|
||||
cmd.addCommand("SM", stepperMove);
|
||||
cmd.addCommand("SE", ignore);
|
||||
cmd.addCommand("TP", togglePen);
|
||||
cmd.addCommand("PO", ignore); // Engraver command, not implemented, gives fake answer
|
||||
cmd.addCommand("NI", nodeCountIncrement);
|
||||
cmd.addCommand("ND", nodeCountDecrement);
|
||||
cmd.addCommand("SN", setNodeCount);
|
||||
cmd.addCommand("QN", queryNodeCount);
|
||||
cmd.addCommand("SL", setLayer);
|
||||
cmd.addCommand("QL", queryLayer);
|
||||
cmd.addCommand("QP", queryPen);
|
||||
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()
|
||||
{
|
||||
Log(__FUNCTION__);
|
||||
Serial.print("OK\r\n");
|
||||
protocolWrite("OK\r\n");
|
||||
}
|
||||
|
||||
void sendError()
|
||||
{
|
||||
Log(__FUNCTION__);
|
||||
Serial.print("unknown CMD\r\n");
|
||||
protocolWrite("unknown CMD\r\n");
|
||||
}
|
||||
|
||||
void motorsOff()
|
||||
@@ -92,19 +92,19 @@ void toggleMotors()
|
||||
|
||||
bool parseSMArgs(uint16_t *duration, int *penStepsEBB, int *rotStepsEBB)
|
||||
{
|
||||
char *arg1;
|
||||
char *arg2;
|
||||
char *arg3;
|
||||
arg1 = SCmd.next();
|
||||
char *arg1 = NULL;
|
||||
char *arg2 = NULL;
|
||||
char *arg3 = NULL;
|
||||
arg1 = nextCommandArg();
|
||||
if (arg1 != NULL)
|
||||
{
|
||||
*duration = atoi(arg1);
|
||||
arg2 = SCmd.next();
|
||||
arg2 = nextCommandArg();
|
||||
}
|
||||
if (arg2 != NULL)
|
||||
{
|
||||
*penStepsEBB = atoi(arg2);
|
||||
arg3 = SCmd.next();
|
||||
arg3 = nextCommandArg();
|
||||
}
|
||||
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
|
||||
Servo penServo;
|
||||
SerialCommand SCmd;
|
||||
#ifdef ESP32
|
||||
SerialCommand g_BLECmd;
|
||||
SerialCommand g_WifiCmd;
|
||||
#endif
|
||||
|
||||
namespace
|
||||
{
|
||||
SerialCommand *g_pActiveParser = &SCmd;
|
||||
ProtocolTransport g_activeTransport = PROTOCOL_TRANSPORT_SERIAL;
|
||||
}
|
||||
|
||||
// create Buttons
|
||||
#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
|
||||
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
|
||||
#ifdef TEST
|
||||
// #define dirPinStepper 16
|
||||
@@ -77,6 +133,7 @@ void setup()
|
||||
Log("Starting...");
|
||||
makeComInterface();
|
||||
initHardware();
|
||||
startBleInterface();
|
||||
startWebInterface();
|
||||
}
|
||||
|
||||
@@ -134,8 +191,11 @@ void loop()
|
||||
}
|
||||
#else
|
||||
// moveOneStep();
|
||||
setActiveProtocolContext(&SCmd, PROTOCOL_TRANSPORT_SERIAL);
|
||||
SCmd.readSerial();
|
||||
handleBleInterface();
|
||||
handleWebInterface();
|
||||
handleWifiProtocolInterface();
|
||||
#endif
|
||||
|
||||
#ifdef penToggleButton
|
||||
|
||||
Reference in New Issue
Block a user