Skip to content

🐛 Android Crash on Second Connection After App Relaunch (monitorCharacteristicForDevice) #1332

Description

@rrubankumar-ctrl

Prerequisites

  • I checked the documentation and FAQ without finding a solution
  • I checked to make sure that this issue has not already been filed

Expected Behavior

The application crashes on Android when connecting to a BLE device for the second time after closing and reopening the app. The crash occurs when navigating to the reading screen where monitorCharacteristicForDevice() is used.

Current Behavior

This seems related to a known issue where the app crashes instead of handling disconnection properly.

Library version

3.5.0

Device

all devices

Environment info

The application crashes on Android when connecting to a BLE device for the second time after closing and reopening the app. The crash occurs when navigating to the reading screen where monitorCharacteristicForDevice() is used.

Steps to reproduce

  1. …
  2. …Install and open the application on an Android device.

Scan and connect to a BLE peripheral device.

Navigate to the reading/data screen where monitorCharacteristicForDevice() is started.

Verify that data is being received correctly.

Close or kill the application from recent apps (without manually disconnecting the BLE device).

Reopen the application.

Connect to the same BLE device again.

Navigate to the reading/data screen again.

Formatted code sample or link to a repository

nil

Relevant log output

nil

Additional information

No response

Activity

  1. rrubankumar-ctrl commented on Mar 20, 2026

    @rrubankumar-ctrl
    Author

    useFocusEffect(
    useCallback(() => {
    isMountedRef.current = true;

      const timer = setTimeout(() => {
        void onClickNewTestRef.current(); // latest devices, not stale closure from old useCallback
      }, 5000); // 🔥 small delay is critical
    
      return () => {
        isMountedRef.current = false;
    
        clearTimeout(timer);
    
        cancelListenersRef.current.forEach((cancel) => {
          try {
            cancel?.();
          } catch (e) {
            console.warn("Cleanup error:", e);
          }
        });
    
        cancelListenersRef.current = [];
    
        shownErrors.current.clear();
        modalQueue.current = [];
        isModalOpen.current = false;
    
        coldFingerStatusRef.current.left = { count: 0, isCold: false };
        coldFingerStatusRef.current.right = { count: 0, isCold: false };
      };
    }, [leftSensor?.device?.id, rightSensor?.device?.id])
    

    );
    const onClickNewTest = async () => {
    cancelListenersRef.current.forEach((stopFn) => {
    try {
    stopFn?.();
    } catch (e) {
    console.warn("Error stopping listener:", e);
    }
    });
    cancelListenersRef.current = [];

    try {
      if (leftSensor?.device) {
        const connected = await isPeripheralConnected(leftSensor.device);
        if (!connected) {
          if (__DEV__) {
            console.warn("[FuncTestPreparation] Left sensor not connected; skip monitor");
          }
        } else {
          const { stop } = monitorDeviceSensorData(
            leftSensor.device as any,
            throttleSample("left")
          );
          cancelListenersRef.current.push(stop);
        }
      }
    
      if (rightSensor?.device) {
        const connected = await isPeripheralConnected(rightSensor.device);
        if (!connected) {
          if (__DEV__) {
            console.warn("[FuncTestPreparation] Right sensor not connected; skip monitor");
          }
        } else {
          const { stop } = monitorDeviceSensorData(
            rightSensor.device as any,
            throttleSample("right")
          );
          cancelListenersRef.current.push(stop);
        }
      }
    } catch (error) {
      console.error("❌ Error during monitoring:", error);
    }
    

    };

  2. rrubankumar-ctrl commented on Mar 23, 2026

    @rrubankumar-ctrl
    Author

    export const monitorDeviceSensorData = (
    device: Device,
    onLiveData?: (data: any) => void,
    timeout?: number
    ): { promise: Promise<any[]>; stop: () => void } => {
    const SERVICE_UUID = FINGER_SERVICE;
    const UUID_FINGER_TEMP = IMU_UUIDS.FINGER_TEMP;
    const UUID_ROOM_TEMP = IMU_UUIDS.ROOM_TEMP;
    const UUID_IMU = IMU_UUIDS.RAW_IMU_DATA;

    const sensorDataArray: any[] = [];

    let latestData: any = {
    fingerTemp: null,
    roomTemp: null,
    imu: null,
    imuEvent: null as "significant_motion" | "wrist_tilt" | "wake_up" | null,
    timestamp: null,
    };

    let isStopped = false;
    const imuEventSubscriptions: { remove: () => void }[] = [];
    let imuDataPollIntervalId: ReturnType | null = null;
    let imuEventPollIntervalId: ReturnType | null = null;

    const pushAndEmit = () => {
    const snapshot = { ...latestData };
    sensorDataArray.push(snapshot);
    onLiveData?.(snapshot);
    };

    const onIMUEvent = (event: "significant_motion" | "wrist_tilt" | "wake_up") => {
    if (isStopped) return;
    latestData.imuEvent = event;
    latestData.timestamp = new Date().toISOString();
    console.log("[BLE] IMU event:", event);
    pushAndEmit();
    setTimeout(() => {
    if (!isStopped) latestData.imuEvent = null;
    }, 2000);
    };

    let fingerTempSubscription: { remove: () => void } | null = null;
    let roomTempSubscription: { remove: () => void } | null = null;
    try {
    fingerTempSubscription = device.monitorCharacteristicForService(
    SERVICE_UUID,
    UUID_FINGER_TEMP,
    (error, characteristic) => {
    if (!isStopped && !error && characteristic?.value) {
    latestData.fingerTemp = parseFingerTemp(characteristic.value);
    latestData.timestamp = new Date().toISOString();
    pushAndEmit();
    }
    }
    );
    } catch (e) {
    console.warn("[BLE] finger temp monitor failed:", (e as Error)?.message ?? e);
    }

    try {
    roomTempSubscription = device.monitorCharacteristicForService(
    SERVICE_UUID,
    UUID_ROOM_TEMP,
    (error, characteristic) => {
    if (!isStopped && !error && characteristic?.value) {
    latestData.roomTemp = parseRoomTemp(characteristic.value);
    latestData.timestamp = new Date().toISOString();
    if (DEV) console.log("[BLE] roomTemp delivered", latestData.roomTemp);
    pushAndEmit();
    }
    }
    );
    } catch (e) {
    console.warn("[BLE] room temp monitor failed:", (e as Error)?.message ?? e);
    }

    let imuSubscription: { remove: () => void } | null = null;

    // IMU: discover service. For c600 use polling only (no notify). Else try subscribe then config + event polling.
    const IMU_DATA_POLL_MS = 150;
    const IMU_POLL_READ_TIMEOUT_MS = 4000;
    let imuDataPollingStarted = false;
    let imuC600NotifyFallbackDone = false;
    let imuDiscoveryStarted = false;
    let imuPollReadFailLogged = false;
    let imuPollEmptyLogged = false;
    let imuPollTimeoutLogged = false;
    const startIMUDataPolling = (svc: string, charUUID: string) => {
    if (isStopped || imuDataPollIntervalId || imuDataPollingStarted) return;
    imuDataPollingStarted = true;
    const devId = (device?.id ?? "").slice(-8);
    console.log("[BLE] IMU data: polling", charUUID, "on", svc, devId ? [${devId}] : "");
    imuDataPollIntervalId = setInterval(async () => {
    if (isStopped) return;
    try {
    const readPromise = device.readCharacteristicForService(svc, charUUID);
    const timeoutPromise = new Promise((_, reject) =>
    setTimeout(() => reject(new Error("timeout")), IMU_POLL_READ_TIMEOUT_MS)
    );
    const c = await Promise.race([readPromise, timeoutPromise]);
    if (isStopped) return;
    if (!c?.value) {
    if (!imuPollEmptyLogged) {
    imuPollEmptyLogged = true;
    console.log("[BLE] IMU data: read ok but value empty (char may be write-only or not streaming)");
    }
    return;
    }
    const parsed = parseIMUData(c.value);
    latestData.imu = parsed;
    latestData.timestamp = new Date().toISOString();
    console.log("[BLE] IMU parsed", parsed);
    pushAndEmit();
    } catch (e) {
    const msg = (e as Error)?.message ?? String(e);
    if (msg === "timeout") {
    if (!imuPollTimeoutLogged) {
    imuPollTimeoutLogged = true;
    if (imuDataPollIntervalId) {
    clearInterval(imuDataPollIntervalId);
    imuDataPollIntervalId = null;
    }
    console.log("[BLE] IMU (c604) read not available, stopped");
    }
    } else if (!imuPollReadFailLogged) {
    imuPollReadFailLogged = true;
    console.log("[BLE] IMU data: poll read failed:", msg);
    }
    }
    }, IMU_DATA_POLL_MS);
    };

    const startIMUWithDiscoveredService = () => {
    if (imuDiscoveryStarted) return;
    imuDiscoveryStarted = true;
    findIMUServiceUUID(device)
    .then((imuServiceUUID) => {
    if (isStopped) return;
    return configureIMUBuiltInFunctions(device, imuServiceUUID).then((res) => ({ imuServiceUUID, res }));
    })
    .then((out) => {
    if (isStopped || !out) return;
    const svc = out.imuServiceUUID;
    if (svc) {
    console.log("[BLE] IMU starting event monitors on", svc.slice(-4));
    startIMUEventMonitoring(svc);
    }
    })
    .catch((e) => {
    console.log("[BLE] IMU setup error:", (e as Error)?.message ?? e);
    });
    };
    startIMUWithDiscoveredService();

    // IMU events per BuiltInIMU.py / process_received_data: poll FUNC_SRC1, FUNC_SRC2, WAKE_UP_SRC every 500ms and parse (data&0x40 sm, data&0x1 wt, data&0x08 wu).
    const startIMUEventMonitoring = (imuServiceUUID: string) => {
    if (isStopped || imuEventSubscriptions.length > 0) return;
    const R = IMU_UUIDS;
    const DEBOUNCE_MS = 1500;
    const lastFired: Record<string, number> = {};

    const checkByteAndFire = (which: "sm" | "wt" | "wu", data: number, event: "significant_motion" | "wrist_tilt" | "wake_up", mask: number) => {
      if (!(data & mask)) return;
      const now = Date.now();
      const key = which;
      if ((now - (lastFired[key] ?? 0)) <= DEBOUNCE_MS) return;
      lastFired[key] = now;
      onIMUEvent(event);
    };
    
    const onEventData = (which: "sm" | "wt" | "wu") => (error: any, characteristic: any) => {
      if (isStopped || error || !characteristic?.value) return;
      const raw = atob(characteristic.value);
      const data = raw.length > 0 ? raw.charCodeAt(0) : 0;
      if (which === "sm") checkByteAndFire("sm", data, "significant_motion", 0x40);
      if (which === "wt") checkByteAndFire("wt", data, "wrist_tilt", 0x01);
      if (which === "wu") checkByteAndFire("wu", data, "wake_up", 0x08);
    };
    
    try {
      imuEventSubscriptions.push(device.monitorCharacteristicForService(imuServiceUUID, R.FUNC_SRC1, onEventData("sm")));
      imuEventSubscriptions.push(device.monitorCharacteristicForService(imuServiceUUID, R.FUNC_SRC2, onEventData("wt")));
      imuEventSubscriptions.push(device.monitorCharacteristicForService(imuServiceUUID, R.WAKE_UP_SRC, onEventData("wu")));
      console.log("[BLE] IMU event monitors subscribed (c653,c654,c61b)", imuServiceUUID);
    } catch (e) {
      console.log("[BLE] IMU event monitor subscribe failed:", (e as Error)?.message ?? e);
    }
    
    // Polling mode per Python: read result registers every 500ms (IMU_REG_UUID_FUNC_SRC1, FUNC_SRC2, WAKE_UP_SRC).
    const IMU_EVENT_POLL_MS = 500;
    const READ_TIMEOUT_MS = 2000;
    const readReg = (charUUID: string): Promise<number> =>
      Promise.race([
        device.readCharacteristicForService(imuServiceUUID, charUUID).then((c: any) => {
          if (!c?.value) return 0;
          const raw = atob(c.value);
          return raw.length > 0 ? raw.charCodeAt(0) : 0;
        }),
        delay(READ_TIMEOUT_MS).then(() => 0),
      ]).catch(() => 0);
    
    if (!imuEventPollIntervalId) {
      imuEventPollIntervalId = setInterval(async () => {
        if (isStopped) return;
        try {
          const [v1, v2, v3] = await Promise.all([
            readReg(R.FUNC_SRC1),
            readReg(R.FUNC_SRC2),
            readReg(R.WAKE_UP_SRC),
          ]);
          if (isStopped) return;
          checkByteAndFire("sm", v1, "significant_motion", 0x40);
          checkByteAndFire("wt", v2, "wrist_tilt", 0x01);
          checkByteAndFire("wu", v3, "wake_up", 0x08);
        } catch (_) {}
      }, IMU_EVENT_POLL_MS);
      console.log("[BLE] IMU event polling 500ms (FUNC_SRC1,FUNC_SRC2,WAKE_UP_SRC) per BuiltInIMU.py");
    }
    

    };

    const stop = () => {
    isStopped = true;
    imuEventSubscriptions.forEach((sub) => {
    try {
    sub.remove();
    } catch () {}
    });
    imuEventSubscriptions.length = 0;
    if (imuEventPollIntervalId) {
    clearInterval(imuEventPollIntervalId);
    imuEventPollIntervalId = null;
    }
    if (imuDataPollIntervalId) {
    clearInterval(imuDataPollIntervalId);
    imuDataPollIntervalId = null;
    }
    try {
    fingerTempSubscription?.remove?.();
    } catch (
    ) {}
    try {
    roomTempSubscription?.remove?.();
    } catch () {}
    try {
    imuSubscription?.remove();
    } catch (
    ) {}
    console.log("🛑 Monitoring manually stopped.");
    };

    const promise = new Promise<any[]>((resolve) => {
    setTimeout(() => {
    stop(); // Auto stop after timeout
    resolve(sensorDataArray);
    }, timeout ?? TIMEOUT_15_MINUTES);
    });

    return { promise, stop };
    };

  3. rrubankumar-ctrl commented on Mar 23, 2026

    @rrubankumar-ctrl
    Author

    03-23 11:10:43.064 28371 28425 I ReactNativeJS: params: { devicename: 'LeftFingerSensor' },
    03-23 11:10:43.064 28371 28425 I ReactNativeJS: path: undefined } ],
    03-23 11:10:43.064 28371 28425 I ReactNativeJS: preloadedRouteKeys: [],
    03-23 11:10:43.064 28371 28425 I ReactNativeJS: default: 'closed' }
    03-23 11:10:43.096 28371 28425 I ReactNativeJS: 'TestsStack', 'activeRoute'
    03-23 11:10:43.543 28534 28564 E BtGatt.ContextMap: Context not found for ID 6
    03-23 11:10:43.543 28534 28564 E BtGatt.ContextMap: Context not found for ID 7
    03-23 11:10:43.543 28534 28564 E BtGatt.ContextMap: Context not found for ID 8
    03-23 11:10:44.591 684 822 W BluetoothEventManager: AclStateChangedHandler: activeDevice is null
    03-23 11:10:44.614 684 822 W BluetoothEventManager: AclStateChangedHandler: activeDevice is null
    03-23 11:10:44.621 684 822 W BluetoothEventManager: AclStateChangedHandler: activeDevice is null

  4. rrubankumar-ctrl commented on Mar 23, 2026

    @rrubankumar-ctrl
    Author

    @aliberski ....Hi can you have any answer

  5. rrubankumar-ctrl commented on Mar 23, 2026

    @rrubankumar-ctrl
    Author

    android 11

  6. andrewcharnley commented on Apr 10, 2026

    @andrewcharnley

    I had a quick look as I don't have this issue but I couldn't see anything obvious. It's difficult without having all the code indented.

    I still use 3.2.6, I think after this Promise issues were introduced and I'm not confident they are fixed. There's a lack of testing with this library.

    The other thing is a few of the calls are passing the uuid but it's not what is expected under the hood, iirc it's the uuid mapping id, a number. When it gets through to android/ios it usually works still as they must have some compat code in there but again I'm not 100% sold on it always working. Instead I use only device.*, i. e device.monitorCharacteristicForService calls.

    Since that's an easy change I'd try that first, then try downgrading to 3.2.*. This library is pretty much abandoned and is not in a good state.

    Edit:

    I had a look again in my code. When your App 'exits' is it going into the background, (the default behaviour)? If so you need to remove all the monitorCharacteristics and set them back up when returning to foreground. See AppState.addEventListener('change', ...

    I can't recall why I had to do this other than good practice for battery life. Something tells me it was to do with if the App isn't a service or have background permissions Android semi terminates the Bluetooth after a small delay and it errors the subscriptions.

  7. p-dot-max commented on May 8, 2026

    @p-dot-max

    Hi, just wanted to flag that this review comment appears to be a duplicate of an earlier issue #1333 #1329 already discussed/resolved in the PR thread.

    The same concern was raised previously, responded to, and addressed in follow-up commits, but seems to have been reopened again without considering the latest changes.

    please take a look and close the duplicate thread if appropriate?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions