/** * BLE Service — Web Bluetooth connection to Bosch Smart System * * Connects via the correct Bosch eaa2 service UUID, * subscribes to notifications on char 0011, * and passes decoded data to the app. */ const BLEService = (() => { let device = null; let server = null; let characteristic = null; let writeCharacteristic = null; let onDataCallback = null; let onStatusCallback = null; /** * Check if Web Bluetooth is available */ function isAvailable() { return !!(navigator.bluetooth && navigator.bluetooth.requestDevice); } /** * Connect to a Bosch Smart System eBike */ async function connect() { if (!isAvailable()) { throw new Error('Web Bluetooth is not supported on this device'); } updateStatus('scanning'); try { // Request device — use namePrefix filters + optionalServices // (some Bosch bikes don't advertise the service UUID directly) device = await navigator.bluetooth.requestDevice({ filters: [ { namePrefix: 'smart system' }, { namePrefix: 'Bosch' }, { namePrefix: 'BRC' } ], optionalServices: [BoschProtocol.SERVICE_UUID] }); device.addEventListener('gattserverdisconnected', onDisconnected); updateStatus('connecting'); server = await device.gatt.connect(); updateStatus('discovering'); const service = await server.getPrimaryService(BoschProtocol.SERVICE_UUID); // Get notify characteristic (0011) characteristic = await service.getCharacteristic(BoschProtocol.CHAR_UUID); // Try to get write characteristic (0012) for potential init commands try { writeCharacteristic = await service.getCharacteristic(BoschProtocol.WRITE_UUID); console.log('Write characteristic (0012) available'); } catch (e) { console.log('Write characteristic not available:', e.message); } // Subscribe to notifications IMMEDIATELY await characteristic.startNotifications(); characteristic.addEventListener('characteristicvaluechanged', onCharacteristicChanged); updateStatus('connected'); console.log('BLE connected to:', device.name); return device.name; } catch (error) { updateStatus('disconnected'); if (error.name === 'NotFoundError') { throw new Error('No bike found. Make sure the Bosch system is powered on.'); } throw error; } } /** * Disconnect from the bike */ function disconnect() { if (characteristic) { characteristic.removeEventListener('characteristicvaluechanged', onCharacteristicChanged); characteristic = null; } writeCharacteristic = null; if (device && device.gatt.connected) { device.gatt.disconnect(); } device = null; server = null; updateStatus('disconnected'); } /** * Handle incoming BLE notifications */ function onCharacteristicChanged(event) { const dataView = event.target.value; // Log raw bytes for debugging const raw = []; for (let i = 0; i < dataView.byteLength; i++) { raw.push(dataView.getUint8(i).toString(16).toUpperCase().padStart(2, '0')); } console.log('BLE notification:', raw.join('-'), `(${dataView.byteLength}B)`); const parsed = BoschProtocol.parseNotification(dataView); if (onDataCallback) { onDataCallback(parsed); } } /** * Handle unexpected disconnection */ function onDisconnected() { console.warn('BLE device disconnected'); characteristic = null; writeCharacteristic = null; server = null; updateStatus('disconnected'); } /** * Update connection status */ function updateStatus(status) { if (onStatusCallback) { onStatusCallback(status); } } /** * Set callback for incoming data */ function onData(callback) { onDataCallback = callback; } /** * Set callback for status changes */ function onStatus(callback) { onStatusCallback = callback; } /** * Check if currently connected */ function isConnected() { return device && device.gatt && device.gatt.connected; } return { isAvailable, connect, disconnect, onData, onStatus, isConnected }; })();