/** * WijiBoard – BLE Singleton * web/js/ble.js * * Provides a singleton BLE object with: * BLE.connect() → connects to device, gets characteristics * BLE.disconnect() → disconnects * BLE.write(cmd) → writes ASCII command (write-without-response) * BLE.isConnected() → boolean * BLE.on(event, cb) → subscribe to 'connected' | 'disconnected' | 'status' * BLE.off(event, cb) → unsubscribe * * Command characteristic (WRITE_NR): * S1+ → step motor 1 clockwise n steps * S1- → step motor 1 counter-clockwise n steps * S2+ → step motor 2 clockwise n steps * S2- → step motor 2 counter-clockwise n steps * SPD: → set max speed (steps/sec) * ACC: → set acceleration (steps/sec²) * HOME → zero both steppers * POS → request position report * * Status characteristic (NOTIFY): * P:, → current step positions for motor 1 & 2 */ // ── UUIDs – must match src/main.cpp exactly ────────────────────── const SERVICE_UUID = '18f3b235-9831-4c75-8ec0-210469b820a0'; const CMD_UUID = 'cd083b06-4447-4cf3-a7c3-322ecf802ce4'; const STATUS_UUID = '82e38c5b-d3ab-41d1-861c-b84dc6bb1e03'; const DEVICE_NAME = 'WijiBoard'; const BLE = (() => { // ── Internal state ────────────────────────────────────────── let device = null; let server = null; let cmdChar = null; let statusChar = null; const encoder = new TextEncoder(); // ── Tiny event emitter ────────────────────────────────────── const listeners = {}; function emit(event, ...args) { (listeners[event] || []).forEach(cb => { try { cb(...args); } catch (e) { console.error('[BLE] listener error', e); } }); } function on(event, cb) { if (!listeners[event]) listeners[event] = []; listeners[event].push(cb); } function off(event, cb) { if (!listeners[event]) return; listeners[event] = listeners[event].filter(fn => fn !== cb); } // ── Disconnect handler ────────────────────────────────────── function onDisconnected() { console.log('[BLE] Device disconnected'); device = null; server = null; cmdChar = null; statusChar = null; emit('disconnected'); } // ── Status notification handler ───────────────────────────── function onStatusNotification(event) { const value = new TextDecoder().decode(event.target.value); emit('status', value.trim()); } // ── connect() ─────────────────────────────────────────────── async function connect() { console.log('[BLE] connect() called'); if (!navigator.bluetooth) { throw new Error('Web Bluetooth API not available. Use Chrome/Edge over HTTPS or localhost.'); } emit('connecting'); console.log('[BLE] Requesting device...'); device = await navigator.bluetooth.requestDevice({ filters: [{ name: DEVICE_NAME }], optionalServices: [SERVICE_UUID], }); console.log('[BLE] Device selected:', device.name); device.addEventListener('gattserverdisconnected', onDisconnected); console.log('[BLE] Connecting to GATT server...'); server = await device.gatt.connect(); console.log('[BLE] GATT server connected'); console.log('[BLE] Getting primary service:', SERVICE_UUID); const service = await server.getPrimaryService(SERVICE_UUID); console.log('[BLE] Primary service found'); console.log('[BLE] Getting command characteristic:', CMD_UUID); cmdChar = await service.getCharacteristic(CMD_UUID); console.log('[BLE] Command characteristic found'); // Status notifications (optional – firmware may not have this char yet) try { console.log('[BLE] Getting status characteristic:', STATUS_UUID); statusChar = await service.getCharacteristic(STATUS_UUID); console.log('[BLE] Starting notifications...'); await statusChar.startNotifications(); statusChar.addEventListener('characteristicvaluechanged', onStatusNotification); console.log('[BLE] Notifications started'); } catch (err) { console.warn('[BLE] Status characteristic not available – notifications disabled', err); } console.log('[BLE] Connection fully established, emitting connected'); emit('connected', device.name); } // ── disconnect() ──────────────────────────────────────────── async function disconnect() { if (device?.gatt?.connected) { device.gatt.disconnect(); } } // ── write() ───────────────────────────────────────────────── async function write(cmd) { if (!cmdChar) throw new Error('BLE not connected'); await cmdChar.writeValueWithoutResponse(encoder.encode(cmd)); emit('sent', cmd); } // ── isConnected() ─────────────────────────────────────────── function isConnected() { return !!(device?.gatt?.connected && cmdChar); } // ── getDeviceName() ───────────────────────────────────────── function getDeviceName() { return device?.name ?? null; } return { on, off, connect, disconnect, write, isConnected, getDeviceName, SERVICE_UUID, CMD_UUID, STATUS_UUID, DEVICE_NAME, }; })(); export default BLE;