react-native-ble-plx
react-native-ble-plx copied to clipboard
Bug Report
Prerequisites
Please answer the following questions for yourself before submitting an issue. YOU MAY DELETE THE PREREQUISITES SECTION.
- [X] I am running the latest version
- [X] I checked the documentation and found no answer
- [X] I checked to make sure that this issue has not already been filed
Expected Behavior
We're developing an app for a client which will be required to connect to a heart-rate bluetooth device. What we have is code to determine if bluetooth is in "PoweredOn" state in ios, and if so, scan for devices. If it finds devices capable of serving heart rate data through a specific service, then list them for the user. When the user taps on one of them, the app should connect to that device and start streaming heart rate data to our api.
Current Behavior
Everything works great on Android. However, on iOS, I continue having issues with the device.connect() method. It will SOMETIMES connect to the device when the app is running through XCode, but as soon as I release it to TestFlight, it NEVER works. And sometimes it doesn't work through XCode either.
I currently have the following logs:
2021-08-23 13:53:31.413995-0700 mobilefrontend[23420:5608921] [javascript] Scanning 2021-08-23 13:53:31.425763-0700 mobilefrontend[23420:5609561] [CoreBluetooth] API MISUSE: <CBCentralManager: 0x281bf4680> has no restore identifier but the delegate implements the centralManager:willRestoreState: method. Restoring will not be supported 2021-08-23 13:53:31.427850-0700 mobilefrontend[23420:5609164] [CoreBluetooth] XPC connection invalid
I also get the error "Device is in an Unknown State" (I don't remember the exact wording of it) sometimes when trying to connect.
Here is my code for this (yes, it's a bit messy. This is a rapid-dev prototype that we just need to get working.) bleManager is currently set up in a REF hook (bluetoothManager = useRef(bleManager))
const requestBLEPermission = async () => {
try {
const granted = await PermissionsAndroid.request(
PermissionsAndroid.PERMISSIONS.ACCESS_COARSE_LOCATION,
{
title: "Gladys",
message:
"Gladys Needs Location Access " +
"in order to capture Bluetooth Data",
buttonNeutral: "Ask Me Later",
buttonNegative: "Cancel",
buttonPositive: "OK"
}
);
if (granted === PermissionsAndroid.RESULTS.GRANTED) {
scanAndConnect();
}
} catch (err) {
}
};
useEffect(() => {
if (Platform.OS == "ios") {
bluetoothManager.current.onStateChange((state) => {
const subscription = bluetoothManager.current.onStateChange((state) => {
if (state === 'PoweredOn') {
startScan();
subscription.remove();
}
}, true);
return () => subscription.remove();
});
} else {
startScan()
}
}, [bluetoothManager]);
function startScan() {
if (allowScan === true) {
if (Platform.OS === "ios") {
scanAndConnect()
} else {
requestBLEPermission();
}
}}
function startConnect(ind) {
setBtStatusText("Connecting...")
setConnectBtID(ind)
setAllowScan(false)
bluetoothManager.current.stopDeviceScan()
connectBt(ind)
}
function connectBt(ind) {
setAllowScan(false)
crashlytics().log("ConnectBT", ind)
bluetoothManager.current.stopDeviceScan()
if (btSubscription.current === null){
crashlytics().log("Handling BT")
handleBtConnection(ind)
} else {
crashlytics().log("ConnectBT - already connected")
}
}
function handleMonitorBt(ind) {
crashlytics().log("Handling")
let device = btDevicesDictCore[btDevices[ind]]
btSubscription.current = device.monitorCharacteristicForService("0000180d-0000-1000-8000-00805f9b34fb", "00002a37-0000-1000-8000-00805f9b34fb", (error, characteristic) => {
crashlytics().log("Subscription")
if (btStatusText !== "Connected") {
setBtStatusText("Connected")
crashlytics().log("Connected")
}
if (error) {
crashlytics().log("ERROR", error)
crashlytics().log("ERROR", error.errorCode)
if (btStatusText !== "Device not connected") {
setBtStatusText("Device not connected")
}
if (error.errorCode !== 201) {
try{
cancelConnect()
} catch(err){
crashlytics().log(err)
}
} else {
cancelConnect2()
}
} else if (characteristic && characteristic.value) {
try{
let heartRate = -1;
let rrRawList = [];
let decoded = Buffer.from(characteristic.value, 'base64');
let firstBitValue = decoded.readInt8(0) & 0x01;
let rrintervalExist = decoded.readInt8(4) & 0x01;
let eeExist = decoded.readInt8(3) & 0x01;
let i = null;
if (firstBitValue == 0) {
// Heart Rate Value Format is in the 2nd byte
heartRate = decoded.readInt8(1);
}
if (rrintervalExist != 0) {
if (eeExist == 0) {
for (i=2; i<decoded.length; i+=2){
let rr = (decoded.readUInt16LE(i, true) / 1024)*1000
rrRawList.push(rr);
}
} else {
for (i=4; i<decoded.length; i+=2){
let rr = (decoded.readUInt16LE(i, true) / 1024)*1000
rrRawList.push(rr);
}
}
}
var date = new Date().toISOString();
if (rrRawList.length !=0) {
if (rrRawList.length < 3) {
for (i=0; i<(4-rrRawList.length); i++){
rrRawList.push(-1)
}
}
let tempData = {"time":date, "BPM": heartRate, "rr1":rrRawList[0], "rr2":rrRawList[1], "rr3":rrRawList[2]}
hrvData.push(tempData)
if (hrvData.length >= 10) {
onTimer()
}
///setHrvData([...hrvData, tempData])
}
} catch(err) {
}
}
}, "device_subscribed")
}
const handleBtConnection = async (ind) => {
let device = btDevicesDictCore[btDevices[ind]]
let isConnected = await device.isConnected()
if (device.isConnected() === true) {
crashlytics().log("Device is connected")
session_start_time = new Date().toISOString()
handleMonitorBt(device)
} else {
crashlytics().log("Attempting to Connect")
device.connect()
.then((device) => {
return(device.discoverAllServicesAndCharacteristics())
})
.then((device) => {
return(device.services(device.id))
// Do work on device with services and characteristics
})
.then((service)=>{
session_start_time = new Date().toISOString()
crashlytics().log("Handle Monitor")
handleMonitorBt(ind)
})
.catch((error) => {
crashlytics().log("CATCH", error)
retryConnection(device)
});
}
}
const retryConnection = (device) => {
device.connect()
.then((device) => {
return(device.discoverAllServicesAndCharacteristics())
})
.then((device) => {
return(device.services(device.id))
// Do work on device with services and characteristics
})
.then((service)=>{
session_start_time = new Date().toISOString()
crashlytics().log("Handle Monitor")
handleMonitorBt(ind)
})
.catch((error) => {
crashlytics().log("CATCH RETRY", error)
setConnectBtID(null)
setBtStatusText("Device not connected")
setAllowScan(true)
});
}
function cancelConnect() {
setBtStatusText("Canceling Connection")
let device = btDevicesDictCore[btDevices[connectBtID]]
if (btSubscription.current !== null) {
btSubscription.current.remove()
btSubscription.current = null
bluetoothManager.current.cancelTransaction("device_subscribed")
}
if (device !== undefined){
bluetoothManager.current.cancelDeviceConnection(device.id)
}
session_end_time = new Date().toISOString()
publishSession()
setConnectBtID(null)
setAllowScan(true)
}
function cancelConnect2() {
setBtStatusText("Canceling Connection 2")
let device = btDevicesDictCore[btDevices[connectBtID]]
if (btSubscription.current !== null) {
btSubscription.current.remove()
btSubscription.current = null
bluetoothManager.current.cancelTransaction("device_subscribed")
}
if (device !== undefined){
bluetoothManager.current.cancelDeviceConnection(device.id)
}
session_end_time = new Date().toISOString()
publishSession()
setAllowScan(true)
}
function addDevices() {
if (btDevices !== []) {
listBtDevicesDict = [];
btDevices.forEach(iterBtDevices);
}
}
function iterBtDevices(val, idx, array) {
const btDevice = {
value: idx,
label: val,
}
let dictBtDevices = btDevicesDict;
if (dictBtDevices[idx] !== val) {
dictBtDevices[idx] = val;
setBtDevicesDict(dictBtDevices)
}
listBtDevicesDict.push(btDevice);
setBtDevicesDictList(listBtDevicesDict)
}
function scanAndConnect() {
bluetoothManager.current.startDeviceScan(null, null, (error, device) => {
console.log("Scanning")
if (error) {
console.log(error)
if (btStatusText !== "Bluetooth is unavailable"){
setBtStatusText("Bluetooth is unavailable")
}
} else {
if(btStatusText !== "Scanning for Devices...") {
setBtStatusText("Scanning for Devices...")
}
if ((device !== null) && (device.name !== null) && (device.id !== null)){
if (btDevices.indexOf(device.name) == -1) {
if ((device.serviceUUIDs !== null) && (device.serviceUUIDs.indexOf("0000180d-0000-1000-8000-00805f9b34fb") > -1)){
let tempDict = btDevicesDictCore;
let tempList = btDevices;
tempDict[device.name] = device
tempList.push(device.name)
setBtDevicesDictCore(tempDict);
setBtDevices(tempList)
addDevices();
}
} else if (connectBtID && btDevicesDictCore && btDevices) {
if (btDevicesDictCore[btDevices[connectBtID]].id === device.id){
bluetoothManager.current.stopDeviceScan()
connectBt(connectBtID)
}}
} else if (device === null) {
if(btStatusText !== "Bluetooth not enabled") {
setBtStatusText("Bluetooth not enabled")
}
}
}});
}
This is only a problem in iOS, Android works PERFECT with the existing code. The problem is exacerbated by pushing to the iOS store/TestFlight.
Any help would be appreciated.
I am also facing a similar issue on iOS (with an example similar to yours working on Android). I believe the bug arises from storing the BleManager instance in a ref. Moving it elsewhere lets me invoke the device scan on iOS, but doing so causes some other strange behavior, such as not being able to stop the device scan. I will let you know if I find a workaround.
I am experiencing the same issues on IOS. 'BluetoothLE is in unknown state' and in xcode console:
[CoreBluetooth] XPC connection invalid
[CoreBluetooth] API MISUSE: <CBCentralManager: 0x280955680> has no restore identifier but the delegate implements the centralManager:willRestoreState: method. Restoring will not be supported
On Android it works flawlessly.
This error occurs regardless of which instantiation I use:
const bleManager = React.useRef(new BleManager())
let bleManager = new BleManager()
and on both debug and release builds. Haven't been able to get it to work for IOS.
Here is a shortened version of my bluetooth screen implementation
export default function BluetoothDeviceDashboard(navigation) {
const bleManager = new BleManager();
const [isScanning, setIsScanning] = React.useState(false);
const [statusMessage, setStatusMessage] = React.useState('');
const [foundDevice, setFoundDevice] = React.useState<Device>()
const scanForDevices = () => { ... }
const connectToDevice = (device: Device) => { ... }
React.useEffect(() => {
if (foundDevice) {
setTimeout(() => { connectToDevice(foundDevice) }, 1500)
}
}, [foundDevice])
const startScan = () => {
setFoundDeviceInformation(undefined)
const subscription = bleManager.onStateChange((state) => {
if (state === 'PoweredOn') {
scanForDevices();
subscription.remove();
} else {
navigation.goBack()
Alert.alert('Bluetooth is off', 'Please turn on Bluetooth to continue')
}
}, true);
}
React.useEffect(() => {
return () => {
bleManager.destroy()
}
}, [])
return (
<View style={styles.container}>
<View style={styles.headerContainer}>
<Pressable onPress={() => { navigation.navigate('BluetoothRegisteredDevices') }}>
<Text style={styles.linkText}>View All Registered Devices</Text>
</Pressable>
</View>
<View style={styles.scanningContainer}>
<Text style={styles.title}>{statusMessage}</Text>
<ActivityIndicator size="large" color={Color.blue} animating={isScanning} />
</View>
<View style={styles.deviceContainer}>
{foundDevice && foundDevice.name ? (
<FoundBluetoothDeviceCard deviceInformation={getConfigData(foundDevice.name) as BluetoothDeviceInformation} activeReadings={liveReadings} />
) : null}
</View>
<View style={styles.buttonFooterContainer}>
<Pressable style={[styles.buttonContainer, styles.defaultButton, isScanning ? styles.greyed : null]} onPress={() => startScan()} disabled={isScanning}>
<Text style={styles.buttonText}>Clear and Scan Again</Text>
</Pressable>
</View>
</View>
)
}
Any help would be greatly appreciated, thanks
What I ended up doing was instantiating/exporting BleManager
in the root of my app (index.js
). There's an example showing how to do this here. Afterwards, I was no longer seeing that error in Xcode. Good luck!
What I ended up doing was instantiating/exporting
BleManager
in the root of my app (index.js
). There's an example showing how to do this here. Afterwards, I was no longer seeing that error in Xcode. Good luck!
Love the work around, thank you!
The new major release updates the core packages that support the latest versions of the operating system and fixes some old bugs. Please confirm if the issue still persists and create a new issue if it still does.