import React, { createContext, useContext, useState, useEffect } from 'react'; import type { BLEDevice, CharacteristicNotification, DeviceConnectionState, UserRole, } from '../types'; import bleMiddleware from '../services/BLEMiddleware'; /** * BLE Context State */ interface BLEContextState { // Connection state bluetoothEnabled: boolean; isScanning: boolean; discoveredDevices: BLEDevice[]; connectedDevices: Map; // Legacy single device support (for backward compatibility) connectedDeviceId: string | null; isInfusionRunning: boolean; ff01Notification: CharacteristicNotification | null; ff21Notification: CharacteristicNotification | null; ff31Notification: CharacteristicNotification | null; ff41Notification: CharacteristicNotification | null; ff02Notification: CharacteristicNotification | null; // Actions startScan: () => Promise; stopScan: () => Promise; connectToDevice: (device: BLEDevice) => Promise; disconnectDevice: (deviceId: string) => Promise; requestPermissions: () => Promise; // Multi-device queries getDeviceState: (deviceId: string) => DeviceConnectionState | undefined; isDeviceConnected: (deviceId: string) => boolean; getConnectedDeviceIds: () => string[]; // Infusion actions startInfusion: (deviceId: string) => Promise; stopInfusion: (deviceId: string) => Promise; setInfusionLevel: (deviceId: string, level: number) => Promise; // Characteristic operations readCharacteristic: ( deviceId: string, serviceUuid: string, characteristicUuid: string ) => Promise; writeCharacteristic: ( deviceId: string, serviceUuid: string, characteristicUuid: string, data: string, options?: { withResponse?: boolean } ) => Promise; // User role management setUserRole: (role: UserRole) => void; getUserRole: () => UserRole; } const BLEContext = createContext(undefined); /** * BLE Provider Component * Manages all BLE state and operations */ export const BLEProvider: React.FC<{ children: React.ReactNode }> = ({ children, }) => { const [bluetoothEnabled, setBluetoothEnabled] = useState(false); const [isScanning, setIsScanning] = useState(false); const [discoveredDevices, setDiscoveredDevices] = useState([]); const [connectedDevices, setConnectedDevices] = useState< Map >(new Map()); // Legacy single device support (tracks the first/primary connected device) const [connectedDeviceId, setConnectedDeviceId] = useState( null ); // Legacy notification states (for the primary device) const [ff01Notification, setFF01Notification] = useState(null); const [ff21Notification, setFF21Notification] = useState(null); const [ff31Notification, setFF31Notification] = useState(null); const [ff41Notification, setFF41Notification] = useState(null); const [ff02Notification, setFF02Notification] = useState(null); // Legacy infusion state (for the primary device) const [isInfusionRunning, setIsInfusionRunning] = useState(false); useEffect(() => { const initialize = async () => { // Wait for BLE middleware to initialize first await bleMiddleware.waitForInitialization(); // Then setup BLE and event listeners await setupBLE(); setupEventListeners(); }; initialize(); return () => { bleMiddleware.removeAllListeners(); }; }, []); const setupBLE = async () => { try { await requestPermissions(); const enabled = await bleMiddleware.isBluetoothEnabled(); setBluetoothEnabled(enabled); } catch (error) { console.error('Failed to setup BLE:', error); } }; const requestPermissions = async () => { try { await bleMiddleware.requestPermissions(); } catch (error) { console.error('Failed to request permissions:', error); } }; const setupEventListeners = () => { // Scan result event bleMiddleware.on('scanResult', ({ device }: any) => { setDiscoveredDevices((prev) => { const exists = prev.find((d) => d.id === device.id); if (exists) { return prev.map((d) => (d.id === device.id ? device : d)); } return [...prev, device]; }); }); // Connected event bleMiddleware.on('connected', ({ deviceId }: any) => { setConnectedDevices((prev) => { const newMap = new Map(prev); newMap.set(deviceId, { deviceId, isConnected: true, isInfusionRunning: false, infusionLevel: null, notifications: { ff01: null, ff21: null, ff31: null, ff41: null, ff02: null, }, }); return newMap; }); // Legacy: Set first connected device as primary setConnectedDeviceId((prev) => prev || deviceId); console.log('✅ Device connected:', deviceId); }); // Disconnected event bleMiddleware.on('disconnected', ({ deviceId }: any) => { setConnectedDevices((prev) => { const newMap = new Map(prev); newMap.delete(deviceId); return newMap; }); // Legacy: Update primary device setConnectedDeviceId((prev) => { if (prev === deviceId) { // If primary device disconnected, set new primary or null const remaining = Array.from(connectedDevices.keys()).filter( (id) => id !== deviceId ); const newPrimary = remaining[0] || null; // Update legacy infusion state if (!newPrimary) { setIsInfusionRunning(false); } return newPrimary; } return prev; }); console.log('❌ Device disconnected:', deviceId); }); // Scan failed event bleMiddleware.on('BleManagerScanFailed', ({ error }: any) => { console.error('BLE Scan Failed:', error); setIsScanning(false); }); // Bluetooth state changed event bleMiddleware.on('bluetoothStateChanged', ({ state }: any) => { setBluetoothEnabled(state === 'poweredOn'); }); // Characteristic changed event bleMiddleware.on('characteristicChanged', (event: any) => { console.log('📡 Characteristic Changed Event:', event); const { characteristicUuid, deviceId } = event; const uuidUpper = characteristicUuid?.toUpperCase(); // Update device-specific notifications setConnectedDevices((prev) => { const newMap = new Map(prev); const deviceState = newMap.get(deviceId); if (deviceState) { const updatedState = { ...deviceState }; if (uuidUpper === '0000FF01-0000-1000-8000-00805F9B34FB') { updatedState.notifications.ff01 = event; } else if (uuidUpper === '0000FF21-0000-1000-8000-00805F9B34FB') { updatedState.notifications.ff21 = event; console.log('✅ FF21 notification stored in device state'); } else if (uuidUpper === '0000FF31-0000-1000-8000-00805F9B34FB') { updatedState.notifications.ff31 = event; } else if (uuidUpper === '0000FF41-0000-1000-8000-00805F9B34FB') { updatedState.notifications.ff41 = event; } else if (uuidUpper === '0000FF02-0000-1000-8000-00805F9B34FB') { updatedState.notifications.ff02 = event; } newMap.set(deviceId, updatedState); } return newMap; }); // Legacy: Update global notifications using functional update to avoid stale closure setConnectedDeviceId((currentPrimaryDeviceId) => { console.log(`🔍 Comparing deviceId: ${deviceId} with primary: ${currentPrimaryDeviceId}`); // Check if this event is for the primary device if (deviceId === currentPrimaryDeviceId) { console.log('✅ This is the primary device, updating global notifications'); if (uuidUpper === '0000FF01-0000-1000-8000-00805F9B34FB') { setFF01Notification(event); } else if (uuidUpper === '0000FF21-0000-1000-8000-00805F9B34FB') { setFF21Notification(event); console.log('✅ FF21 global notification updated!'); } else if (uuidUpper === '0000FF31-0000-1000-8000-00805F9B34FB') { setFF31Notification(event); } else if (uuidUpper === '0000FF41-0000-1000-8000-00805F9B34FB') { setFF41Notification(event); } else if (uuidUpper === '0000FF02-0000-1000-8000-00805F9B34FB') { setFF02Notification(event); } else { console.log('⚠️ Unhandled characteristic:', characteristicUuid); } } else { console.log(`⚠️ Not primary device - skipping global update`); } return currentPrimaryDeviceId; // Return unchanged }); }); }; const startScan = async () => { console.log('Starting device scan...'); try { setDiscoveredDevices([]); setIsScanning(true); await bleMiddleware.startScan({ timeout: 10000, allowDuplicates: false }); } catch (error) { console.error('Failed to start scan:', error); setIsScanning(false); } }; const stopScan = async () => { try { await bleMiddleware.stopScan(); setIsScanning(false); } catch (error) { console.error('Failed to stop scan:', error); } }; const connectToDevice = async (device: BLEDevice) => { console.log('Connecting to device:', device); try { await bleMiddleware.connect(device.id); console.log('Connected successfully'); } catch (error) { console.error('Failed to connect:', error); } }; const disconnectDevice = async (deviceId: string) => { try { await bleMiddleware.disconnect(deviceId); // State will be updated in the 'disconnected' event handler console.log('Disconnected from device:', deviceId); } catch (error) { console.error('Failed to disconnect:', error); } }; // Multi-device query helpers const getDeviceState = ( deviceId: string ): DeviceConnectionState | undefined => { return connectedDevices.get(deviceId); }; const isDeviceConnected = (deviceId: string): boolean => { return connectedDevices.has(deviceId); }; const getConnectedDeviceIds = (): string[] => { return Array.from(connectedDevices.keys()); }; const startInfusion = async (deviceId: string) => { try { await bleMiddleware.startInfusion(deviceId); // Update device-specific state setConnectedDevices((prev) => { const newMap = new Map(prev); const deviceState = newMap.get(deviceId); if (deviceState) { newMap.set(deviceId, { ...deviceState, isInfusionRunning: true, }); } return newMap; }); // Legacy: Update global state if this is the primary device if (deviceId === connectedDeviceId) { setIsInfusionRunning(true); } console.log('✅ Infusion started successfully for device:', deviceId); } catch (error) { console.error('Failed to start infusion:', error); throw error; } }; const stopInfusion = async (deviceId: string) => { try { await bleMiddleware.stopInfusion(deviceId); // Update device-specific state setConnectedDevices((prev) => { const newMap = new Map(prev); const deviceState = newMap.get(deviceId); if (deviceState) { newMap.set(deviceId, { ...deviceState, isInfusionRunning: false, }); } return newMap; }); // Legacy: Update global state if this is the primary device if (deviceId === connectedDeviceId) { setIsInfusionRunning(false); } console.log('✅ Infusion stopped successfully for device:', deviceId); } catch (error) { console.error('Failed to stop infusion:', error); throw error; } }; const setInfusionLevel = async (deviceId: string, level: number) => { try { await bleMiddleware.setInfusionLevel(deviceId, level); // Update device-specific state setConnectedDevices((prev) => { const newMap = new Map(prev); const deviceState = newMap.get(deviceId); if (deviceState) { newMap.set(deviceId, { ...deviceState, infusionLevel: level, }); } return newMap; }); console.log( `✅ Infusion level set to ${level} successfully for device:`, deviceId ); } catch (error) { console.error('Failed to set infusion level:', error); throw error; } }; const readCharacteristic = async ( deviceId: string, serviceUuid: string, characteristicUuid: string ) => { try { return await bleMiddleware.readCharacteristic( deviceId, serviceUuid, characteristicUuid ); } catch (error) { console.error('Failed to read characteristic:', error); throw error; } }; const writeCharacteristic = async ( deviceId: string, serviceUuid: string, characteristicUuid: string, data: string, options?: { withResponse?: boolean } ) => { try { await bleMiddleware.writeCharacteristic( deviceId, serviceUuid, characteristicUuid, data, options ); } catch (error) { console.error('Failed to write characteristic:', error); throw error; } }; const setUserRole = (role: UserRole) => { bleMiddleware.setUserRole(role); }; const getUserRole = (): UserRole => { return bleMiddleware.getUserRole(); }; const value: BLEContextState = { bluetoothEnabled, isScanning, discoveredDevices, connectedDevices, connectedDeviceId, isInfusionRunning, ff01Notification, ff21Notification, ff31Notification, ff41Notification, ff02Notification, startScan, stopScan, connectToDevice, disconnectDevice, requestPermissions, getDeviceState, isDeviceConnected, getConnectedDeviceIds, startInfusion, stopInfusion, setInfusionLevel, readCharacteristic, writeCharacteristic, setUserRole, getUserRole, }; return {children}; }; /** * Hook to access BLE context */ export const useBLEContext = (): BLEContextState => { const context = useContext(BLEContext); if (!context) { throw new Error('useBLEContext must be used within a BLEProvider'); } return context; };