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