1

I'm building a BLE adapter for an medical device. It scans, connects, sets up notifications, and requests data. Everything works when the adapter first finds and connects to the device, but if the device disconnects (intentional or accidental), subsequent scans often do not find the device. After a few app reopens or good amount of time and rescan attempts, it eventually shows up and reconnects.

Full class (sensitive values redacted):


public class ThermometerAdapter implements BleDeviceManager.DeviceAdapter {

    private static final String TAG = "ThermoMeterAdapter";
    private static final String DEVICE_NAME = "My Thermometer";
    private static final int DEFAULT_TIMEOUT_SEC = 20;
    private static final int SCAN_RETRY_MAX = 5;

    // GATT UUIDs
    private static final UUID SERVICE_UUID = UUID.fromString("<UUID>");
    private static final UUID CHAR_UUID = UUID.fromString("<UUID>");
//    private static final UUID CCCD_UUID = UUID.fromString("<UUID>");
    // Data frame constants
    private static final byte START_CODE = (byte) 0xaa;
    private static final byte TYPE_EAR = 0x22;
    private static final byte TYPE_FOREHEAD = 0x33;
    private static final byte TYPE_OBJECT = 0x55;

    // Retry policy
    private static final long RESCAN_DELAY_MS = 2500;
    private static final long KEEPALIVE_INTERVAL_MS = 30000; // 30 seconds

    private final Handler main = new Handler(Looper.getMainLooper());
    private Context appCtx;
    private InternalCallback cb;
    private BleDeviceManager.VitalType requestedType;
    private volatile boolean shouldRun;
    private volatile boolean isConnected;
    private long attempts;

    private BluetoothAdapter btAdapter;
    private BluetoothGatt gatt;
    private BluetoothDevice targetDevice;
    private ScanCallback scanCallback;

    @Override
    public void start(Context ctx, BleDeviceManager.VitalType type, InternalCallback callback) {
        stop();
        this.appCtx = ctx.getApplicationContext();
        this.cb = callback;
        this.requestedType = type;
        this.shouldRun = true;
        this.isConnected = false;
        this.attempts = 0;

        postStatus("Initializing thermometer…");
        main.postDelayed(this::startScan, 300);
    }

    @RequiresPermission(allOf = {Manifest.permission.BLUETOOTH_CONNECT, Manifest.permission.BLUETOOTH_SCAN})
    @Override
    public void stop() {
        shouldRun = false;
        isConnected = false;
        main.removeCallbacksAndMessages(null);

        // Stop scanning first
        try {
            if (btAdapter != null && scanCallback != null) {
                BluetoothLeScanner scanner = btAdapter.getBluetoothLeScanner();
                if (scanner != null) {
                    scanner.stopScan(scanCallback);
                    Log.d(TAG, "Scanner stopped");
                }
            }
        } catch (Throwable t) {
            Log.w(TAG, "stop scanner", t);
        }

        // Then disconnect GATT
        try {
            if (gatt != null) {
                gatt.disconnect();
                // Small delay before close to ensure disconnect completes
                try { Thread.sleep(100); } catch (InterruptedException ignored) {}
                gatt.close();
                gatt = null;
                Log.d(TAG, "GATT closed");
            }
        } catch (Throwable t) {
            Log.w(TAG, "stop gatt", t);
        }

        targetDevice = null;
        scanCallback = null;

        Log.d(TAG, "Adapter stopped completely");
    }

    // ===== Scanning =====

