Vollständige Klasse (sensible Werte geschwärzt):
Code: Select all
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("");
private static final UUID CHAR_UUID = UUID.fromString("");
// private static final UUID CCCD_UUID = UUID.fromString("");
// 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)
Mobile version