react-native-bluetooth-obd-manager
v1.0.4
Published
A React Native hook library to manage Bluetooth Low Energy connections and communication with ELM327 OBD-II adapters.
Maintainers
Readme
React Native Bluetooth OBD Manager
A React Native hook library (useBluetooth) designed to simplify Bluetooth Low Energy (BLE) communication with ELM327-compatible OBD-II adapters. It handles device scanning, smart connection (auto-detecting common ELM327 service/characteristic patterns), command execution (AT commands, OBD PIDs), streaming state, and connection management.
Note: This library provides the communication layer. Parsing the responses from OBD-II commands (e.g., converting hex strings from PIDs like 010C into RPM values) is not included and must be implemented by your application according to OBD-II standards (SAE J1979).
Features
- Safe Initialization: Provides
isInitializingstate to safely handle the async initialization of Bluetooth functionality. - Simple Hook Interface: Manage all BLE OBD interactions via the
useBluetoothhook. - State Management: Provides reactive state for Bluetooth power status, permissions, scanning activity, connection status, command status, streaming status, errors, etc.
- Permission Handling: Includes functions to check (
checkPermissions) and request (requestBluetoothPermissions) necessary permissions (Location, Bluetooth Scan/Connect) directly via the hook. - Bluetooth Enabling: Provides a function (
promptEnableBluetooth) to prompt the user to enable Bluetooth (Android only). - Device Scanning: Scan for nearby BLE peripherals (
scanDevices) with status indication (isScanning) and results (discoveredDevices). Includes a basic heuristic flag (isLikelyOBD) on discovered devices based on name. - Smart Connection: Automatically attempts to connect (
connectToDevice) using a list of known Service/Characteristic UUIDs common among ELM327 clones, increasing compatibility. - Command Execution: Send AT/OBD commands with
sendCommand(for string responses) orsendCommandRaw(forUint8Arrayresponses). Handles:- Automatic write type selection (
WritevsWriteWithoutResponse). - Required command termination (
\r). - Waiting for ELM327 prompt (
>) to signal response completion. - Configurable command timeouts.
- Error handling for writes, timeouts, and disconnects.
- Automatic write type selection (
- Raw Byte Commands: Option to send commands and receive the complete raw
Uint8Arrayresponse (sendCommandRaw) after the>prompt is detected. - Chunked Response: Advanced option to receive responses with preserved packet boundaries (
sendCommandRawChunked) useful for protocols requiring exact chunk boundaries or line breaks. - Connection Management: Graceful
connectToDeviceanddisconnectfunctions. - Real-time Disconnect Detection: Automatically updates connection state if the device disconnects unexpectedly.
- Streaming Helper State: Includes state (
isStreaming) and control (setStreaming) managed by the application, plus an automatic inactivity timeout (~4s) managed by the library to detect stalled polling loops. - TypeScript Support: Written entirely in TypeScript with strict typings.
Installation
Install Library:
npm install react-native-bluetooth-obd-manager # or yarn add react-native-bluetooth-obd-managerInstall Peer Dependencies: This library requires
react-native-ble-managerandreact-native-permissions. You must install and configure them natively according to their documentation. This library will not work without correct native setup of these dependencies.npm install react-native-ble-manager react-native-permissions # or yarn add react-native-ble-manager react-native-permissionsreact-native-ble-managerSetup: Carefully follow the official react-native-ble-manager installation guide. This includes:- iOS:
pod install, addingNSBluetoothAlwaysUsageDescription(orNSBluetoothPeripheralUsageDescription) toInfo.plist. - Android: Adding required permissions (
BLUETOOTH,BLUETOOTH_ADMIN(maxSdk 30),ACCESS_FINE_LOCATION,BLUETOOTH_SCAN,BLUETOOTH_CONNECT) toAndroidManifest.xml. Ensure linking is correct.
- iOS:
react-native-permissionsSetup: Follow the official react-native-permissions setup guide. This includes:- iOS:
pod install, adding relevantNS...UsageDescriptionkeys toInfo.plist(especiallyNSLocationWhenInUseUsageDescriptionfor scanning). - Android: Usually no extra steps needed beyond ensuring the permissions requested exist in
AndroidManifest.xml.
- iOS:
Required Permissions (Manifest/Info.plist Examples): Ensure these (or equivalent) are correctly added as per the peer dependency setup guides.
Info.plist(iOS):<key>NSBluetoothAlwaysUsageDescription</key> <!-- Or NSBluetoothPeripheralUsageDescription --> <string>Allow $(PRODUCT_NAME) to connect to your OBD adapter.</string> <key>NSLocationWhenInUseUsageDescription</key> <string>Allow $(PRODUCT_NAME) to find nearby Bluetooth OBD adapters.</string>AndroidManifest.xml(Android):<!-- Basic Bluetooth (Android 11 and below) --> <uses-permission android:name="android.permission.BLUETOOTH" android:maxSdkVersion="30" /> <uses-permission android:name="android.permission.BLUETOOTH_ADMIN" android:maxSdkVersion="30" /> <!-- Location (Needed for BLE scanning, required always before Android 12) --> <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" /> <!-- Add android:usesPermissionFlags="neverForLocation" to BLUETOOTH_SCAN if you don't derive location from scan results and target Android 12+ --> <!-- Android 12+ (API 31+) Specific Permissions --> <uses-permission android:name="android.permission.BLUETOOTH_SCAN" /> <uses-permission android:name="android.permission.BLUETOOTH_CONNECT" /> <!-- Declare Bluetooth features --> <uses-feature android:name="android.hardware.bluetooth_le" android:required="true"/>
Usage
Wrap your app with
BluetoothProviderand handle initialization:// App.tsx or similar entry point import React from 'react'; import { BluetoothProvider } from 'react-native-bluetooth-obd-manager'; import YourMainAppComponent from './YourMainAppComponent'; const App = () => { return ( <BluetoothProvider> <YourMainAppComponent /> </BluetoothProvider> ); }; export default App; // YourMainAppComponent.tsx import React from 'react'; import { View, Text, ActivityIndicator } from 'react-native'; import { useBluetooth } from 'react-native-bluetooth-obd-manager'; const YourMainAppComponent = () => { const { isInitializing } = useBluetooth(); // Important: Wait for BluetoothProvider to initialize if (isInitializing) { return ( <View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}> <ActivityIndicator size="large" /> <Text>Initializing Bluetooth...</Text> </View> ); } // Render your main component UI only after initialization is complete return ( <View> {/* Your component content */} </View> ); }; export default YourMainAppComponent;⚠️ Important: Always check
isInitializingfrom theuseBluetoothhook before using any Bluetooth functionality. TheBluetoothProviderperforms asynchronous initialization of the native Bluetooth module, and your components should wait for this to complete.Use the
useBluetoothhook:// YourMainAppComponent.tsx import React, { useState, useEffect, useCallback, useRef } from 'react'; import { View, Text, Button, FlatList, TouchableOpacity, ActivityIndicator, Alert, ScrollView, StyleSheet, Switch } from 'react-native'; import { useBluetooth, type PeripheralWithPrediction, type BleError // Optional: for more specific error type checking } from 'react-native-bluetooth-obd-manager'; const YourMainAppComponent = () => { // Get state and functions from the hook const { isBluetoothOn, hasPermissions, isInitializing, // <-- Destructure the initializing flag isScanning, discoveredDevices, connectedDevice, isConnecting, isDisconnecting, error, isAwaitingResponse, isStreaming, lastSuccessfulCommandTimestamp, checkPermissions, requestBluetoothPermissions, promptEnableBluetooth, scanDevices, connectToDevice, disconnect, sendCommand, sendCommandRawChunked, sendCommandRaw, setStreaming, } = useBluetooth(); const [lastResponse, setLastResponse] = useState<string | null>(null); const [lastRawResponse, setLastRawResponse] = useState<Uint8Array | null>(null); const [isLoadingCommand, setIsLoadingCommand] = useState(false); const [appIsStreaming, setAppIsStreaming] = useState(false); // App's view of streaming // Ref for streaming interval const streamIntervalRef = useRef<NodeJS.Timeout | null>(null); // --- Effects --- useEffect(() => { // Check permissions status on mount checkPermissions(); }, [checkPermissions]); // Effect to synchronize app's streaming state with library's state // (Handles case where library stops streaming due to inactivity/disconnect) useEffect(() => { if (!isStreaming && appIsStreaming) { console.log("Library stopped streaming (inactivity/disconnect), stopping app interval."); if (streamIntervalRef.current) { clearInterval(streamIntervalRef.current); streamIntervalRef.current = null; } setAppIsStreaming(false); // Update app state } }, [isStreaming, appIsStreaming]); // Effect to cleanup streaming on unmount useEffect(() => { return () => { if (streamIntervalRef.current) { clearInterval(streamIntervalRef.current); } }; }, []); // --- Handlers --- const handleRequestPermissions = useCallback(async () => { const granted = await requestBluetoothPermissions(); if (!granted) { Alert.alert("Permissions Required", "Please grant permissions via Settings."); } }, [requestBluetoothPermissions]); const handleEnableBluetooth = useCallback(async () => { try { await promptEnableBluetooth(); } catch (err) { Alert.alert("Enable Bluetooth", "Please enable Bluetooth in device settings."); } }, [promptEnableBluetooth]); const handleScan = useCallback(async () => { if (isScanning) return; try { await scanDevices(5000); } // Scan for 5 seconds catch (err: any) { Alert.alert('Scan Error', err.message); } }, [isScanning, scanDevices]); const handleConnect = useCallback(async (device: PeripheralWithPrediction) => { if (isConnecting || connectedDevice) return; try { await connectToDevice(device.id); Alert.alert('Connected!', `Connected to ${device.name || device.id}`); } catch (err: any) { Alert.alert('Connection Error', err.message); } }, [isConnecting, connectedDevice, connectToDevice]); const handleDisconnect = useCallback(async () => { await stopDataStream(); // Use await here if (connectedDevice) { try { await disconnect(); Alert.alert('Disconnected'); setLastResponse(null); setLastRawResponse(null); } catch (err: any) { Alert.alert('Disconnect Error', err.message); } } }, [connectedDevice, disconnect, stopDataStream]); const handleSendCommand = useCallback(async (cmd: string) => { if (!connectedDevice) { Alert.alert("Not Connected"); return; } setIsLoadingCommand(true); setLastResponse(null); setLastRawResponse(null); try { const response = await sendCommand(cmd); setLastResponse(response); // TODO: Parse 'response' string here based on 'cmd' } catch (err: any) { Alert.alert(`Command Error (${cmd})`, err.message); } finally { setIsLoadingCommand(false); } }, [connectedDevice, sendCommand]); const handleSendCommandRaw = useCallback(async (cmd: string) => { if (!connectedDevice) { Alert.alert("Not Connected"); return; } setIsLoadingCommand(true); setLastResponse(null); setLastRawResponse(null); try { const response = await sendCommandRaw(cmd); setLastRawResponse(response); console.log(`Raw Response Bytes: [${response.join(', ')}]`); // TODO: Parse raw 'response' bytes here } catch (err: any) { Alert.alert(`Raw Command Error (${cmd})`, err.message); } finally { setIsLoadingCommand(false); } }, [connectedDevice, sendCommandRaw]); // --- Streaming Logic --- const fetchDataForStream = useCallback(async () => { // Check app's view of streaming intention AND library's state if (!appIsStreaming || !isStreaming || !connectedDevice) { console.log("fetchDataForStream: Stopping condition met."); await stopDataStream(); // Ensure stopped if condition not met return; }; console.log("Stream: Fetching..."); try { // Fetch multiple PIDs - NOTE: sendCommand awaits each response // Use shorter timeouts for better responsiveness in streaming const rpmResponse = await sendCommand('010C', { timeout: 1500 }); // TODO: Parse RPM const speedResponse = await sendCommand('010D', { timeout: 1500 }); // TODO: Parse Speed setLastResponse(`RPM: ${rpmResponse} | Speed: ${speedResponse}`); // Update UI // Library automatically updates lastSuccessfulCommandTimestamp internally } catch (err: any) { console.error("Streaming fetch error:", err.message); // Library's inactivity timer will eventually stop isStreaming if errors persist. // The effect monitoring isStreaming will then stop the app's interval. } }, [appIsStreaming, isStreaming, connectedDevice, sendCommand, stopDataStream]); // Function called by button/switch to START polling const startDataStream = useCallback(() => { if (!connectedDevice || appIsStreaming || streamIntervalRef.current) return; console.log("Starting data stream..."); setAppIsStreaming(true); // Update app state setStreaming(true); // Signal intention to library // Fetch immediately then start interval fetchDataForStream(); streamIntervalRef.current = setInterval(fetchDataForStream, 1000); // Adjust interval as needed }, [connectedDevice, appIsStreaming, setStreaming, fetchDataForStream]); // Function called by button/switch to STOP polling const stopDataStream = useCallback(async () => { // Made async for potential await inside if (streamIntervalRef.current) { console.log("Stopping data stream..."); clearInterval(streamIntervalRef.current); streamIntervalRef.current = null; } // Always update app state and library state when explicitly stopping setAppIsStreaming(false); setStreaming(false); // Signal stop intention to library }, [setStreaming]); // --- Render Device Item --- const renderDeviceItem = ({ item }: { item: PeripheralWithPrediction }) => ( <TouchableOpacity onPress={() => handleConnect(item)} disabled={isConnecting || !!connectedDevice} style={[styles.listItem, {backgroundColor: item.isLikelyOBD ? '#e0ffe0' : 'white'}]} > <Text style={{ fontWeight: 'bold' }}>{item.name || 'Unnamed Device'}</Text> <Text>ID: {item.id}</Text> <Text>RSSI: {item.rssi} {item.isLikelyOBD ? '(Likely OBD)' : ''}</Text> </TouchableOpacity> ); // --- Main Render --- // Add the check for isInitializing if (isInitializing) { return ( <View style={styles.centered}> <ActivityIndicator size="large" /> <Text>Initializing Bluetooth...</Text> </View> ); } // Render the rest of the component only when not initializing return ( <ScrollView style={styles.container} contentContainerStyle={styles.contentContainer}> {/* Status Section */} <View style={styles.statusBox}> <Text>Bluetooth: {isBluetoothOn ? 'ON' : 'OFF'}</Text> <Text>Permissions: {hasPermissions ? 'Granted' : 'Missing'}</Text> <Text>Status: {connectedDevice ? `Connected to ${connectedDevice.name || connectedDevice.id}` : 'Disconnected'}</Text> {isConnecting && <Text style={styles.infoText}>Connecting...</Text>} {isDisconnecting && <Text style={styles.infoText}>Disconnecting...</Text>} {error && <Text style={styles.errorText}>Error: {(error as Error)?.message ?? 'Unknown error'}</Text>} </View> {/* Action Buttons */} <View style={styles.buttonGroup}> {!isBluetoothOn && <Button title="Enable Bluetooth" onPress={handleEnableBluetooth} />} {!hasPermissions && <Button title="Request Permissions" onPress={handleRequestPermissions} />} <Button title={isScanning ? 'Scanning...' : 'Scan Devices (5s)'} onPress={handleScan} disabled={isScanning || !isBluetoothOn || !hasPermissions || !!connectedDevice} /> </View> {/* Discovered Devices List */} {!connectedDevice && (isScanning || discoveredDevices.length > 0) ? ( <> <Text style={styles.sectionTitle}>Discovered Devices:</Text> <FlatList data={discoveredDevices} renderItem={renderDeviceItem} keyExtractor={(item) => item.id} style={styles.list} ListEmptyComponent={isScanning ? <ActivityIndicator style={{ marginVertical: 20 }}/> : <Text style={styles.emptyList}>No devices found.</Text>} /> </> ) : null} {/* Connected Device Section */} {connectedDevice && ( <View style={styles.section}> <Text style={styles.sectionTitle}>Connected: {connectedDevice.name || connectedDevice.id}</Text> {/* Basic Commands */} <Text style={styles.subSectionTitle}>Send Commands:</Text> <View style={styles.buttonGrid}> <Button title="ATZ" onPress={() => handleSendCommand('ATZ')} disabled={isLoadingCommand || isAwaitingResponse} /> <Button title="ATE0" onPress={() => handleSendCommand('ATE0')} disabled={isLoadingCommand || isAwaitingResponse} /> <Button title="010C (RPM)" onPress={() => handleSendCommand('010C')} disabled={isLoadingCommand || isAwaitingResponse} /> <Button title="010D (Speed)" onPress={() => handleSendCommand('010D')} disabled={isLoadingCommand || isAwaitingResponse} /> <Button title="ATDPN (Raw)" onPress={() => handleSendCommandRaw('ATDPN')} disabled={isLoadingCommand || isAwaitingResponse} /> </View> {(isLoadingCommand || isAwaitingResponse) && <ActivityIndicator style={{ marginTop: 5 }} />} {/* Response Display */} {lastResponse !== null && ( <View style={styles.responseBox}> <Text style={styles.responseTitle}>Last String Response:</Text> <Text style={styles.responseText}>{lastResponse || 'N/A'}</Text> <Text style={styles.parseNote}>(Remember to parse this data!)</Text> </View> )} {lastRawResponse !== null && ( <View style={styles.responseBox}> <Text style={styles.responseTitle}>Last Raw Response (Bytes):</Text> <Text style={styles.responseText}>{`[${lastRawResponse.join(', ')}]`}</Text> </View> )} {/* Streaming Controls */} <Text style={styles.subSectionTitle}>Real-time Data:</Text> <Text>Status: {appIsStreaming ? `Polling Active` : 'Polling Inactive'} {isStreaming && appIsStreaming ? `(Library OK - Last OK: ${lastSuccessfulCommandTimestamp ? new Date(lastSuccessfulCommandTimestamp).toLocaleTimeString() : 'N/A'})` : isStreaming && !appIsStreaming ? '(Library thinks active?)' : ''}</Text> <View style={styles.buttonGroup}> <Button title="Start Polling" onPress={startDataStream} disabled={appIsStreaming || isLoadingCommand || isAwaitingResponse} /> <Button title="Stop Polling" onPress={stopDataStream} disabled={!appIsStreaming} /> </View> {/* Disconnect */} <Button title="Disconnect" onPress={handleDisconnect} color="red" disabled={isDisconnecting} /> </View> )} </ScrollView> ); }; // Basic Styling const styles = StyleSheet.create({ container: { flex: 1 }, contentContainer: { padding: 15, paddingBottom: 50 }, // Added padding bottom centered: { flex: 1, justifyContent: 'center', alignItems: 'center' }, statusBox: { padding: 10, marginBottom: 10, backgroundColor: '#f0f0f0', borderRadius: 5, borderWidth: 1, borderColor: '#ddd'}, infoText: { fontStyle: 'italic', color: '#333'}, errorText: { color: 'red', marginTop: 5, fontWeight: 'bold' }, buttonGroup: { flexDirection: 'row', justifyContent: 'space-around', alignItems: 'center', marginVertical: 10, flexWrap: 'wrap', gap: 10 }, buttonGrid: { flexDirection: 'row', flexWrap: 'wrap', justifyContent: 'center', marginVertical: 5, gap: 10}, sectionTitle: { fontSize: 18, fontWeight: 'bold', marginTop: 15, marginBottom: 5 }, subSectionTitle: { fontSize: 16, fontWeight: '600', marginTop: 10, marginBottom: 5 }, list: { maxHeight: 250, borderWidth: 1, borderColor: '#ccc', borderRadius: 5, marginBottom: 10 }, listItem: { padding: 10, borderBottomWidth: 1, borderColor: '#eee'}, emptyList: { padding: 15, textAlign: 'center', fontStyle: 'italic', color: '#777' }, section: { marginTop: 20, borderTopWidth: 1, borderTopColor: '#eee', paddingTop: 15 }, responseBox: { marginTop: 10, padding: 8, backgroundColor: '#e8f4f8', borderRadius: 3, borderWidth: 1, borderColor: '#c7e0e8' }, responseTitle: { fontWeight: 'bold' }, responseText: { fontFamily: 'monospace', marginTop: 3 }, parseNote: { fontStyle: 'italic', fontSize: 10, color: '#555', marginTop: 2 }, }); export default YourMainAppComponent;
API Reference (useBluetooth)
The useBluetooth hook provides the primary interface for interacting with Bluetooth OBD adapters. It returns an object containing the current state and functions to perform actions.
State Variables
isBluetoothOn: boolean: Indicates if the device's Bluetooth adapter is currently powered ON. Updates automatically based on system events.hasPermissions: boolean: Reflects the status of required Bluetooth/Location permissions based on the last call tocheckPermissionsorrequestBluetoothPermissions.trueindicates necessary permissions appeared granted.isInitializing: boolean:truewhile the underlying nativeBleManagermodule is being initialized on app start. Your UI might want to wait for this to becomefalse.isScanning: boolean:trueif a BLE device scan initiated byscanDevices()is currently in progress.discoveredDevices: PeripheralWithPrediction[]: An array containing discovered BLE devices. Each object is aPeripheral(fromreact-native-ble-manager) potentially augmented with anisLikelyOBD: booleanflag based on device name heuristics. This array is cleared when a new scan starts and populated during the scan.isConnecting: boolean:truewhile an attempt to connect to a device viaconnectToDevice()is in progress.isDisconnecting: boolean:truewhiledisconnect()is executing.connectedDevice: Peripheral | null: Holds thePeripheralobject of the currently connected OBD adapter, ornullif no device is connected. This is the source of truth for connection status.activeDeviceConfig: ActiveDeviceConfig | null: If connected, contains the specific BLEserviceUUID,writeCharacteristicUUID,notifyCharacteristicUUID, and determinedwriteType('Write' or 'WriteWithoutResponse') being used for communication.nullotherwise.isAwaitingResponse: boolean:truewhensendCommandorsendCommandRawhas been called and the library is actively waiting for the response terminator (>) from the adapter. Use this to prevent sending concurrent commands.isStreaming: boolean: Reflects the intended streaming state set bysetStreaming()and whether the library's automatic inactivity timer has stopped it.truemeans the app intends to poll data and the library hasn't detected inactivity.falsemeans streaming is off or was stopped due to inactivity/disconnect. Monitor this state in your app to synchronize your polling loop.lastSuccessfulCommandTimestamp: number | null: TheDate.now()timestamp marking the completion of the last successful command (string or raw). Used by the streaming inactivity timer.nullif no commands have succeeded recently or streaming is off.error: Error | BleError | null: Holds the last error object encountered during any operation (permissions, scan, connect, command, etc.). Can be checked to display error messages. It's often cleared when a new operation starts.
Functions
checkPermissions(): Promise<boolean>- Checks the current status of required Bluetooth and Location permissions based on the platform and OS version.
- Updates the
hasPermissionsstate. - Returns
trueif all necessary permissions are currently granted,falseotherwise.
requestBluetoothPermissions(): Promise<boolean>- Initiates the native system prompts to request necessary Bluetooth and Location permissions.
- Updates the
hasPermissionsstate based on the user's response. - Returns
trueif all necessary permissions were granted by the user,falseotherwise. Note: If permissions areBLOCKED, returnsfalse, and the user must manually enable them in device settings.
promptEnableBluetooth(): Promise<void>- On Android, attempts to trigger the system dialog asking the user to turn on Bluetooth. Resolves when the prompt is dismissed or Bluetooth is enabled. Rejects if the user denies the request or an error occurs.
- On iOS, this function has no effect (logs a warning). Users must enable Bluetooth via Settings/Control Center. Resolves immediately.
scanDevices(scanDurationMs?: number): Promise<void>- Starts a BLE scan for nearby peripherals.
- Checks prerequisites (Bluetooth ON, Permissions Granted). Throws error if not met.
scanDurationMs(optional, default: 5000): Duration of the scan in milliseconds.- Sets
isScanningtotrueand clearsdiscoveredDevices. - Populates
discoveredDevicesas devices are found. - Resolves when the scan stops (either by duration or manually). Rejects on scan initiation error.
connectToDevice(deviceId: string): Promise<Peripheral>- Attempts to establish a BLE connection to the device with the given ID.
- Performs "smart discovery" by iterating through
KNOWN_ELM327_TARGETSto find compatible service/characteristic UUIDs. - Determines the correct write type (
WriteorWriteWithoutResponse). - Starts notifications for the response characteristic.
- Updates
isConnecting,connectedDevice,activeDeviceConfigstate. - Resolves with the connected
Peripheralobject on success. - Rejects on failure (incompatible device, connection timeout, service discovery error, notification error). Attempts cleanup via
disconnecton failure.
disconnect(): Promise<void>- Disconnects from the currently connected device.
- Stops notifications on the characteristic.
- Updates
isDisconnectingstate. TheconnectedDevicestate becomesnullvia the internal disconnect event listener. - Resolves when the disconnection process is successfully initiated. Rejects on error during the disconnection attempt.
sendCommand(command: string, options?: { timeout?: number }): Promise<string>- Sends an AT or OBD command string to the connected device. Do not include
\r. options.timeout(optional, default: ~4000ms): Custom timeout in milliseconds for waiting for the>response terminator for this specific command.- Automatically appends
\r, selects the correct BLE write method, waits for the complete response ending in>, and handles timeouts. - Updates
lastSuccessfulCommandTimestampon success. - Resolves with the trimmed response string (excluding
>). - Rejects on error (not connected, command pending, write error, timeout, disconnect during command).
- Sends an AT or OBD command string to the connected device. Do not include
sendCommandRaw(command: string, options?: { timeout?: number }): Promise<Uint8Array>- Identical to
sendCommandin operation (sends command, waits for>), but resolves with the complete raw response as aUint8Array(excluding the final>byte). Useful for non-ASCII or binary responses where exact byte values are needed. - Updates
lastSuccessfulCommandTimestampon success. - Rejects on error (not connected, command pending, write error, timeout, disconnect during command).
- Identical to
sendCommandRawChunked(command: string, options?: { timeout?: number }): Promise<ChunkedResponse>- Similar to
sendCommandRaw, but preserves the original data packet boundaries as received from the BLE characteristic. - Returns a
ChunkedResponseobject containing:data: Uint8Array- The complete flattened response (excluding the prompt byte)chunks: Uint8Array[]- Array of individual response chunks with preserved boundaries
- Useful when protocol-specific line breaks or packet boundaries must be preserved (e.g., multiline DTC responses).
- Updates
lastSuccessfulCommandTimestampon success. - Rejects on error (not connected, command pending, write error, timeout, disconnect during command).
- Similar to
setStreaming(shouldStream: boolean): void- Allows the application to signal its intent to start (
true) or stop (false) continuous data polling. - Updates the library's internal
isStreamingstate flag. - Setting to
trueresets thelastSuccessfulCommandTimestampand enables the library's internal inactivity timer (~4 seconds). - Setting to
falsedisables the inactivity timer and clears the timestamp. Your application should call this when explicitly stopping its polling loop.
- Allows the application to signal its intent to start (
Important Notes
Initialization Handling: Components using the
useBluetoothhook must check theisInitializingstate before accessing any Bluetooth functionality. This is critical because:- The
BluetoothProviderneeds time to initialize the native Bluetooth module - Attempting to use Bluetooth functions before initialization is complete may cause errors
- Always render a loading state when
isInitializingistrue - Only render your main component UI after
isInitializingbecomesfalse
- The
Native Setup: Correctly installing and configuring
react-native-ble-managerandreact-native-permissionsfor both iOS and Android is essential for this library to function. Refer to their official documentation.PID Parsing: This library does not parse OBD-II responses. Your application needs to implement the logic to convert the string (from
sendCommand) or byte (fromsendCommandRaw) responses into meaningful data based on the requested PID and OBD-II standards (SAE J1979).Error Handling: Always wrap function calls (
scanDevices,connectToDevice,sendCommand, etc.) intry...catchblocks or use.catch()on the returned promises to handle potential errors gracefully. Check theerrorstate variable for persistent errors.Concurrency: The library prevents sending a new command while
isAwaitingResponseis true. Ensure your application logic respects this flag or queues commands appropriately.Data Buffering: Both
sendCommandandsendCommandRawinternally buffer incoming data chunks from the BLE device. They only resolve their respective Promises after the complete response (signalled by the>character) has been received or a timeout occurs. For preserving exact packet boundaries, usesendCommandRawChunked.Streaming State Synchronization: The application should manage its own polling interval (
setInterval). UsesetStreaming(true)when starting the interval andsetStreaming(false)when clearing it. Monitor theisStreamingstate from the hook; if it becomesfalseunexpectedly (due to inactivity timeout or disconnect), your application should clear its own interval timer.
License
MIT