    private void startScan() {
        if (!shouldRun) return;
        if (attempts >= SCAN_RETRY_MAX) {
            postError("Failed to find thermometer after " + SCAN_RETRY_MAX + " attempts.");
            Log.d(TAG,"Failed to find thermometer after " + SCAN_RETRY_MAX + " attempts.");
            return;
        }

        attempts++;
        postStatus("Scanning for thermometer (" + attempts + ")…");
        Log.d(TAG,"Scanning for thermometer (" + attempts + ")…");

        try {
            btAdapter = BluetoothAdapter.getDefaultAdapter();
            if (btAdapter == null) {
                postError("Bluetooth adapter not available");
                Log.d(TAG,"Bluetooth adapter not available");
                return;
            }

            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
                if (!hasPermission(Manifest.permission.BLUETOOTH_SCAN)) {
                    postError("BLUETOOTH_SCAN permission denied");
                    Log.d(TAG,"BLUETOOTH_SCAN permission denied");
                    return;
                }
            }

            BluetoothLeScanner scanner = btAdapter.getBluetoothLeScanner();
            if (scanner == null) {
                postError("BLE scanner not available");
                scheduleRetry();
                return;
            }

            scanCallback = new ScanCallback() {
                @Override
                public void onScanResult(int callbackType, ScanResult result) {
                    if (!shouldRun) return;

                    String name = result.getDevice().getName();
                    if (name != null && name.trim().equalsIgnoreCase(DEVICE_NAME.trim())) {
                        Log.d(TAG, "Found " + DEVICE_NAME);
                        targetDevice = result.getDevice();
                        postStatus("Found " + DEVICE_NAME + " — connecting…");

                        // Stop scanning before connecting
                        try {
                            scanner.stopScan(this);
                        } catch (SecurityException e) {
                            Log.w(TAG, "Failed to stop scan", e);
                        }

                        connectToDevice();
                    }
                }

                @Override
                public void onScanFailed(int errorCode) {
                    if (!shouldRun) return;
                    postStatus("Scan failed (code: " + errorCode + "). Retrying…");
                    scheduleRetry();
                }
            };
            scanner.startScan(scanCallback);

            // Timeout for scan
            main.postDelayed(() -> {
                if (shouldRun && !isConnected && targetDevice == null && scanCallback != null) {
                    try {
                        scanner.stopScan(scanCallback);
                    } catch (Exception ignored) {}
                    postStatus("No devices found. Retrying…");
                    scheduleRetry();
                }
            }, DEFAULT_TIMEOUT_SEC * 1000L);

        } catch (SecurityException e) {
            Log.e(TAG, "startScan: permission issue", e);
            postError("Bluetooth permission denied: " + e.getMessage());
        } catch (Exception e) {
            Log.e(TAG, "startScan: error", e);
            postError("Scan error: " + e.getMessage());
            scheduleRetry();
        }
    }

    // ===== Connection & GATT =====

    private void connectToDevice() {
        if (!shouldRun || targetDevice == null) return;

        try {
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
                if (!hasPermission(Manifest.permission.BLUETOOTH_CONNECT)) {
                    postError("BLUETOOTH_CONNECT permission denied");
                    return;
                }
            }

            // Ensure any previous GATT is closed
            if (gatt != null) {
                Log.d(TAG, "Closing previous GATT before new connection");
                try {
                    gatt.disconnect();
                    gatt.close();
                } catch (Exception e) {
                    Log.w(TAG, "Error closing previous GATT", e);
                }
                gatt = null;
            }
            postStatus("Connecting…");
            gatt = targetDevice.connectGatt(appCtx, false, gattCallback);

        } catch (SecurityException e) {
            Log.e(TAG, "connectToDevice: permission issue", e);
            postError("Connection permission denied");
        } catch (Exception e) {
            Log.e(TAG, "connectToDevice: error", e);
            postError("Connection failed: " + e.getMessage());
            scheduleRetry();
        }
    }

    private final BluetoothGattCallback gattCallback = new BluetoothGattCallback() {
        @Override
        public void onConnectionStateChange(BluetoothGatt gatt, int status, int newState) {
            if (!shouldRun) return;

            if (newState == BluetoothProfile.STATE_CONNECTED) {
                isConnected = true;
                postStatus("Connected. Discovering services…");
                // Request high priority connection for better stability
                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
                    try {
                        gatt.requestConnectionPriority(BluetoothGatt.CONNECTION_PRIORITY_HIGH);
                    } catch (Exception e) {
                        Log.w(TAG, "Failed to request connection priority", e);
                    }
                }
                try {
                    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
                        if (!hasPermission(Manifest.permission.BLUETOOTH_CONNECT)) return;
                    }
                    // Small delay before service discovery for stability
                    main.postDelayed(() -> {
                        if (gatt != null && shouldRun) {
                            gatt.discoverServices();
                        }
                    }, 300);
                } catch (SecurityException e) {
                    Log.e(TAG, "onConnectionStateChange: permission", e);
                }
            } else if (newState == BluetoothProfile.STATE_DISCONNECTED) {
                postStatus("Disconnected.");
                closeGatt();
                if (shouldRun) scheduleRetry();
            }
        }

        @Override
        public void onServicesDiscovered(BluetoothGatt gatt, int status) {
            if (!shouldRun) return;

            if (status != BluetoothGatt.GATT_SUCCESS) {
                postError("Service discovery failed (status: " + status + ")");
                closeGatt();
                scheduleRetry();
                return;
            }

            postStatus("Services discovered. Enabling notifications…");
            enableNotifications(gatt);
        }

        @Override
        public void onCharacteristicChanged(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic,
                                            byte[] value) {
            if (!shouldRun) return;

            Log.d(TAG, "onCharacteristicChanged: " + bytesToHex(value));
            handleTempData(value);
        }

        @Override
        public void onCharacteristicWrite(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic,
                                          int status) {
            if (!shouldRun) return;

            if (status != BluetoothGatt.GATT_SUCCESS) {
                Log.w(TAG, "onCharacteristicWrite failed: " + status);
            }
        }

        @Override
        public void onDescriptorWrite(BluetoothGatt gatt, BluetoothGattDescriptor descriptor, int status) {
            if (!shouldRun) return;

            if (status == BluetoothGatt.GATT_SUCCESS) {
                postStatus("Waiting for temperature reading…");
//                startKeepalive();
            } else {
                postError("Failed to enable notifications (status: " + status + ")");
                closeGatt();
                scheduleRetry();
            }
        }
    };

    private void enableNotifications(BluetoothGatt gatt) {
        try {
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
                if (!hasPermission(Manifest.permission.BLUETOOTH_CONNECT)) return;
            }

            BluetoothGattService service = gatt.getService(SERVICE_UUID);
            if (service == null) {
                postError("Service 0xFFF0 not found");
                closeGatt();
                scheduleRetry();
                return;
            }

            BluetoothGattCharacteristic characteristic = service.getCharacteristic(CHAR_UUID);
            if (characteristic == null) {
                postError("Characteristic 0xFFF3 not found");
                closeGatt();
                scheduleRetry();
                return;
            }

            // Enable notifications
            gatt.setCharacteristicNotification(characteristic, true);

            BluetoothGattDescriptor descriptor = characteristic.getDescriptor(
                    UUID.fromString("00002902-0000-1000-8000-00805f9b34fb")  // CCCD
            );
            if (descriptor != null) {
                descriptor.setValue(BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE);
                gatt.writeDescriptor(descriptor);
            }

        } catch (SecurityException e) {
            Log.e(TAG, "enableNotifications: permission", e);
        } catch (Exception e) {
            Log.e(TAG, "enableNotifications: error", e);
            postError("Notification setup failed: " + e.getMessage());
            closeGatt();
            scheduleRetry();
        }
    }
//    private void startKeepalive() {
//        // Cancel any existing keepalive
//        if (keepaliveRunnable != null) {
//            main.removeCallbacks(keepaliveRunnable);
//        }
//
//        keepaliveRunnable = new Runnable() {
//            @Override
//            public void run() {
//                if (shouldRun && isConnected && gatt != null) {
//                    try {
//                        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
//                            if (!hasPermission(Manifest.permission.BLUETOOTH_CONNECT)) {
//                                return;
//                            }
//                        }
//
//                        // Read RSSI as a lightweight keepalive operation
//                        boolean readStarted = gatt.readRemoteRssi();
//                        if (!readStarted) {
//                            Log.w(TAG, "Keepalive: readRemoteRssi returned false");
//                        }
//                    } catch (SecurityException e) {
//                        Log.w(TAG, "Keepalive: permission denied", e);
//                    } catch (Exception e) {
//                        Log.w(TAG, "Keepalive: error", e);
//                    }
//
//                    // Schedule next keepalive
//                    main.postDelayed(this, KEEPALIVE_INTERVAL_MS);
//                }
//            }
//        };
//
//        // Start first keepalive
//        main.postDelayed(keepaliveRunnable, KEEPALIVE_INTERVAL_MS);
//        Log.d(TAG, "Keepalive started (interval: " + (KEEPALIVE_INTERVAL_MS / 1000) + "s)");
//    }
    // ===== Temperature Data Parsing =====

    private void handleTempData(byte[] data) {
        if (data == null || data.length < 5) {
            Log.w(TAG, "Invalid data length: " + (data != null ? data.length : 0));
            return;
        }

        try {
            // Parse frame
            byte startCode = data[0];
            byte typeCode = data[1];
            int tempRaw = ((data[2] & 0xFF) << 8) | (data[3] & 0xFF);
            byte checkCode = data[4];

            // Validate start code
            if (startCode != START_CODE) {
                Log.w(TAG, "Invalid start code: " + String.format("0x%02x", startCode));
                return;
            }

            // Validate checksum
            if (!validateChecksum(data, checkCode)) {
                Log.w(TAG, "Checksum validation failed");
                return;
            }

            // Convert temperature: divide by 10
            float temperature = tempRaw / 100.0f;
            temperature = celsiusToFahrenheit(temperature);


            // Get temperature type name for logging
            String tempType = getTemperatureTypeName(typeCode);
            Log.d(TAG, "Temperature (" + tempType + "): " + temperature + "°C");

            // Post the value
            postValue(BleDeviceManager.VitalType.BODY_TEMPERATURE, temperature);
            postStatus("Got " + tempType + ": " + temperature + "°C");

            // Keep listening for more readings
            // (don't close on success - device may send multiple readings)

        } catch (Exception e) {
            Log.e(TAG, "handleTempData: error", e);
            postError("Data parse error: " + e.getMessage());
        }
    }
    private float celsiusToFahrenheit(float celsius) {
        return (celsius * 9.0f / 5.0f) + 32.0f;
    }
    private boolean validateChecksum(byte[] data, byte receivedCheck) {
        int sum = 0;
        // Sum first 4 bytes
        for (int i = 0; i < 4; i++) {
            sum += (data[i] & 0xFF);
        }
        byte calculatedCheck = (byte) (sum & 0xFF);
        return calculatedCheck == receivedCheck;
    }

    private String getTemperatureTypeName(byte typeCode) {
        switch (typeCode) {
            case TYPE_EAR:
                return "Ear";
            case TYPE_FOREHEAD:
                return "Forehead";
            case TYPE_OBJECT:
                return "Object";
            default:
                return "Unknown (0x" + String.format("%02x", typeCode) + ")";
        }
    }

    // ===== Helpers =====

    private void scheduleRetry() {
        if (!shouldRun) return;
        if (attempts >= SCAN_RETRY_MAX) {
            postError("Max retry attempts reached.");
            return;
        }

        // Clean up before retry
        targetDevice = null;

        // Ensure GATT is closed before rescanning
        if (gatt != null) {
            try {
                gatt.close();
            } catch (Exception e) {
                Log.w(TAG, "Error closing GATT in scheduleRetry", e);
            }
            gatt = null;
        }

        postStatus("Retrying in " + (RESCAN_DELAY_MS / 1000f) + "s…");
        main.postDelayed(this::startScan, RESCAN_DELAY_MS);
    }

    private void closeGatt() {
        try {
            if (gatt != null) {
                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
                    if (hasPermission(Manifest.permission.BLUETOOTH_CONNECT)) {
                        gatt.disconnect();
                        // Give disconnect time to complete
                        try { Thread.sleep(100); } catch (InterruptedException ignored) {}
                        gatt.close();
                    }
                } else {
                    gatt.disconnect();
                    // Give disconnect time to complete
                    try { Thread.sleep(100); } catch (InterruptedException ignored) {}
                    gatt.close();
                }
            }
        } catch (SecurityException e) {
            Log.e(TAG, "closeGatt: permission", e);
        } finally {
            gatt = null;
            isConnected = false;
        }
    }

    private boolean hasPermission(String permission) {
        return ContextCompat.checkSelfPermission(appCtx, permission) == PackageManager.PERMISSION_GRANTED;
    }

    private String bytesToHex(byte[] bytes) {
        if (bytes == null) return "";
        StringBuilder sb = new StringBuilder();
        for (byte b : bytes) {
            sb.append(String.format("%02x ", b));
        }
        return sb.toString().trim();
    }

    private void postStatus(String s) {
        if (cb != null) cb.postStatus(s);
    }

    private void postError(String e) {
        if (cb != null) cb.postError(e);
    }

    private void postValue(BleDeviceManager.VitalType t, float v) {
        if (cb != null) cb.postValue(t, v);
    }
}

0

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.